diff --git a/crates/neon-runtime/src/napi/bindings/functions.rs b/crates/neon-runtime/src/napi/bindings/functions.rs index b8d29b16e..6fcff6988 100644 --- a/crates/neon-runtime/src/napi/bindings/functions.rs +++ b/crates/neon-runtime/src/napi/bindings/functions.rs @@ -244,6 +244,7 @@ mod napi4 { #[cfg(feature = "napi-5")] mod napi5 { use super::super::types::*; + use std::ffi::c_void; generate!( extern "C" { @@ -252,6 +253,15 @@ mod napi5 { fn get_date_value(env: Env, value: Value, result: *mut f64) -> Status; fn is_date(env: Env, value: Value, result: *mut bool) -> Status; + + fn add_finalizer( + env: Env, + js_object: Value, + native_object: *mut c_void, + finalize_cb: Finalize, + finalize_hint: *mut c_void, + result: Ref, + ) -> Status; } ); } diff --git a/crates/neon-runtime/src/napi/call.rs b/crates/neon-runtime/src/napi/call.rs index a8c917d53..39d09041e 100644 --- a/crates/neon-runtime/src/napi/call.rs +++ b/crates/neon-runtime/src/napi/call.rs @@ -7,21 +7,6 @@ use smallvec::{smallvec, SmallVec}; use crate::napi::bindings as napi; use crate::raw::{Env, FunctionCallbackInfo, Local}; -#[repr(C)] -pub struct CCallback { - pub static_callback: *mut c_void, - pub dynamic_callback: *mut c_void, -} - -impl Default for CCallback { - fn default() -> Self { - CCallback { - static_callback: null_mut(), - dynamic_callback: null_mut(), - } - } -} - pub unsafe fn is_construct(env: Env, info: FunctionCallbackInfo) -> bool { let mut target: MaybeUninit = MaybeUninit::zeroed(); diff --git a/crates/neon-runtime/src/napi/fun.rs b/crates/neon-runtime/src/napi/fun.rs index f05ab51b5..8cc8bdc46 100644 --- a/crates/neon-runtime/src/napi/fun.rs +++ b/crates/neon-runtime/src/napi/fun.rs @@ -1,29 +1,81 @@ //! Facilities for working with JS functions. +use std::mem::MaybeUninit; use std::os::raw::c_void; -use std::ptr::null; +use std::ptr; -use crate::call::CCallback; use crate::napi::bindings as napi; use crate::raw::{Env, Local}; -/// Mutates the `out` argument provided to refer to a newly created `v8::Function`. Returns -/// `false` if the value couldn't be created. -pub unsafe fn new(out: &mut Local, env: Env, callback: CCallback) -> bool { +pub unsafe fn new(env: Env, name: &str, callback: F) -> Result +where + F: Fn(Env, napi::CallbackInfo) -> Local + Send + 'static, +{ + let mut out = MaybeUninit::uninit(); + let data = Box::into_raw(Box::new(callback)); let status = napi::create_function( env, - null(), - 0, - Some(std::mem::transmute(callback.static_callback)), - callback.dynamic_callback, - out as *mut Local, + name.as_ptr().cast(), + name.len(), + Some(call_boxed::), + data.cast(), + out.as_mut_ptr(), ); - status == napi::Status::Ok + if status == napi::Status::PendingException { + Box::from_raw(data); + + return Err(status); + } + + assert_eq!(status, napi::Status::Ok); + + let out = out.assume_init(); + + #[cfg(feature = "napi-5")] + { + unsafe extern "C" fn drop_function( + _env: Env, + _finalize_data: *mut c_void, + finalize_hint: *mut c_void, + ) { + Box::from_raw(finalize_hint.cast::()); + } + + let status = napi::add_finalizer( + env, + out, + ptr::null_mut(), + Some(drop_function::), + data.cast(), + ptr::null_mut(), + ); + + assert_eq!(status, napi::Status::Ok); + } + + Ok(out) } -pub unsafe fn get_dynamic_callback(_env: Env, data: *mut c_void) -> *mut c_void { - data +unsafe extern "C" fn call_boxed(env: Env, info: napi::CallbackInfo) -> Local +where + F: Fn(Env, napi::CallbackInfo) -> Local + Send + 'static, +{ + let mut data = MaybeUninit::uninit(); + let status = napi::get_cb_info( + env, + info, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + data.as_mut_ptr(), + ); + + assert_eq!(status, napi::Status::Ok); + + let callback = &*data.assume_init().cast::(); + + callback(env, info) } pub unsafe fn call( diff --git a/src/context/internal.rs b/src/context/internal.rs index c300c5c04..8e5a27cce 100644 --- a/src/context/internal.rs +++ b/src/context/internal.rs @@ -28,6 +28,13 @@ pub struct Env(raw::Isolate); #[derive(Clone, Copy)] pub struct Env(raw::Env); +#[cfg(feature = "napi-1")] +impl From for Env { + fn from(env: raw::Env) -> Self { + Self(env) + } +} + thread_local! { #[allow(unused)] pub(crate) static IS_RUNNING: RefCell = RefCell::new(false); diff --git a/src/context/mod.rs b/src/context/mod.rs index e0fdb9313..f2635a2cc 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -178,6 +178,7 @@ use std; use std::cell::RefCell; use std::convert::Into; use std::marker::PhantomData; +#[cfg(feature = "legacy-runtime")] use std::os::raw::c_void; use std::panic::UnwindSafe; @@ -190,6 +191,15 @@ pub(crate) struct CallbackInfo<'a> { } impl CallbackInfo<'_> { + #[cfg(feature = "napi-1")] + pub unsafe fn new(info: raw::FunctionCallbackInfo) -> Self { + Self { + info, + _lifetime: PhantomData, + } + } + + #[cfg(feature = "legacy-runtime")] pub fn data(&self, env: Env) -> *mut c_void { unsafe { let mut raw_data: *mut c_void = std::mem::zeroed(); @@ -198,6 +208,7 @@ impl CallbackInfo<'_> { } } + #[cfg(feature = "legacy-runtime")] pub unsafe fn with_cx FnOnce(CallContext<'a, T>) -> U>( &self, env: Env, @@ -606,6 +617,7 @@ impl<'a> ModuleContext<'a> { Scope::with(env, |scope| f(ModuleContext { scope, exports })) } + #[cfg(not(feature = "napi-5"))] /// Convenience method for exporting a Neon function from a module. pub fn export_function( &mut self, @@ -617,6 +629,18 @@ impl<'a> ModuleContext<'a> { Ok(()) } + #[cfg(feature = "napi-5")] + /// Convenience method for exporting a Neon function from a module. + pub fn export_function(&mut self, key: &str, f: F) -> NeonResult<()> + where + F: Fn(FunctionContext) -> JsResult + Send + 'static, + V: Value, + { + let value = JsFunction::new(self, f)?.upcast::(); + self.exports.set(self, key, value)?; + Ok(()) + } + #[cfg(feature = "legacy-runtime")] /// Convenience method for exporting a Neon class constructor from a module. pub fn export_class(&mut self, key: &str) -> NeonResult<()> { diff --git a/src/types/internal.rs b/src/types/internal.rs index 7a349002b..6b9b3d4f8 100644 --- a/src/types/internal.rs +++ b/src/types/internal.rs @@ -1,13 +1,17 @@ use super::Value; use crate::context::internal::Env; -use crate::context::{CallbackInfo, FunctionContext}; +#[cfg(feature = "legacy-runtime")] +use crate::context::CallbackInfo; +#[cfg(feature = "legacy-runtime")] +use crate::context::FunctionContext; +#[cfg(feature = "legacy-runtime")] use crate::result::JsResult; -use crate::types::error::convert_panics; -use crate::types::{Handle, JsObject, Managed}; +use crate::types::{Handle, Managed}; use neon_runtime; +#[cfg(feature = "legacy-runtime")] use neon_runtime::call::CCallback; use neon_runtime::raw; -use std::mem; +#[cfg(feature = "legacy-runtime")] use std::os::raw::c_void; pub trait ValueInternal: Managed + 'static { @@ -28,47 +32,24 @@ pub trait ValueInternal: Managed + 'static { } } +#[cfg(feature = "legacy-runtime")] #[repr(C)] pub struct FunctionCallback(pub fn(FunctionContext) -> JsResult); #[cfg(feature = "legacy-runtime")] impl Callback<()> for FunctionCallback { extern "C" fn invoke(env: Env, info: CallbackInfo<'_>) { - unsafe { - info.with_cx::(env, |cx| { - let data = info.data(env); - let dynamic_callback: fn(FunctionContext) -> JsResult = - mem::transmute(neon_runtime::fun::get_dynamic_callback(env.to_raw(), data)); - if let Ok(value) = convert_panics(env, || dynamic_callback(cx)) { - info.set_return(value); - } - }) - } - } + use crate::types::error::convert_panics; + use crate::types::JsObject; - fn into_ptr(self) -> *mut c_void { - self.0 as *mut _ - } -} - -#[cfg(feature = "napi-1")] -impl Callback for FunctionCallback { - extern "C" fn invoke(env: Env, info: CallbackInfo<'_>) -> raw::Local { unsafe { info.with_cx::(env, |cx| { let data = info.data(env); - let dynamic_callback: fn(FunctionContext) -> JsResult = - mem::transmute(neon_runtime::fun::get_dynamic_callback(env.to_raw(), data)); + let dynamic_callback: fn(FunctionContext) -> JsResult = std::mem::transmute( + neon_runtime::fun::get_dynamic_callback(env.to_raw(), data), + ); if let Ok(value) = convert_panics(env, || dynamic_callback(cx)) { - value.to_raw() - } else { - // We do not have a Js Value to return, most likely due to an exception. - // If we are in a throwing state, constructing a Js Value would be invalid. - // While not explicitly written, the N-API documentation includes many examples - // of returning `NULL` when a native function does not return a value. - // Note, `raw::Local` in this context is a type alias for `*mut napi_value` and not a struct - // https://nodejs.org/api/n-api.html#n_api_napi_create_function - std::ptr::null_mut() + info.set_return(value); } }) } @@ -83,6 +64,7 @@ impl Callback for FunctionCallback { /// This type makes it possible to export a dynamically computed Rust function /// as a pair of 1) a raw pointer to the dynamically computed function, and 2) /// a static function that knows how to transmute that raw pointer and call it. +#[cfg(feature = "legacy-runtime")] pub(crate) trait Callback: Sized { /// Extracts the computed Rust function and invokes it. The Neon runtime /// ensures that the computed function is provided as the extra data field, @@ -91,8 +73,6 @@ pub(crate) trait Callback: Sized { /// See `invoke`. This is used by the non-n-api implementation, so that every impl for this /// trait doesn't need to provide two versions of `invoke`. - #[cfg(feature = "legacy-runtime")] - #[doc(hidden)] extern "C" fn invoke_compat(info: CallbackInfo<'_>) -> T { Self::invoke(Env::current(), info) } @@ -103,12 +83,8 @@ pub(crate) trait Callback: Sized { /// Exports the callback as a pair consisting of the static `Self::invoke` /// method and the computed callback, both converted to raw void pointers. fn into_c_callback(self) -> CCallback { - #[cfg(feature = "napi-1")] - let invoke = Self::invoke; - #[cfg(feature = "legacy-runtime")] - let invoke = Self::invoke_compat; CCallback { - static_callback: unsafe { mem::transmute(invoke as usize) }, + static_callback: unsafe { std::mem::transmute(Self::invoke_compat as usize) }, dynamic_callback: self.into_ptr(), } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 82ade11c9..6fb985e1e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -84,7 +84,7 @@ pub(crate) mod error; pub(crate) mod internal; pub(crate) mod utf8; -use self::internal::{FunctionCallback, ValueInternal}; +use self::internal::ValueInternal; use self::utf8::Utf8; use crate::context::internal::Env; use crate::context::{Context, FunctionContext}; @@ -92,7 +92,6 @@ use crate::handle::internal::SuperType; use crate::handle::{Handle, Managed}; use crate::object::{Object, This}; use crate::result::{JsResult, JsResultExt, NeonResult, Throw}; -use crate::types::internal::Callback; use neon_runtime; use neon_runtime::raw; use smallvec::SmallVec; @@ -700,6 +699,7 @@ where } impl JsFunction { + #[cfg(feature = "legacy-runtime")] pub fn new<'a, C, U>( cx: &mut C, f: fn(FunctionContext) -> JsResult, @@ -708,6 +708,9 @@ impl JsFunction { C: Context<'a>, U: Value, { + use self::internal::FunctionCallback; + use crate::types::internal::Callback; + build(cx.env(), |out| { let env = cx.env().to_raw(); unsafe { @@ -716,6 +719,64 @@ impl JsFunction { } }) } + + #[cfg(all(feature = "napi-1", not(feature = "napi-5")))] + pub fn new<'a, C, U>( + cx: &mut C, + f: fn(FunctionContext) -> JsResult, + ) -> JsResult<'a, JsFunction> + where + C: Context<'a>, + U: Value, + { + Self::new_internal(cx, f) + } + + #[cfg(feature = "napi-5")] + pub fn new<'a, C, F, V>(cx: &mut C, f: F) -> JsResult<'a, JsFunction> + where + C: Context<'a>, + F: Fn(FunctionContext) -> JsResult + Send + 'static, + V: Value, + { + Self::new_internal(cx, f) + } + + #[cfg(feature = "napi-1")] + fn new_internal<'a, C, F, V>(cx: &mut C, f: F) -> JsResult<'a, JsFunction> + where + C: Context<'a>, + F: Fn(FunctionContext) -> JsResult + Send + 'static, + V: Value, + { + use std::any; + use std::panic::AssertUnwindSafe; + use std::ptr; + + use crate::context::CallbackInfo; + use crate::types::error::convert_panics; + + let name = any::type_name::(); + let f = move |env: raw::Env, info| { + let env = env.into(); + let info = unsafe { CallbackInfo::new(info) }; + + FunctionContext::with(env, &info, |cx| { + convert_panics(env, AssertUnwindSafe(|| f(cx))) + .map(|v| v.to_raw()) + .unwrap_or_else(|_| ptr::null_mut()) + }) + }; + + if let Ok(raw) = unsafe { neon_runtime::fun::new(cx.env().to_raw(), name, f) } { + Ok(Handle::new_internal(JsFunction { + raw, + marker: PhantomData, + })) + } else { + Err(Throw) + } + } } impl JsFunction { diff --git a/test/napi/lib/functions.js b/test/napi/lib/functions.js index 1db8ec417..718db4ce5 100644 --- a/test/napi/lib/functions.js +++ b/test/napi/lib/functions.js @@ -114,4 +114,19 @@ describe('JsFunction', function() { assert.equal(addon.is_construct.call({}).wasConstructed, false); assert.equal((new addon.is_construct()).wasConstructed, true); }); + + it('should be able to call a function from a closure', function() { + assert.strictEqual(addon.count_called() + 1, addon.count_called()); + }); + + (global.gc ? it : it.skip)('should drop function when going out of scope', function(cb) { + (() => { + const msg = "Hello, World!"; + const f = addon.caller_with_drop_callback(() => msg, cb); + + assert.strictEqual(f(), msg); + })(); + + global.gc(); + }); }); diff --git a/test/napi/src/js/functions.rs b/test/napi/src/js/functions.rs index 5a8bd819c..0c4f735f7 100644 --- a/test/napi/src/js/functions.rs +++ b/test/napi/src/js/functions.rs @@ -147,3 +147,42 @@ pub fn is_construct(mut cx: FunctionContext) -> JsResult { this.set(&mut cx, "wasConstructed", construct)?; Ok(this) } + +pub fn caller_with_drop_callback(mut cx: FunctionContext) -> JsResult { + struct Callback { + f: Root, + drop: Option>, + channel: Channel, + } + + impl Drop for Callback { + fn drop(&mut self) { + let callback = self.drop.take(); + + self.channel.send(move |mut cx| { + let this = cx.undefined(); + let args: [Handle; 0] = []; + + callback + .unwrap() + .into_inner(&mut cx) + .call(&mut cx, this, args)?; + + Ok(()) + }); + } + } + + let callback = Callback { + f: cx.argument::(0)?.root(&mut cx), + drop: Some(cx.argument::(1)?.root(&mut cx)), + channel: cx.channel(), + }; + + JsFunction::new(&mut cx, move |mut cx| { + let this = cx.undefined(); + let args: [Handle; 0] = []; + + callback.f.to_inner(&mut cx).call(&mut cx, this, args) + }) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index f081c07a1..133580692 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -234,6 +234,17 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("call_and_catch", call_and_catch)?; cx.export_function("get_number_or_default", get_number_or_default)?; cx.export_function("is_construct", is_construct)?; + cx.export_function("caller_with_drop_callback", caller_with_drop_callback)?; + + cx.export_function("count_called", { + let n = std::cell::RefCell::new(0); + + move |mut cx| { + *n.borrow_mut() += 1; + + Ok(cx.number(*n.borrow())) + } + })?; fn call_get_own_property_names(mut cx: FunctionContext) -> JsResult { let object = cx.argument::(0)?;