// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::ops::*; use crate::OpResult; use crate::PromiseId; use anyhow::Error; use futures::future::Either; use futures::future::Future; use futures::future::FutureExt; use futures::task::noop_waker_ref; use std::borrow::Cow; use std::cell::RefCell; use std::future::ready; use std::mem::MaybeUninit; use std::option::Option; use std::task::Context; use std::task::Poll; #[inline] pub fn queue_fast_async_op( ctx: &OpCtx, promise_id: PromiseId, op: impl Future> + 'static, ) { let get_class = { let state = RefCell::borrow(&ctx.state); state.tracker.track_async(ctx.id); state.get_error_class_fn }; let fut = op.map(|result| crate::_ops::to_op_result(get_class, result)); ctx .context_state .borrow_mut() .pending_ops .spawn(OpCall::new(ctx, promise_id, fut)); } #[inline] pub fn map_async_op1( ctx: &OpCtx, op: impl Future> + 'static, ) -> impl Future { let get_class = { let state = RefCell::borrow(&ctx.state); state.tracker.track_async(ctx.id); state.get_error_class_fn }; op.map(|res| crate::_ops::to_op_result(get_class, res)) } #[inline] pub fn map_async_op2( ctx: &OpCtx, op: impl Future + 'static, ) -> impl Future { let state = RefCell::borrow(&ctx.state); state.tracker.track_async(ctx.id); op.map(|res| OpResult::Ok(res.into())) } #[inline] pub fn map_async_op3( ctx: &OpCtx, op: Result> + 'static, Error>, ) -> impl Future { let get_class = { let state = RefCell::borrow(&ctx.state); state.tracker.track_async(ctx.id); state.get_error_class_fn }; match op { Err(err) => { Either::Left(ready(OpResult::Err(OpError::new(get_class, err)))) } Ok(fut) => { Either::Right(fut.map(|res| crate::_ops::to_op_result(get_class, res))) } } } #[inline] pub fn map_async_op4( ctx: &OpCtx, op: Result + 'static, Error>, ) -> impl Future { let get_class = { let state = RefCell::borrow(&ctx.state); state.tracker.track_async(ctx.id); state.get_error_class_fn }; match op { Err(err) => { Either::Left(ready(OpResult::Err(OpError::new(get_class, err)))) } Ok(fut) => Either::Right(fut.map(|r| OpResult::Ok(r.into()))), } } pub fn queue_async_op<'s>( ctx: &OpCtx, scope: &'s mut v8::HandleScope, deferred: bool, promise_id: PromiseId, op: impl Future + 'static, ) -> Option> { // An op's realm (as given by `OpCtx::realm_idx`) must match the realm in // which it is invoked. Otherwise, we might have cross-realm object exposure. // deno_core doesn't currently support such exposure, even though embedders // can cause them, so we panic in debug mode (since the check is expensive). // TODO(mmastrac): Restore this // debug_assert_eq!( // runtime_state.borrow().context(ctx.realm_idx as usize, scope), // Some(scope.get_current_context()) // ); let id = ctx.id; // TODO(mmastrac): We have to poll every future here because that assumption is baked into a large number // of ops. If we can figure out a way around this, we can remove this call to boxed_local and save a malloc per future. let mut pinned = op.map(move |res| (promise_id, id, res)).boxed_local(); match pinned.poll_unpin(&mut Context::from_waker(noop_waker_ref())) { Poll::Pending => {} Poll::Ready(mut res) => { if deferred { ctx.context_state.borrow_mut().pending_ops.spawn(ready(res)); return None; } else { ctx.state.borrow_mut().tracker.track_async_completed(ctx.id); return Some(res.2.to_v8(scope).unwrap()); } } } ctx.context_state.borrow_mut().pending_ops.spawn(pinned); None } macro_rules! try_number { ($n:ident $type:ident $is:ident) => { if $n.$is() { // SAFETY: v8 handles can be transmuted let n: &v8::Uint32 = unsafe { std::mem::transmute($n) }; return n.value() as _; } }; } pub fn to_u32(number: &v8::Value) -> u32 { try_number!(number Uint32 is_uint32); try_number!(number Int32 is_int32); try_number!(number Number is_number); if number.is_big_int() { // SAFETY: v8 handles can be transmuted let n: &v8::BigInt = unsafe { std::mem::transmute(number) }; return n.u64_value().0 as _; } 0 } pub fn to_i32(number: &v8::Value) -> i32 { try_number!(number Uint32 is_uint32); try_number!(number Int32 is_int32); try_number!(number Number is_number); if number.is_big_int() { // SAFETY: v8 handles can be transmuted let n: &v8::BigInt = unsafe { std::mem::transmute(number) }; return n.i64_value().0 as _; } 0 } #[allow(unused)] pub fn to_u64(number: &v8::Value) -> u32 { try_number!(number Uint32 is_uint32); try_number!(number Int32 is_int32); try_number!(number Number is_number); if number.is_big_int() { // SAFETY: v8 handles can be transmuted let n: &v8::BigInt = unsafe { std::mem::transmute(number) }; return n.u64_value().0 as _; } 0 } #[allow(unused)] pub fn to_i64(number: &v8::Value) -> i32 { try_number!(number Uint32 is_uint32); try_number!(number Int32 is_int32); try_number!(number Number is_number); if number.is_big_int() { // SAFETY: v8 handles can be transmuted let n: &v8::BigInt = unsafe { std::mem::transmute(number) }; return n.i64_value().0 as _; } 0 } /// Expands `inbuf` to `outbuf`, assuming that `outbuf` has at least 2x `input_length`. #[inline(always)] unsafe fn latin1_to_utf8( input_length: usize, inbuf: *const u8, outbuf: *mut u8, ) -> usize { let mut output = 0; let mut input = 0; while input < input_length { let char = *(inbuf.add(input)); if char < 0x80 { *(outbuf.add(output)) = char; output += 1; } else { // Top two bits *(outbuf.add(output)) = (char >> 6) | 0b1100_0000; // Bottom six bits *(outbuf.add(output + 1)) = (char & 0b0011_1111) | 0b1000_0000; output += 2; } input += 1; } output } /// Converts a [`v8::fast_api::FastApiOneByteString`] to either an owned string, or a borrowed string, depending on whether it fits into the /// provided buffer. pub fn to_str_ptr<'a, const N: usize>( string: &mut v8::fast_api::FastApiOneByteString, buffer: &'a mut [MaybeUninit; N], ) -> Cow<'a, str> { let input_buf = string.as_bytes(); let input_len = input_buf.len(); let output_len = buffer.len(); // We know that this string is full of either one or two-byte UTF-8 chars, so if it's < 1/2 of N we // can skip the ASCII check and just start copying. if input_len < N / 2 { debug_assert!(output_len >= input_len * 2); let buffer = buffer.as_mut_ptr() as *mut u8; let written = // SAFETY: We checked that buffer is at least 2x the size of input_buf unsafe { latin1_to_utf8(input_buf.len(), input_buf.as_ptr(), buffer) }; debug_assert!(written <= output_len); let slice = std::ptr::slice_from_raw_parts(buffer, written); // SAFETY: We know it's valid UTF-8, so make a string Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(&*slice) }) } else { // TODO(mmastrac): We could be smarter here about not allocating Cow::Owned(to_string_ptr(string)) } } /// Converts a [`v8::fast_api::FastApiOneByteString`] to an owned string. May over-allocate to avoid /// re-allocation. pub fn to_string_ptr( string: &mut v8::fast_api::FastApiOneByteString, ) -> String { let input_buf = string.as_bytes(); let capacity = input_buf.len() * 2; // SAFETY: We're allocating a buffer of 2x the input size, writing valid UTF-8, then turning that into a string unsafe { // Create an uninitialized buffer of `capacity` bytes. We need to be careful here to avoid // accidentally creating a slice of u8 which would be invalid. let layout = std::alloc::Layout::from_size_align(capacity, 1).unwrap(); let out = std::alloc::alloc(layout); let written = latin1_to_utf8(input_buf.len(), input_buf.as_ptr(), out); debug_assert!(written <= capacity); // We know it's valid UTF-8, so make a string String::from_raw_parts(out, written, capacity) } } /// Converts a [`v8::String`] to either an owned string, or a borrowed string, depending on whether it fits into the /// provided buffer. #[inline(always)] pub fn to_str<'a, const N: usize>( scope: &mut v8::Isolate, string: &v8::Value, buffer: &'a mut [MaybeUninit; N], ) -> Cow<'a, str> { if !string.is_string() { return Cow::Borrowed(""); } // SAFETY: We checked is_string above let string: &v8::String = unsafe { std::mem::transmute(string) }; string.to_rust_cow_lossy(scope, buffer) } #[cfg(test)] mod tests { use crate::error::generic_error; use crate::error::AnyError; use crate::error::JsError; use crate::FastString; use crate::JsRuntime; use crate::RuntimeOptions; use deno_ops::op2; use std::borrow::Cow; use std::cell::Cell; crate::extension!( testing, ops = [ op_test_fail, op_test_add, op_test_add_option, op_test_result_void_switch, op_test_result_void_ok, op_test_result_void_err, op_test_result_primitive_ok, op_test_result_primitive_err, op_test_string_owned, op_test_string_ref, op_test_string_cow, op_test_string_roundtrip_char, op_test_string_return, op_test_string_option_return, op_test_string_roundtrip, op_test_generics, ] ); thread_local! { static FAIL: Cell = Cell::new(false) } #[op2(core, fast)] pub fn op_test_fail() { FAIL.with(|b| b.set(true)) } /// Run a test for a single op. fn run_test2(repeat: usize, op: &str, test: &str) -> Result<(), AnyError> { let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![testing::init_ops_and_esm()], ..Default::default() }); runtime .execute_script( "", FastString::Owned( format!( r" const {{ op_test_fail, {op} }} = Deno.core.ensureFastOps(); function assert(b) {{ if (!b) {{ op_test_fail(); }} }} " ) .into(), ), ) .unwrap(); FAIL.with(|b| b.set(false)); runtime.execute_script( "", FastString::Owned( format!( r" for (let __index__ = 0; __index__ < {repeat}; __index__++) {{ {test} }} " ) .into(), ), )?; if FAIL.with(|b| b.get()) { Err(generic_error(format!("{op} test failed ({test})"))) } else { Ok(()) } } #[tokio::test(flavor = "current_thread")] pub async fn test_op_fail() { assert!(run_test2(1, "", "assert(false)").is_err()); } #[op2(core, fast)] pub fn op_test_add(a: u32, b: u32) -> u32 { a + b } #[tokio::test(flavor = "current_thread")] pub async fn test_op_add() -> Result<(), Box> { Ok(run_test2( 10000, "op_test_add", "assert(op_test_add(1, 11) == 12)", )?) } #[op2(core)] pub fn op_test_add_option(a: u32, b: Option) -> u32 { a + b.unwrap_or(100) } #[tokio::test(flavor = "current_thread")] pub async fn test_op_add_option() -> Result<(), Box> { // This isn't fast, so we don't repeat it run_test2( 1, "op_test_add_option", "assert(op_test_add_option(1, 11) == 12)", )?; run_test2( 1, "op_test_add_option", "assert(op_test_add_option(1, null) == 101)", )?; Ok(()) } thread_local! { static RETURN_COUNT: Cell = Cell::new(0); } #[op2(core, fast)] pub fn op_test_result_void_switch() -> Result<(), AnyError> { let count = RETURN_COUNT.with(|count| { let new = count.get() + 1; count.set(new); new }); if count > 5000 { Err(generic_error("failed!!!")) } else { Ok(()) } } #[op2(core, fast)] pub fn op_test_result_void_err() -> Result<(), AnyError> { Err(generic_error("failed!!!")) } #[op2(core, fast)] pub fn op_test_result_void_ok() -> Result<(), AnyError> { Ok(()) } #[tokio::test(flavor = "current_thread")] pub async fn test_op_result_void() -> Result<(), Box> { // Test the non-switching kinds run_test2( 10000, "op_test_result_void_err", "try { op_test_result_void_err(); assert(false) } catch (e) {}", )?; run_test2(10000, "op_test_result_void_ok", "op_test_result_void_ok()")?; Ok(()) } #[tokio::test(flavor = "current_thread")] pub async fn test_op_result_void_switch( ) -> Result<(), Box> { RETURN_COUNT.with(|count| count.set(0)); let err = run_test2( 10000, "op_test_result_void_switch", "op_test_result_void_switch();", ) .expect_err("Expected this to fail"); let js_err = err.downcast::().unwrap(); assert_eq!(js_err.message, Some("failed!!!".into())); assert_eq!(RETURN_COUNT.with(|count| count.get()), 5001); Ok(()) } #[op2(core, fast)] pub fn op_test_result_primitive_err() -> Result { Err(generic_error("failed!!!")) } #[op2(core, fast)] pub fn op_test_result_primitive_ok() -> Result { Ok(123) } #[tokio::test] pub async fn test_op_result_primitive( ) -> Result<(), Box> { run_test2( 10000, "op_test_result_primitive_err", "try { op_test_result_primitive_err(); assert(false) } catch (e) {}", )?; run_test2( 10000, "op_test_result_primitive_ok", "op_test_result_primitive_ok()", )?; Ok(()) } #[op2(core, fast)] pub fn op_test_string_owned(#[string] s: String) -> u32 { s.len() as _ } #[op2(core, fast)] pub fn op_test_string_ref(#[string] s: &str) -> u32 { s.len() as _ } #[op2(core, fast)] pub fn op_test_string_cow(#[string] s: Cow) -> u32 { s.len() as _ } #[op2(core, fast)] pub fn op_test_string_roundtrip_char(#[string] s: Cow) -> u32 { s.chars().next().unwrap() as u32 } #[tokio::test] pub async fn test_op_strings() -> Result<(), Box> { for op in [ "op_test_string_owned", "op_test_string_cow", "op_test_string_ref", ] { for (len, str) in [ // ASCII (3, "'abc'"), // Latin-1 (one byte but two UTF-8 chars) (2, "'\\u00a0'"), // ASCII (10000, "'a'.repeat(10000)"), // Latin-1 (20000, "'\\u00a0'.repeat(10000)"), // 4-byte UTF-8 emoji (1F995 = 🦕) (40000, "'\\u{1F995}'.repeat(10000)"), ] { let test = format!("assert({op}({str}) == {len})"); run_test2(10000, op, &test)?; } } // Ensure that we're correctly encoding UTF-8 run_test2( 10000, "op_test_string_roundtrip_char", "assert(op_test_string_roundtrip_char('\\u00a0') == 0xa0)", )?; run_test2( 10000, "op_test_string_roundtrip_char", "assert(op_test_string_roundtrip_char('\\u00ff') == 0xff)", )?; run_test2( 10000, "op_test_string_roundtrip_char", "assert(op_test_string_roundtrip_char('\\u0080') == 0x80)", )?; run_test2( 10000, "op_test_string_roundtrip_char", "assert(op_test_string_roundtrip_char('\\u0100') == 0x100)", )?; Ok(()) } #[op2(core)] #[string] pub fn op_test_string_return( #[string] a: Cow, #[string] b: Cow, ) -> String { (a + b).to_string() } #[op2(core)] #[string] pub fn op_test_string_option_return( #[string] a: Cow, #[string] b: Cow, ) -> Option { if a == "none" { return None; } Some((a + b).to_string()) } #[op2(core)] #[string] pub fn op_test_string_roundtrip(#[string] s: String) -> String { s } #[tokio::test] pub async fn test_op_string_returns() -> Result<(), Box> { run_test2( 1, "op_test_string_return", "assert(op_test_string_return('a', 'b') == 'ab')", )?; run_test2( 1, "op_test_string_option_return", "assert(op_test_string_option_return('a', 'b') == 'ab')", )?; run_test2( 1, "op_test_string_option_return", "assert(op_test_string_option_return('none', 'b') == null)", )?; run_test2( 1, "op_test_string_roundtrip", "assert(op_test_string_roundtrip('\\u0080\\u00a0\\u00ff') == '\\u0080\\u00a0\\u00ff')", )?; Ok(()) } // We don't actually test this one -- we just want it to compile #[op2(core, fast)] pub fn op_test_generics() {} }