From 4aba1599585eb763cb64a38ed6ccf61f2336e205 Mon Sep 17 00:00:00 2001 From: Daniel Henry-Mantilla Date: Wed, 17 Mar 2021 19:48:31 +0100 Subject: [PATCH] Add a way to convert _stateful_ (Rust) closures into `JsFunction`s. --- napi-derive/src/lib.rs | 2 +- napi/src/call_context.rs | 33 +++++++-- napi/src/env.rs | 127 ++++++++++++++++++++++++++++++++ napi/src/js_values/object.rs | 7 +- napi/src/js_values/value_ref.rs | 9 +-- 5 files changed, 163 insertions(+), 15 deletions(-) diff --git a/napi-derive/src/lib.rs b/napi-derive/src/lib.rs index dbecfb63..d85f43ac 100644 --- a/napi-derive/src/lib.rs +++ b/napi-derive/src/lib.rs @@ -114,7 +114,7 @@ pub fn js_function(attr: TokenStream, input: TokenStream) -> TokenStream { } let mut env = unsafe { Env::from_raw(raw_env) }; - let ctx = CallContext::new(&mut env, cb_info, raw_this, &raw_args, #arg_len_span, argc); + let ctx = CallContext::new(&mut env, cb_info, raw_this, &raw_args, argc); #execute_js_function } }; diff --git a/napi/src/call_context.rs b/napi/src/call_context.rs index cff33ad2..c5d67503 100644 --- a/napi/src/call_context.rs +++ b/napi/src/call_context.rs @@ -9,19 +9,33 @@ pub struct CallContext<'env> { raw_this: sys::napi_value, callback_info: sys::napi_callback_info, args: &'env [sys::napi_value], - arg_len: usize, /// arguments.length pub length: usize, } impl<'env> CallContext<'env> { + /// The number of N-api obtained values. In practice this is the numeric + /// parameter provided to the `#[js_function(arg_len)]` macro. + /// + /// As a comparison, the (arguments) `.length` represents the actual number + /// of arguments given at a specific function call. + /// + /// If `.length < .arg_len`, then the elements in the `length .. arg_len` + /// range are just `JsUndefined`s. + /// + /// If `.length > .arg_len`, then truncation has happened and some args have + /// been lost. + #[inline] + fn arg_len(&self) -> usize { + self.args.len() + } + #[inline] pub fn new( env: &'env mut Env, callback_info: sys::napi_callback_info, raw_this: sys::napi_value, args: &'env [sys::napi_value], - arg_len: usize, length: usize, ) -> Self { Self { @@ -29,14 +43,13 @@ impl<'env> CallContext<'env> { callback_info, raw_this, args, - arg_len, length, } } #[inline] pub fn get(&self, index: usize) -> Result { - if index + 1 > self.arg_len { + if index >= self.arg_len() { Err(Error { status: Status::GenericFailure, reason: "Arguments index out of range".to_owned(), @@ -48,7 +61,7 @@ impl<'env> CallContext<'env> { #[inline] pub fn try_get(&self, index: usize) -> Result> { - if index + 1 > self.arg_len { + if index >= self.arg_len() { Err(Error { status: Status::GenericFailure, reason: "Arguments index out of range".to_owned(), @@ -60,6 +73,16 @@ impl<'env> CallContext<'env> { } } + #[inline] + pub fn get_all(&self) -> Vec { + /* (0 .. self.arg_len()).map(|i| self.get(i).unwrap()).collect() */ + self + .args + .iter() + .map(|&raw| unsafe { crate::JsUnknown::from_raw_unchecked(self.env.0, raw) }) + .collect() + } + #[inline] pub fn get_new_target(&self) -> Result where diff --git a/napi/src/env.rs b/napi/src/env.rs index 7ea54ff1..37a9c969 100644 --- a/napi/src/env.rs +++ b/napi/src/env.rs @@ -509,6 +509,133 @@ impl Env { Ok(unsafe { JsFunction::from_raw_unchecked(self.0, raw_result) }) } + #[cfg(feature = "napi5")] + pub fn create_function_from_closure(&self, name: &str, callback: F) -> Result + where + F: 'static + Send + Sync + Fn(crate::CallContext<'_>) -> Result, + R: NapiValue, + { + use crate::CallContext; + let boxed_callback = Box::new(callback); + let closure_data_ptr: *mut F = Box::into_raw(boxed_callback); + + let mut raw_result = ptr::null_mut(); + let len = name.len(); + let name = CString::new(name)?; + check_status!(unsafe { + sys::napi_create_function( + self.0, + name.as_ptr(), + len, + Some({ + unsafe extern "C" fn trampoline) -> Result>( + raw_env: sys::napi_env, + cb_info: sys::napi_callback_info, + ) -> sys::napi_value { + use ::std::panic::{self, AssertUnwindSafe}; + panic::catch_unwind(AssertUnwindSafe(|| { + let (raw_this, ref raw_args, closure_data_ptr) = { + let argc = { + let mut argc = 0; + let status = sys::napi_get_cb_info( + raw_env, + cb_info, + &mut argc, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ); + debug_assert!( + Status::from(status) == Status::Ok, + "napi_get_cb_info failed" + ); + argc + }; + let mut raw_args = vec![ptr::null_mut(); argc]; + let mut raw_this = ptr::null_mut(); + let mut closure_data_ptr = ptr::null_mut(); + + let status = sys::napi_get_cb_info( + raw_env, + cb_info, + &mut { argc }, + raw_args.as_mut_ptr(), + &mut raw_this, + &mut closure_data_ptr, + ); + debug_assert!( + Status::from(status) == Status::Ok, + "napi_get_cb_info failed" + ); + (raw_this, raw_args, closure_data_ptr) + }; + + let closure: &F = closure_data_ptr + .cast::() + .as_ref() + .expect("`napi_get_cb_info` should have yielded non-`NULL` assoc data"); + let ref mut env = Env::from_raw(raw_env); + let ctx = CallContext::new(env, cb_info, raw_this, raw_args, raw_args.len()); + closure(ctx).map(|ret: R| ret.raw()) + })) + .map_err(|e| { + Error::from_reason(format!( + "panic from Rust code: {}", + if let Some(s) = e.downcast_ref::() { + s + } else if let Some(s) = e.downcast_ref::<&str>() { + s + } else { + "" + }, + )) + }) + .and_then(|v| v) + .unwrap_or_else(|e| { + JsError::from(e).throw_into(raw_env); + ptr::null_mut() + }) + } + + trampoline:: + }), + closure_data_ptr.cast(), // We let it borrow the data here + &mut raw_result, + ) + })?; + + // Note: based on N-API docs, at this point, we have created an effective + // `&'static dyn Fn…` in Rust parlance, in that thanks to `Box::into_raw()` + // we are sure the context won't be freed, and thus the callback may use + // it to call the actual method thanks to the trampoline… + // But we thus have a data leak: there is nothing yet reponsible for + // running the `drop(Box::from_raw(…))` cleanup code. + // + // To solve that, according to the docs, we need to attach a finalizer: + check_status!(unsafe { + sys::napi_add_finalizer( + self.0, + raw_result, + closure_data_ptr.cast(), + Some({ + unsafe extern "C" fn finalize_box_trampoline( + _raw_env: sys::napi_env, + closure_data_ptr: *mut c_void, + _finalize_hint: *mut c_void, + ) { + drop(Box::::from_raw(closure_data_ptr.cast())) + } + + finalize_box_trampoline:: + }), + ptr::null_mut(), + ptr::null_mut(), + ) + })?; + + Ok(unsafe { JsFunction::from_raw_unchecked(self.0, raw_result) }) + } + #[inline] /// This API retrieves a napi_extended_error_info structure with information about the last error that occurred. /// diff --git a/napi/src/js_values/object.rs b/napi/src/js_values/object.rs index 81df628d..75bd0ef0 100644 --- a/napi/src/js_values/object.rs +++ b/napi/src/js_values/object.rs @@ -13,7 +13,9 @@ use crate::sys; #[cfg(feature = "napi5")] use crate::Env; #[cfg(feature = "napi6")] -use crate::{Error, Result}; +use crate::Error; +#[cfg(feature = "napi5")] +use crate::Result; pub struct JsObject(pub(crate) Value); @@ -52,7 +54,7 @@ impl JsObject { ), ), Box::leak(Box::new(finalize_hint)) as *mut _ as *mut c_void, - &mut maybe_ref, + &mut maybe_ref, // Note: this does not point to the boxed one… ) }) } @@ -73,6 +75,7 @@ unsafe extern "C" fn finalize_callback( let env = Env::from_raw(raw_env); callback(FinalizeContext { value, hint, env }); if !raw_ref.is_null() { + // … ⬆️ this branch is thus unreachable. let status = sys::napi_delete_reference(raw_env, raw_ref); debug_assert!( status == sys::Status::napi_ok, diff --git a/napi/src/js_values/value_ref.rs b/napi/src/js_values/value_ref.rs index 1e22c090..f5da1d42 100644 --- a/napi/src/js_values/value_ref.rs +++ b/napi/src/js_values/value_ref.rs @@ -18,14 +18,9 @@ impl Ref { #[inline] pub(crate) fn new(js_value: Value, ref_count: u32, inner: T) -> Result> { let mut raw_ref = ptr::null_mut(); - let initial_ref_count = 1; + assert_ne!(ref_count, 0, "Initial `ref_count` must be > 0"); check_status!(unsafe { - sys::napi_create_reference( - js_value.env, - js_value.value, - initial_ref_count, - &mut raw_ref, - ) + sys::napi_create_reference(js_value.env, js_value.value, ref_count, &mut raw_ref) })?; Ok(Ref { raw_ref,