diff --git a/core/ops.rs b/core/ops.rs index a5c76e412d..361fc3cb77 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -14,6 +14,7 @@ use futures::Future; use pin_project::pin_project; use serde::Serialize; use std::cell::RefCell; +use std::cell::UnsafeCell; use std::ops::Deref; use std::ops::DerefMut; use std::ptr::NonNull; @@ -106,7 +107,10 @@ pub fn to_op_result( } } -// TODO(@AaronO): optimize OpCtx(s) mem usage ? +/// Per-op context. +/// +// Note: We don't worry too much about the size of this struct because it's allocated once per realm, and is +// stored in a contiguous array. pub struct OpCtx { pub id: OpId, pub state: Rc>, @@ -114,6 +118,8 @@ pub struct OpCtx { pub fast_fn_c_info: Option>, pub runtime_state: Weak>, pub(crate) context_state: Rc>, + /// If the last fast op failed, stores the error to be picked up by the slow op. + pub(crate) last_fast_error: UnsafeCell>, } impl OpCtx { @@ -145,8 +151,35 @@ impl OpCtx { decl, context_state, fast_fn_c_info, + last_fast_error: UnsafeCell::new(None), } } + + /// This takes the last error from an [`OpCtx`], assuming that no other code anywhere + /// can hold a `&mut` to the last_fast_error field. + /// + /// # Safety + /// + /// Must only be called from op implementations. + #[inline(always)] + pub unsafe fn unsafely_take_last_error_for_ops_only( + &self, + ) -> Option { + let opt_mut = &mut *self.last_fast_error.get(); + opt_mut.take() + } + + /// This set the last error for an [`OpCtx`], assuming that no other code anywhere + /// can hold a `&mut` to the last_fast_error field. + /// + /// # Safety + /// + /// Must only be called from op implementations. + #[inline(always)] + pub unsafe fn unsafely_set_last_error_for_ops_only(&self, error: AnyError) { + let opt_mut = &mut *self.last_fast_error.get(); + *opt_mut = Some(error); + } } /// Maintains the resources and ops inside a JS runtime. diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs index 76a29c5c3c..84b578aeb0 100644 --- a/core/runtime/ops.rs +++ b/core/runtime/ops.rs @@ -215,12 +215,15 @@ mod tests { use crate::JsRuntime; use crate::RuntimeOptions; use deno_ops::op2; + 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, @@ -228,47 +231,84 @@ mod tests { ] ); + thread_local! { + static FAIL: Cell = Cell::new(false) + } + + #[op2(core, fast)] + pub fn op_test_fail() { + FAIL.with(|b| { + println!("fail"); + b.set(true) + }) + } + /// Run a test for a single op. - fn run_test( + fn run_test2( + repeat: usize, op: &'static str, test: &'static str, - f: impl FnOnce(Result<&v8::Value, anyhow::Error>, &mut v8::HandleScope), - ) { + ) -> Result<(), AnyError> { let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![testing::init_ext()], ..Default::default() }); - let value: Result, anyhow::Error> = runtime + runtime .execute_script( "", FastString::Owned( - format!("const {{ {op} }} = Deno.core.ensureFastOps(); {test}") - .into(), + format!( + r" + const {{ op_test_fail, {op} }} = Deno.core.ensureFastOps(); + function assert(b) {{ + if (!b) {{ + op_test_fail(); + }} + }} + " + ) + .into(), ), - ); - let mut scope: v8::HandleScope = - // SAFETY: transmute for test (this lifetime should be safe for this purpose) - unsafe { std::mem::transmute(runtime.handle_scope()) }; - match value { - Ok(value) => { - let value = value.open(&mut scope); - f(Ok(value), &mut scope) - } - Err(err) => f(Err(err), &mut scope), + ) + .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("test failed")) + } 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] + #[tokio::test(flavor = "current_thread")] pub async fn test_op_add() -> Result<(), Box> { - run_test("op_test_add", "op_test_add(1, 11)", |value, scope| { - assert_eq!(value.unwrap().int32_value(scope), Some(12)); - }); - Ok(()) + Ok(run_test2( + 10000, + "op_test_add", + "assert(op_test_add(1, 11) == 12)", + )?) } #[op2(core)] @@ -276,59 +316,84 @@ mod tests { a + b.unwrap_or(100) } - #[tokio::test] + #[tokio::test(flavor = "current_thread")] pub async fn test_op_add_option() -> Result<(), Box> { - run_test( + // This isn't fast, so we don't repeat it + run_test2( + 1, "op_test_add_option", - "op_test_add_option(1, 11)", - |value, scope| { - assert_eq!(value.unwrap().int32_value(scope), Some(12)); - }, - ); - run_test( + "assert(op_test_add_option(1, 11) == 12)", + )?; + run_test2( + 1, "op_test_add_option", - "op_test_add_option(1, null)", - |value, scope| { - assert_eq!(value.unwrap().int32_value(scope), Some(101)); - }, - ); + "assert(op_test_add_option(1, null) == 101)", + )?; Ok(()) } - #[op2(core)] + 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)] + #[op2(core, fast)] pub fn op_test_result_void_ok() -> Result<(), AnyError> { Ok(()) } - #[tokio::test] + #[tokio::test(flavor = "current_thread")] pub async fn test_op_result_void() -> Result<(), Box> { - run_test( + // Test the non-switching kinds + run_test2( + 10000, "op_test_result_void_err", - "op_test_result_void_err()", - |value, _scope| { - let js_error = value.err().unwrap().downcast::().unwrap(); - assert_eq!(js_error.message, Some("failed!!!".to_owned())); - }, - ); - run_test( - "op_test_result_void_ok", - "op_test_result_void_ok()", - |value, _scope| assert!(value.unwrap().is_null_or_undefined()), - ); + "try { op_test_result_void_err(); assert(false) } catch (e) {}", + )?; + run_test2(10000, "op_test_result_void_ok", "op_test_result_void_ok()")?; Ok(()) } - #[op2(core)] + #[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)] + #[op2(core, fast)] pub fn op_test_result_primitive_ok() -> Result { Ok(123) } @@ -336,19 +401,16 @@ mod tests { #[tokio::test] pub async fn test_op_result_primitive( ) -> Result<(), Box> { - run_test( + run_test2( + 10000, "op_test_result_primitive_err", - "op_test_result_primitive_err()", - |value, _scope| { - let js_error = value.err().unwrap().downcast::().unwrap(); - assert_eq!(js_error.message, Some("failed!!!".to_owned())); - }, - ); - run_test( + "try { op_test_result_primitive_err(); assert(false) } catch (e) {}", + )?; + run_test2( + 10000, "op_test_result_primitive_ok", "op_test_result_primitive_ok()", - |value, scope| assert_eq!(value.unwrap().int32_value(scope), Some(123)), - ); + )?; Ok(()) } } diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs index 94140dbf6f..5262196f4c 100644 --- a/ops/op2/dispatch_fast.rs +++ b/ops/op2/dispatch_fast.rs @@ -11,7 +11,7 @@ use quote::quote; #[allow(unused)] #[derive(Debug, Default, PartialEq, Clone)] -pub(crate) enum FastValue { +pub(crate) enum V8FastCallType { #[default] Void, Bool, @@ -27,66 +27,74 @@ pub(crate) enum FastValue { Uint32Array, Float64Array, SeqOneByteString, + CallbackOptions, } -impl FastValue { +impl V8FastCallType { /// Quote fast value type. - fn quote_rust_type(&self) -> TokenStream { + fn quote_rust_type(&self, deno_core: &TokenStream) -> TokenStream { match self { - FastValue::Void => quote!(()), - FastValue::Bool => quote!(bool), - FastValue::U32 => quote!(u32), - FastValue::I32 => quote!(i32), - FastValue::U64 => quote!(u64), - FastValue::I64 => quote!(i64), - FastValue::F32 => quote!(f32), - FastValue::F64 => quote!(f64), - FastValue::Pointer => quote!(*mut ::std::ffi::c_void), - FastValue::V8Value => unimplemented!("v8::Local"), - FastValue::Uint8Array - | FastValue::Uint32Array - | FastValue::Float64Array - | FastValue::SeqOneByteString => unreachable!(), + V8FastCallType::Void => quote!(()), + V8FastCallType::Bool => quote!(bool), + V8FastCallType::U32 => quote!(u32), + V8FastCallType::I32 => quote!(i32), + V8FastCallType::U64 => quote!(u64), + V8FastCallType::I64 => quote!(i64), + V8FastCallType::F32 => quote!(f32), + V8FastCallType::F64 => quote!(f64), + V8FastCallType::Pointer => quote!(*mut ::std::ffi::c_void), + V8FastCallType::V8Value => { + quote!(#deno_core::v8::Local<#deno_core::v8::Value>) + } + V8FastCallType::CallbackOptions => { + quote!(*mut #deno_core::v8::fast_api::FastApiCallbackOptions) + } + V8FastCallType::Uint8Array + | V8FastCallType::Uint32Array + | V8FastCallType::Float64Array + | V8FastCallType::SeqOneByteString => unreachable!(), } } /// Quote fast value type's variant. fn quote_ctype(&self) -> TokenStream { match &self { - FastValue::Void => quote!(CType::Void), - FastValue::Bool => quote!(CType::Bool), - FastValue::U32 => quote!(CType::Uint32), - FastValue::I32 => quote!(CType::Int32), - FastValue::U64 => quote!(CType::Uint64), - FastValue::I64 => quote!(CType::Int64), - FastValue::F32 => quote!(CType::Float32), - FastValue::F64 => quote!(CType::Float64), - FastValue::Pointer => quote!(CType::Pointer), - FastValue::V8Value => quote!(CType::V8Value), - FastValue::Uint8Array => unreachable!(), - FastValue::Uint32Array => unreachable!(), - FastValue::Float64Array => unreachable!(), - FastValue::SeqOneByteString => quote!(CType::SeqOneByteString), + V8FastCallType::Void => quote!(CType::Void), + V8FastCallType::Bool => quote!(CType::Bool), + V8FastCallType::U32 => quote!(CType::Uint32), + V8FastCallType::I32 => quote!(CType::Int32), + V8FastCallType::U64 => quote!(CType::Uint64), + V8FastCallType::I64 => quote!(CType::Int64), + V8FastCallType::F32 => quote!(CType::Float32), + V8FastCallType::F64 => quote!(CType::Float64), + V8FastCallType::Pointer => quote!(CType::Pointer), + V8FastCallType::V8Value => quote!(CType::V8Value), + V8FastCallType::CallbackOptions => quote!(CType::CallbackOptions), + V8FastCallType::Uint8Array => unreachable!(), + V8FastCallType::Uint32Array => unreachable!(), + V8FastCallType::Float64Array => unreachable!(), + V8FastCallType::SeqOneByteString => quote!(CType::SeqOneByteString), } } /// Quote fast value type's variant. fn quote_type(&self) -> TokenStream { match &self { - FastValue::Void => quote!(Type::Void), - FastValue::Bool => quote!(Type::Bool), - FastValue::U32 => quote!(Type::Uint32), - FastValue::I32 => quote!(Type::Int32), - FastValue::U64 => quote!(Type::Uint64), - FastValue::I64 => quote!(Type::Int64), - FastValue::F32 => quote!(Type::Float32), - FastValue::F64 => quote!(Type::Float64), - FastValue::Pointer => quote!(Type::Pointer), - FastValue::V8Value => quote!(Type::V8Value), - FastValue::Uint8Array => quote!(Type::TypedArray(CType::Uint8)), - FastValue::Uint32Array => quote!(Type::TypedArray(CType::Uint32)), - FastValue::Float64Array => quote!(Type::TypedArray(CType::Float64)), - FastValue::SeqOneByteString => quote!(Type::SeqOneByteString), + V8FastCallType::Void => quote!(Type::Void), + V8FastCallType::Bool => quote!(Type::Bool), + V8FastCallType::U32 => quote!(Type::Uint32), + V8FastCallType::I32 => quote!(Type::Int32), + V8FastCallType::U64 => quote!(Type::Uint64), + V8FastCallType::I64 => quote!(Type::Int64), + V8FastCallType::F32 => quote!(Type::Float32), + V8FastCallType::F64 => quote!(Type::Float64), + V8FastCallType::Pointer => quote!(Type::Pointer), + V8FastCallType::V8Value => quote!(Type::V8Value), + V8FastCallType::CallbackOptions => quote!(Type::CallbackOptions), + V8FastCallType::Uint8Array => quote!(Type::TypedArray(CType::Uint8)), + V8FastCallType::Uint32Array => quote!(Type::TypedArray(CType::Uint32)), + V8FastCallType::Float64Array => quote!(Type::TypedArray(CType::Float64)), + V8FastCallType::SeqOneByteString => quote!(Type::SeqOneByteString), } } } @@ -95,104 +103,181 @@ pub fn generate_dispatch_fast( generator_state: &mut GeneratorState, signature: &ParsedSignature, ) -> Result, V8MappingError> { - // Result not fast-call compatible (yet) - if matches!(signature.ret_val, RetVal::Result(..)) { - return Ok(None); - } - let mut inputs = vec![]; for arg in &signature.args { - let fv = match arg { - Arg::OptionNumeric(_) | Arg::SerdeV8(_) => return Ok(None), - Arg::Numeric(NumericArg::bool) => FastValue::Bool, - Arg::Numeric(NumericArg::u32) - | Arg::Numeric(NumericArg::u16) - | Arg::Numeric(NumericArg::u8) => FastValue::U32, - Arg::Numeric(NumericArg::i32) - | Arg::Numeric(NumericArg::i16) - | Arg::Numeric(NumericArg::i8) - | Arg::Numeric(NumericArg::__SMI__) => FastValue::I32, - Arg::Numeric(NumericArg::u64) | Arg::Numeric(NumericArg::usize) => { - FastValue::U64 - } - Arg::Numeric(NumericArg::i64) | Arg::Numeric(NumericArg::isize) => { - FastValue::I64 - } - _ => { - return Err(V8MappingError::NoMapping("a fast argument", arg.clone())) - } + let Some(fv) = map_arg_to_v8_fastcall_type(arg)? else { + return Ok(None); }; inputs.push(fv); } + let mut names = inputs + .iter() + .enumerate() + .map(|(i, _)| format_ident!("arg{i}")) + .collect::>(); let ret_val = match &signature.ret_val { RetVal::Infallible(arg) => arg, RetVal::Result(arg) => arg, }; - let output = match ret_val { - Arg::OptionNumeric(_) | Arg::SerdeV8(_) => return Ok(None), - Arg::Void => FastValue::Void, - Arg::Numeric(NumericArg::bool) => FastValue::Bool, - Arg::Numeric(NumericArg::u32) - | Arg::Numeric(NumericArg::u16) - | Arg::Numeric(NumericArg::u8) => FastValue::U32, - Arg::Numeric(NumericArg::i32) - | Arg::Numeric(NumericArg::i16) - | Arg::Numeric(NumericArg::i8) => FastValue::I32, - Arg::Numeric(NumericArg::u64) | Arg::Numeric(NumericArg::usize) => { - FastValue::U64 - } - Arg::Numeric(NumericArg::i64) | Arg::Numeric(NumericArg::isize) => { - FastValue::I64 - } - Arg::Special(_) => return Ok(None), - _ => { - return Err(V8MappingError::NoMapping( - "a fast return value", - ret_val.clone(), - )) - } + let output = match map_retval_to_v8_fastcall_type(ret_val)? { + None => return Ok(None), + Some(rv) => rv, }; let GeneratorState { fast_function, deno_core, + result, + opctx, + fast_api_callback_options, + needs_fast_api_callback_options, + needs_fast_opctx, .. - } = &generator_state; + } = generator_state; - let input_types = inputs.iter().map(|fv| fv.quote_type()); + let handle_error = match signature.ret_val { + RetVal::Infallible(_) => quote!(), + RetVal::Result(_) => { + *needs_fast_api_callback_options = true; + *needs_fast_opctx = true; + inputs.push(V8FastCallType::CallbackOptions); + quote! { + let #result = match #result { + Ok(#result) => #result, + Err(err) => { + // FASTCALL FALLBACK: This is where we set the errors for the slow-call error pickup path. There + // is no code running between this and the other FASTCALL FALLBACK comment, except some V8 code + // required to perform the fallback process. This is why the below call is safe. + + // The reason we need to do this is because V8 does not allow exceptions to be thrown from the + // fast call. Instead, you are required to set the fallback flag, which indicates to V8 that it + // should re-call the slow version of the function. Technically the slow call should perform the + // same operation and then throw the same error (because it should be idempotent), but in our + // case we stash the error and pick it up on the slow path before doing any work. + + // TODO(mmastrac): We should allow an #[op] flag to re-perform slow calls without the error path when + // the method is performance sensitive. + + // SAFETY: We guarantee that OpCtx has no mutable references once ops are live and being called, + // allowing us to perform this one little bit of mutable magic. + unsafe { #opctx.unsafely_set_last_error_for_ops_only(err); } + #fast_api_callback_options.fallback = true; + return ::std::default::Default::default(); + } + }; + } + } + }; + + let input_types = inputs.iter().map(|fv| fv.quote_type()).collect::>(); let output_type = output.quote_ctype(); let fast_definition = quote! { use #deno_core::v8::fast_api::Type; use #deno_core::v8::fast_api::CType; #deno_core::v8::fast_api::FastFunction::new( - &[ #( #input_types ),* ], + &[ Type::V8Value, #( #input_types ),* ], #output_type, Self::#fast_function as *const ::std::ffi::c_void ) }; - let output_type = output.quote_rust_type(); - let names = &inputs + let output_type = output.quote_rust_type(deno_core); + let mut types = inputs .iter() - .enumerate() - .map(|(i, _)| format_ident!("arg{i}")) + .map(|rv| rv.quote_rust_type(deno_core)) .collect::>(); - let types = inputs.iter().map(|rv| rv.quote_rust_type()); + + let call_args = names.clone(); + + let with_fast_api_callback_options = if *needs_fast_api_callback_options { + types.push(V8FastCallType::CallbackOptions.quote_rust_type(deno_core)); + names.push(fast_api_callback_options.clone()); + quote! { + let #fast_api_callback_options = unsafe { &mut *#fast_api_callback_options }; + } + } else { + quote!() + }; + let with_opctx = if *needs_fast_opctx { + quote!( + let #opctx = unsafe { + &*(#deno_core::v8::Local::::cast(unsafe { #fast_api_callback_options.data.data }).value() + as *const #deno_core::_ops::OpCtx) + }; + ) + } else { + quote!() + }; let fast_fn = quote!( fn #fast_function( _: #deno_core::v8::Local<#deno_core::v8::Object>, #( #names: #types, )* ) -> #output_type { - #( - let #names = #names as _; - )* - Self::call(#(#names),*) + #with_fast_api_callback_options + #with_opctx + let #result = Self::call(#(#call_args as _),*); + #handle_error + #result } ); Ok(Some((fast_definition, fast_fn))) } + +fn map_arg_to_v8_fastcall_type( + arg: &Arg, +) -> Result, V8MappingError> { + let rv = match arg { + Arg::OptionNumeric(_) | Arg::SerdeV8(_) => return Ok(None), + Arg::Numeric(NumericArg::bool) => V8FastCallType::Bool, + Arg::Numeric(NumericArg::u32) + | Arg::Numeric(NumericArg::u16) + | Arg::Numeric(NumericArg::u8) => V8FastCallType::U32, + Arg::Numeric(NumericArg::i32) + | Arg::Numeric(NumericArg::i16) + | Arg::Numeric(NumericArg::i8) + | Arg::Numeric(NumericArg::__SMI__) => V8FastCallType::I32, + Arg::Numeric(NumericArg::u64) | Arg::Numeric(NumericArg::usize) => { + V8FastCallType::U64 + } + Arg::Numeric(NumericArg::i64) | Arg::Numeric(NumericArg::isize) => { + V8FastCallType::I64 + } + _ => return Err(V8MappingError::NoMapping("a fast argument", arg.clone())), + }; + Ok(Some(rv)) +} + +fn map_retval_to_v8_fastcall_type( + arg: &Arg, +) -> Result, V8MappingError> { + let rv = match arg { + Arg::OptionNumeric(_) | Arg::SerdeV8(_) => return Ok(None), + Arg::Void => V8FastCallType::Void, + Arg::Numeric(NumericArg::bool) => V8FastCallType::Bool, + Arg::Numeric(NumericArg::u32) + | Arg::Numeric(NumericArg::u16) + | Arg::Numeric(NumericArg::u8) => V8FastCallType::U32, + Arg::Numeric(NumericArg::i32) + | Arg::Numeric(NumericArg::i16) + | Arg::Numeric(NumericArg::i8) => V8FastCallType::I32, + Arg::Numeric(NumericArg::u64) | Arg::Numeric(NumericArg::usize) => { + V8FastCallType::U64 + } + Arg::Numeric(NumericArg::i64) | Arg::Numeric(NumericArg::isize) => { + V8FastCallType::I64 + } + Arg::Special(_) => return Ok(None), + _ => { + return Err(V8MappingError::NoMapping( + "a fast return value", + arg.clone(), + )) + } + }; + Ok(Some(rv)) +} diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs index f54a28f1c0..f10217a2d2 100644 --- a/ops/op2/dispatch_slow.rs +++ b/ops/op2/dispatch_slow.rs @@ -1,3 +1,4 @@ +use super::MacroConfig; // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use super::generator_state::GeneratorState; use super::signature::Arg; @@ -9,11 +10,31 @@ use super::V8MappingError; use proc_macro2::TokenStream; use quote::quote; -pub fn generate_dispatch_slow( +pub(crate) fn generate_dispatch_slow( + config: &MacroConfig, generator_state: &mut GeneratorState, signature: &ParsedSignature, ) -> Result { let mut output = TokenStream::new(); + + // Fast ops require the slow op to check op_ctx for the last error + if config.fast && matches!(signature.ret_val, RetVal::Result(_)) { + generator_state.needs_opctx = true; + let throw_exception = throw_exception(generator_state)?; + // If the fast op returned an error, we must throw it rather than doing work. + output.extend(quote!{ + // FASTCALL FALLBACK: This is where we pick up the errors for the slow-call error pickup + // path. There is no code running between this and the other FASTCALL FALLBACK comment, + // except some V8 code required to perform the fallback process. This is why the below call is safe. + + // SAFETY: We guarantee that OpCtx has no mutable references once ops are live and being called, + // allowing us to perform this one little bit of mutable magic. + if let Some(err) = unsafe { opctx.unsafely_take_last_error_for_ops_only() } { + #throw_exception + } + }); + } + for (index, arg) in signature.args.iter().enumerate() { output.extend(extract_arg(generator_state, index)?); output.extend(from_arg(generator_state, index, arg)?); @@ -27,6 +48,12 @@ pub fn generate_dispatch_slow( quote!() }; + let with_opctx = if generator_state.needs_opctx { + with_opctx(generator_state) + } else { + quote!() + }; + let with_retval = if generator_state.needs_retval { with_retval(generator_state) } else { @@ -51,6 +78,7 @@ pub fn generate_dispatch_slow( #with_scope #with_retval #with_args + #with_opctx #output }}) @@ -94,9 +122,11 @@ fn with_opctx(generator_state: &mut GeneratorState) -> TokenStream { deno_core, opctx, fn_args, + needs_args, .. - } = &generator_state; + } = generator_state; + *needs_args = true; quote!(let #opctx = unsafe { &*(#deno_core::v8::Local::<#deno_core::v8::External>::cast(#fn_args.data()).value() as *const #deno_core::_ops::OpCtx) @@ -246,56 +276,68 @@ pub fn return_value_infallible( Ok(res) } -pub fn return_value_result( +fn return_value_result( generator_state: &mut GeneratorState, ret_type: &Arg, ) -> Result { let infallible = return_value_infallible(generator_state, ret_type)?; + let exception = throw_exception(generator_state)?; + + let GeneratorState { result, .. } = &generator_state; + + let tokens = quote!( + match #result { + Ok(#result) => { + #infallible + } + Err(err) => { + #exception + } + }; + ); + Ok(tokens) +} + +/// Generates code to throw an exception, adding required additional dependencies as needed. +fn throw_exception( + generator_state: &mut GeneratorState, +) -> Result { let maybe_scope = if generator_state.needs_scope { quote!() } else { with_scope(generator_state) }; - let maybe_args = if generator_state.needs_args { - quote!() - } else { - with_fn_args(generator_state) - }; - let maybe_opctx = if generator_state.needs_opctx { quote!() } else { with_opctx(generator_state) }; + let maybe_args = if generator_state.needs_args { + quote!() + } else { + with_fn_args(generator_state) + }; + let GeneratorState { deno_core, - result, scope, opctx, .. } = &generator_state; - let tokens = quote!( - match #result { - Ok(#result) => { - #infallible - } - Err(err) => { - #maybe_scope - #maybe_args - #maybe_opctx - let opstate = ::std::cell::RefCell::borrow(&*#opctx.state); - let exception = #deno_core::error::to_v8_error( - #scope, - opstate.get_error_class_fn, - &err, - ); - scope.throw_exception(exception); - return; - } - }; - ); - Ok(tokens) + Ok(quote! { + #maybe_scope + #maybe_args + #maybe_opctx + let opstate = ::std::cell::RefCell::borrow(&*#opctx.state); + let exception = #deno_core::error::to_v8_error( + #scope, + opstate.get_error_class_fn, + &err, + ); + scope.throw_exception(exception); + return; + }) } diff --git a/ops/op2/generator_state.rs b/ops/op2/generator_state.rs index 16249c217f..e437ea47c2 100644 --- a/ops/op2/generator_state.rs +++ b/ops/op2/generator_state.rs @@ -21,6 +21,8 @@ pub struct GeneratorState { pub fn_args: Ident, /// The `OpCtx` used for various information required for some ops. pub opctx: Ident, + /// The `FastApiCallbackOptions` used in fast calls for fallback returns. + pub fast_api_callback_options: Ident, /// The `v8::ReturnValue` used in the slow function pub retval: Ident, /// The "slow" function (ie: the one that isn't a fastcall) @@ -33,4 +35,6 @@ pub struct GeneratorState { pub needs_scope: bool, pub needs_opstate: bool, pub needs_opctx: bool, + pub needs_fast_opctx: bool, + pub needs_fast_api_callback_options: bool, } diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs index 558d7c7dcd..67a92d4507 100644 --- a/ops/op2/mod.rs +++ b/ops/op2/mod.rs @@ -50,7 +50,7 @@ pub enum V8MappingError { } #[derive(Default)] -struct MacroConfig { +pub(crate) struct MacroConfig { pub core: bool, pub fast: bool, } @@ -135,6 +135,8 @@ fn generate_op2( let opctx = Ident::new("opctx", Span::call_site()); let slow_function = Ident::new("slow_function", Span::call_site()); let fast_function = Ident::new("fast_function", Span::call_site()); + let fast_api_callback_options = + Ident::new("fast_api_callback_options", Span::call_site()); let deno_core = if config.core { syn2::parse_str::("crate") @@ -151,6 +153,7 @@ fn generate_op2( scope, info, opctx, + fast_api_callback_options, deno_core, result, retval, @@ -161,10 +164,14 @@ fn generate_op2( needs_scope: false, needs_opctx: false, needs_opstate: false, + needs_fast_opctx: false, + needs_fast_api_callback_options: false, }; let name = func.sig.ident; - let slow_fn = generate_dispatch_slow(&mut generator_state, &signature)?; + + let slow_fn = + generate_dispatch_slow(&config, &mut generator_state, &signature)?; let (fast_definition, fast_fn) = match generate_dispatch_fast(&mut generator_state, &signature)? { Some((fast_definition, fast_fn)) => { diff --git a/ops/op2/test_cases/sync/add.out b/ops/op2/test_cases/sync/add.out index a7269c5cf2..7d97a71616 100644 --- a/ops/op2/test_cases/sync/add.out +++ b/ops/op2/test_cases/sync/add.out @@ -13,7 +13,7 @@ impl op_add { use deno_core::v8::fast_api::Type; use deno_core::v8::fast_api::CType; deno_core::v8::fast_api::FastFunction::new( - &[Type::Uint32, Type::Uint32], + &[Type::V8Value, Type::Uint32, Type::Uint32], CType::Uint32, Self::fast_function as *const ::std::ffi::c_void, ) @@ -43,9 +43,8 @@ impl op_add { arg0: u32, arg1: u32, ) -> u32 { - let arg0 = arg0 as _; - let arg1 = arg1 as _; - Self::call(arg0, arg1) + let result = Self::call(arg0 as _, arg1 as _); + result } #[inline(always)] fn call(a: u32, b: u32) -> u32 { diff --git a/ops/op2/test_cases/sync/doc_comment.out b/ops/op2/test_cases/sync/doc_comment.out index bd0d0b21fa..e9f063102a 100644 --- a/ops/op2/test_cases/sync/doc_comment.out +++ b/ops/op2/test_cases/sync/doc_comment.out @@ -13,7 +13,7 @@ impl op_has_doc_comment { use deno_core::v8::fast_api::Type; use deno_core::v8::fast_api::CType; deno_core::v8::fast_api::FastFunction::new( - &[], + &[Type::V8Value], CType::Void, Self::fast_function as *const ::std::ffi::c_void, ) @@ -28,7 +28,8 @@ impl op_has_doc_comment { let result = Self::call(); } fn fast_function(_: deno_core::v8::Local) -> () { - Self::call() + let result = Self::call(); + result } #[inline(always)] pub fn call() -> () {} diff --git a/ops/op2/test_cases/sync/result_primitive.out b/ops/op2/test_cases/sync/result_primitive.out index a8ac50174c..151e8e730f 100644 --- a/ops/op2/test_cases/sync/result_primitive.out +++ b/ops/op2/test_cases/sync/result_primitive.out @@ -9,7 +9,15 @@ impl op_u32_with_result { name: stringify!(op_u32_with_result), v8_fn_ptr: Self::slow_function as _, enabled: true, - fast_fn: None, + fast_fn: Some({ + use deno_core::v8::fast_api::Type; + use deno_core::v8::fast_api::CType; + deno_core::v8::fast_api::FastFunction::new( + &[Type::V8Value, Type::CallbackOptions], + CType::Uint32, + Self::fast_function as *const ::std::ffi::c_void, + ) + }), is_async: false, is_unstable: false, is_v8: false, @@ -20,6 +28,27 @@ impl op_u32_with_result { let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe { &*info }); + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { + &*info + }); + let opctx = unsafe { + &*(deno_core::v8::Local::::cast(args.data()).value() + as *const deno_core::_ops::OpCtx) + }; + if let Some(err) = unsafe { opctx.unsafely_take_last_error_for_ops_only() } { + let scope = &mut unsafe { deno_core::v8::CallbackScope::new(&*info) }; + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { + &*info + }); + let opstate = ::std::cell::RefCell::borrow(&*opctx.state); + let exception = deno_core::error::to_v8_error( + scope, + opstate.get_error_class_fn, + &err, + ); + scope.throw_exception(exception); + return; + } let result = Self::call(); match result { Ok(result) => { @@ -30,10 +59,6 @@ impl op_u32_with_result { let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { &*info }); - let opctx = unsafe { - &*(deno_core::v8::Local::::cast(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; let opstate = ::std::cell::RefCell::borrow(&*opctx.state); let exception = deno_core::error::to_v8_error( scope, @@ -45,6 +70,30 @@ impl op_u32_with_result { } }; } + fn fast_function( + _: deno_core::v8::Local, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, + ) -> u32 { + let fast_api_callback_options = unsafe { &mut *fast_api_callback_options }; + let opctx = unsafe { + &*(deno_core::v8::Local::< + v8::External, + >::cast(unsafe { fast_api_callback_options.data.data }) + .value() as *const deno_core::_ops::OpCtx) + }; + let result = Self::call(); + let result = match result { + Ok(result) => result, + Err(err) => { + unsafe { + opctx.unsafely_set_last_error_for_ops_only(err); + } + fast_api_callback_options.fallback = true; + return ::std::default::Default::default(); + } + }; + result + } #[inline(always)] pub fn call() -> Result {} } diff --git a/ops/op2/test_cases/sync/result_primitive.rs b/ops/op2/test_cases/sync/result_primitive.rs index 6f68fa2286..df89c2432f 100644 --- a/ops/op2/test_cases/sync/result_primitive.rs +++ b/ops/op2/test_cases/sync/result_primitive.rs @@ -1,4 +1,4 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -#[op2] +#[op2(fast)] pub fn op_u32_with_result() -> Result {} diff --git a/ops/op2/test_cases/sync/result_void.out b/ops/op2/test_cases/sync/result_void.out index 74c0c66a66..afc10582ba 100644 --- a/ops/op2/test_cases/sync/result_void.out +++ b/ops/op2/test_cases/sync/result_void.out @@ -9,7 +9,15 @@ impl op_void_with_result { name: stringify!(op_void_with_result), v8_fn_ptr: Self::slow_function as _, enabled: true, - fast_fn: None, + fast_fn: Some({ + use deno_core::v8::fast_api::Type; + use deno_core::v8::fast_api::CType; + deno_core::v8::fast_api::FastFunction::new( + &[Type::V8Value, Type::CallbackOptions], + CType::Void, + Self::fast_function as *const ::std::ffi::c_void, + ) + }), is_async: false, is_unstable: false, is_v8: false, @@ -17,6 +25,27 @@ impl op_void_with_result { } } pub extern "C" fn slow_function(info: *const deno_core::v8::FunctionCallbackInfo) { + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { + &*info + }); + let opctx = unsafe { + &*(deno_core::v8::Local::::cast(args.data()).value() + as *const deno_core::_ops::OpCtx) + }; + if let Some(err) = unsafe { opctx.unsafely_take_last_error_for_ops_only() } { + let scope = &mut unsafe { deno_core::v8::CallbackScope::new(&*info) }; + let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { + &*info + }); + let opstate = ::std::cell::RefCell::borrow(&*opctx.state); + let exception = deno_core::error::to_v8_error( + scope, + opstate.get_error_class_fn, + &err, + ); + scope.throw_exception(exception); + return; + } let result = Self::call(); match result { Ok(result) => {} @@ -25,10 +54,6 @@ impl op_void_with_result { let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe { &*info }); - let opctx = unsafe { - &*(deno_core::v8::Local::::cast(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; let opstate = ::std::cell::RefCell::borrow(&*opctx.state); let exception = deno_core::error::to_v8_error( scope, @@ -40,6 +65,30 @@ impl op_void_with_result { } }; } + fn fast_function( + _: deno_core::v8::Local, + fast_api_callback_options: *mut deno_core::v8::fast_api::FastApiCallbackOptions, + ) -> () { + let fast_api_callback_options = unsafe { &mut *fast_api_callback_options }; + let opctx = unsafe { + &*(deno_core::v8::Local::< + v8::External, + >::cast(unsafe { fast_api_callback_options.data.data }) + .value() as *const deno_core::_ops::OpCtx) + }; + let result = Self::call(); + let result = match result { + Ok(result) => result, + Err(err) => { + unsafe { + opctx.unsafely_set_last_error_for_ops_only(err); + } + fast_api_callback_options.fallback = true; + return ::std::default::Default::default(); + } + }; + result + } #[inline(always)] pub fn call() -> Result<(), AnyError> {} } diff --git a/ops/op2/test_cases/sync/result_void.rs b/ops/op2/test_cases/sync/result_void.rs index 41256e8c4e..ef3aa7b321 100644 --- a/ops/op2/test_cases/sync/result_void.rs +++ b/ops/op2/test_cases/sync/result_void.rs @@ -1,4 +1,4 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -#[op2] +#[op2(fast)] pub fn op_void_with_result() -> Result<(), AnyError> {} diff --git a/ops/op2/test_cases/sync/smi.out b/ops/op2/test_cases/sync/smi.out index e6c1bc1e3f..7210e05729 100644 --- a/ops/op2/test_cases/sync/smi.out +++ b/ops/op2/test_cases/sync/smi.out @@ -13,7 +13,7 @@ impl op_add { use deno_core::v8::fast_api::Type; use deno_core::v8::fast_api::CType; deno_core::v8::fast_api::FastFunction::new( - &[Type::Int32, Type::Uint32], + &[Type::V8Value, Type::Int32, Type::Uint32], CType::Uint32, Self::fast_function as *const ::std::ffi::c_void, ) @@ -43,9 +43,8 @@ impl op_add { arg0: i32, arg1: u32, ) -> u32 { - let arg0 = arg0 as _; - let arg1 = arg1 as _; - Self::call(arg0, arg1) + let result = Self::call(arg0 as _, arg1 as _); + result } #[inline(always)] fn call(id: ResourceId, extra: u16) -> u32 {}