Merge pull request #508 from getditto/ditto/closure-into-jsfunction
Add a way to convert stateful (Rust) closures into `JsFunction`s.
This commit is contained in:
commit
2ae748249d
5 changed files with 163 additions and 15 deletions
|
@ -114,7 +114,7 @@ pub fn js_function(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut env = unsafe { Env::from_raw(raw_env) };
|
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
|
#execute_js_function
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,19 +9,33 @@ pub struct CallContext<'env> {
|
||||||
raw_this: sys::napi_value,
|
raw_this: sys::napi_value,
|
||||||
callback_info: sys::napi_callback_info,
|
callback_info: sys::napi_callback_info,
|
||||||
args: &'env [sys::napi_value],
|
args: &'env [sys::napi_value],
|
||||||
arg_len: usize,
|
|
||||||
/// arguments.length
|
/// arguments.length
|
||||||
pub length: usize,
|
pub length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'env> CallContext<'env> {
|
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]
|
#[inline]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
env: &'env mut Env,
|
env: &'env mut Env,
|
||||||
callback_info: sys::napi_callback_info,
|
callback_info: sys::napi_callback_info,
|
||||||
raw_this: sys::napi_value,
|
raw_this: sys::napi_value,
|
||||||
args: &'env [sys::napi_value],
|
args: &'env [sys::napi_value],
|
||||||
arg_len: usize,
|
|
||||||
length: usize,
|
length: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -29,14 +43,13 @@ impl<'env> CallContext<'env> {
|
||||||
callback_info,
|
callback_info,
|
||||||
raw_this,
|
raw_this,
|
||||||
args,
|
args,
|
||||||
arg_len,
|
|
||||||
length,
|
length,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get<ArgType: NapiValue>(&self, index: usize) -> Result<ArgType> {
|
pub fn get<ArgType: NapiValue>(&self, index: usize) -> Result<ArgType> {
|
||||||
if index + 1 > self.arg_len {
|
if index >= self.arg_len() {
|
||||||
Err(Error {
|
Err(Error {
|
||||||
status: Status::GenericFailure,
|
status: Status::GenericFailure,
|
||||||
reason: "Arguments index out of range".to_owned(),
|
reason: "Arguments index out of range".to_owned(),
|
||||||
|
@ -48,7 +61,7 @@ impl<'env> CallContext<'env> {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn try_get<ArgType: NapiValue>(&self, index: usize) -> Result<Either<ArgType, JsUndefined>> {
|
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 {
|
Err(Error {
|
||||||
status: Status::GenericFailure,
|
status: Status::GenericFailure,
|
||||||
reason: "Arguments index out of range".to_owned(),
|
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]
|
#[inline]
|
||||||
pub fn get_new_target<V>(&self) -> Result<V>
|
pub fn get_new_target<V>(&self) -> Result<V>
|
||||||
where
|
where
|
||||||
|
|
127
napi/src/env.rs
127
napi/src/env.rs
|
@ -509,6 +509,133 @@ impl Env {
|
||||||
Ok(unsafe { JsFunction::from_raw_unchecked(self.0, raw_result) })
|
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]
|
#[inline]
|
||||||
/// This API retrieves a napi_extended_error_info structure with information about the last error that occurred.
|
/// This API retrieves a napi_extended_error_info structure with information about the last error that occurred.
|
||||||
///
|
///
|
||||||
|
|
|
@ -13,7 +13,9 @@ use crate::sys;
|
||||||
#[cfg(feature = "napi5")]
|
#[cfg(feature = "napi5")]
|
||||||
use crate::Env;
|
use crate::Env;
|
||||||
#[cfg(feature = "napi6")]
|
#[cfg(feature = "napi6")]
|
||||||
use crate::{Error, Result};
|
use crate::Error;
|
||||||
|
#[cfg(feature = "napi5")]
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
pub struct JsObject(pub(crate) Value);
|
pub struct JsObject(pub(crate) Value);
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ impl JsObject {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Box::leak(Box::new(finalize_hint)) as *mut _ as *mut c_void,
|
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);
|
let env = Env::from_raw(raw_env);
|
||||||
callback(FinalizeContext { value, hint, env });
|
callback(FinalizeContext { value, hint, env });
|
||||||
if !raw_ref.is_null() {
|
if !raw_ref.is_null() {
|
||||||
|
// … ⬆️ this branch is thus unreachable.
|
||||||
let status = sys::napi_delete_reference(raw_env, raw_ref);
|
let status = sys::napi_delete_reference(raw_env, raw_ref);
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
status == sys::Status::napi_ok,
|
status == sys::Status::napi_ok,
|
||||||
|
|
|
@ -18,14 +18,9 @@ impl<T> Ref<T> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(js_value: Value, ref_count: u32, inner: T) -> Result<Ref<T>> {
|
pub(crate) fn new(js_value: Value, ref_count: u32, inner: T) -> Result<Ref<T>> {
|
||||||
let mut raw_ref = ptr::null_mut();
|
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 {
|
check_status!(unsafe {
|
||||||
sys::napi_create_reference(
|
sys::napi_create_reference(js_value.env, js_value.value, ref_count, &mut raw_ref)
|
||||||
js_value.env,
|
|
||||||
js_value.value,
|
|
||||||
initial_ref_count,
|
|
||||||
&mut raw_ref,
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
Ok(Ref {
|
Ok(Ref {
|
||||||
raw_ref,
|
raw_ref,
|
||||||
|
|
Loading…
Reference in a new issue