Add a way to convert _stateful_ (Rust) closures into JsFunctions.

This commit is contained in:
Daniel Henry-Mantilla 2021-03-17 19:48:31 +01:00
parent 3f40b9270e
commit 4aba159958
5 changed files with 163 additions and 15 deletions

View file

@ -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
}
};

View file

@ -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<ArgType: NapiValue>(&self, index: usize) -> Result<ArgType> {
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<ArgType: NapiValue>(&self, index: usize) -> Result<Either<ArgType, JsUndefined>> {
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<crate::JsUnknown> {
/* (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<V>(&self) -> Result<V>
where

View file

@ -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<R, F>(&self, name: &str, callback: F) -> Result<JsFunction>
where
F: 'static + Send + Sync + Fn(crate::CallContext<'_>) -> Result<R>,
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<R: NapiValue, F: Fn(CallContext<'_>) -> Result<R>>(
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::<F>()
.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::<String>() {
s
} else if let Some(s) = e.downcast_ref::<&str>() {
s
} else {
"<no error message>"
},
))
})
.and_then(|v| v)
.unwrap_or_else(|e| {
JsError::from(e).throw_into(raw_env);
ptr::null_mut()
})
}
trampoline::<R, F>
}),
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<F>(
_raw_env: sys::napi_env,
closure_data_ptr: *mut c_void,
_finalize_hint: *mut c_void,
) {
drop(Box::<F>::from_raw(closure_data_ptr.cast()))
}
finalize_box_trampoline::<F>
}),
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.
///

View file

@ -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<T, Hint, F>(
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,

View file

@ -18,14 +18,9 @@ impl<T> Ref<T> {
#[inline]
pub(crate) fn new(js_value: Value, ref_count: u32, inner: T) -> Result<Ref<T>> {
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,