feat(napi): support async class factory (#1779)

- Close https://github.com/napi-rs/napi-rs/issues/1777
This commit is contained in:
LongYinan 2023-11-06 10:58:23 +08:00 committed by GitHub
parent 0dc1ef738b
commit 546b108a5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 14 deletions

View file

@ -112,8 +112,15 @@ impl TryToTokens for NapiFn {
} }
}; };
// async factory only
let use_after_async = if self.is_async && self.parent.is_some() && self.fn_self.is_none() {
quote! { true }
} else {
quote! { false }
};
let function_call_inner = quote! { let function_call_inner = quote! {
napi::bindgen_prelude::CallbackInfo::<#args_len>::new(env, cb, None).and_then(|mut cb| { napi::bindgen_prelude::CallbackInfo::<#args_len>::new(env, cb, None, #use_after_async).and_then(|mut cb| {
#build_ref_container #build_ref_container
#(#arg_conversions)* #(#arg_conversions)*
#native_call #native_call
@ -511,6 +518,8 @@ impl NapiFn {
if self.is_ret_result { if self.is_ret_result {
if self.parent_is_generator { if self.parent_is_generator {
quote! { cb.generator_factory(#js_name, #ret?) } quote! { cb.generator_factory(#js_name, #ret?) }
} else if self.is_async {
quote! { cb.factory(#js_name, #ret) }
} else { } else {
quote! { cb.factory(#js_name, #ret?) } quote! { cb.factory(#js_name, #ret?) }
} }

View file

@ -250,7 +250,7 @@ impl NapiStruct {
env: napi::bindgen_prelude::sys::napi_env, env: napi::bindgen_prelude::sys::napi_env,
cb: napi::bindgen_prelude::sys::napi_callback_info cb: napi::bindgen_prelude::sys::napi_callback_info
) -> napi::bindgen_prelude::sys::napi_value { ) -> napi::bindgen_prelude::sys::napi_value {
napi::bindgen_prelude::CallbackInfo::<#fields_len>::new(env, cb, None) napi::bindgen_prelude::CallbackInfo::<#fields_len>::new(env, cb, None, false)
.and_then(|cb| #constructor) .and_then(|cb| #constructor)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
unsafe { napi::bindgen_prelude::JsError::from(e).throw_into(env) }; unsafe { napi::bindgen_prelude::JsError::from(e).throw_into(env) };
@ -669,7 +669,7 @@ impl NapiStruct {
env: napi::bindgen_prelude::sys::napi_env, env: napi::bindgen_prelude::sys::napi_env,
cb: napi::bindgen_prelude::sys::napi_callback_info cb: napi::bindgen_prelude::sys::napi_callback_info
) -> napi::bindgen_prelude::sys::napi_value { ) -> napi::bindgen_prelude::sys::napi_value {
napi::bindgen_prelude::CallbackInfo::<0>::new(env, cb, Some(0)) napi::bindgen_prelude::CallbackInfo::<0>::new(env, cb, Some(0), false)
.and_then(|mut cb| unsafe { cb.unwrap_borrow_mut::<#struct_name>() }) .and_then(|mut cb| unsafe { cb.unwrap_borrow_mut::<#struct_name>() })
.and_then(|obj| { .and_then(|obj| {
#to_napi_value_convert #to_napi_value_convert
@ -691,7 +691,7 @@ impl NapiStruct {
env: napi::bindgen_prelude::sys::napi_env, env: napi::bindgen_prelude::sys::napi_env,
cb: napi::bindgen_prelude::sys::napi_callback_info cb: napi::bindgen_prelude::sys::napi_callback_info
) -> napi::bindgen_prelude::sys::napi_value { ) -> napi::bindgen_prelude::sys::napi_value {
napi::bindgen_prelude::CallbackInfo::<1>::new(env, cb, Some(1)) napi::bindgen_prelude::CallbackInfo::<1>::new(env, cb, Some(1), false)
.and_then(|mut cb_info| unsafe { .and_then(|mut cb_info| unsafe {
cb_info.unwrap_borrow_mut::<#struct_name>() cb_info.unwrap_borrow_mut::<#struct_name>()
.and_then(|obj| { .and_then(|obj| {

View file

@ -226,7 +226,14 @@ impl NapiFn {
FnKind::Factory => self FnKind::Factory => self
.parent .parent
.clone() .clone()
.map(|i| format!(": {}", i.to_string().to_case(Case::Pascal))) .map(|i| {
let parent = i.to_string().to_case(Case::Pascal);
if self.is_async {
format!(": Promise<{}>", parent)
} else {
format!(": {}", parent)
}
})
.unwrap_or_else(|| "".to_owned()), .unwrap_or_else(|| "".to_owned()),
_ => { _ => {
let ret = if let Some(ret) = &self.ret { let ret = if let Some(ret) = &self.ret {

View file

@ -16,6 +16,7 @@ pub struct CallbackInfo<const N: usize> {
env: sys::napi_env, env: sys::napi_env,
pub this: sys::napi_value, pub this: sys::napi_value,
pub args: [sys::napi_value; N], pub args: [sys::napi_value; N],
this_reference: sys::napi_ref,
} }
impl<const N: usize> CallbackInfo<N> { impl<const N: usize> CallbackInfo<N> {
@ -24,6 +25,9 @@ impl<const N: usize> CallbackInfo<N> {
env: sys::napi_env, env: sys::napi_env,
callback_info: sys::napi_callback_info, callback_info: sys::napi_callback_info,
required_argc: Option<usize>, required_argc: Option<usize>,
// for async class factory, the `this` will be used after the async call
// so we must create reference for it and use it after async resolved
use_after_async: bool,
) -> Result<Self> { ) -> Result<Self> {
let mut this = ptr::null_mut(); let mut this = ptr::null_mut();
let mut args = [ptr::null_mut(); N]; let mut args = [ptr::null_mut(); N];
@ -55,7 +59,21 @@ impl<const N: usize> CallbackInfo<N> {
} }
} }
Ok(Self { env, this, args }) let mut this_reference = ptr::null_mut();
if use_after_async {
check_status!(
unsafe { sys::napi_create_reference(env, this, 1, &mut this_reference) },
"Failed to create reference for `this` in async class factory"
)?;
}
Ok(Self {
env,
this,
args,
this_reference,
})
} }
pub fn get_arg(&self, index: usize) -> sys::napi_value { pub fn get_arg(&self, index: usize) -> sys::napi_value {
@ -141,8 +159,18 @@ impl<const N: usize> CallbackInfo<N> {
js_name: &str, js_name: &str,
obj: T, obj: T,
) -> Result<(sys::napi_value, *mut T)> { ) -> Result<(sys::napi_value, *mut T)> {
let this = self.this(); let mut this = self.this();
let mut instance = ptr::null_mut(); let mut instance = ptr::null_mut();
if !self.this_reference.is_null() {
check_status!(
unsafe { sys::napi_get_reference_value(self.env, self.this_reference, &mut this) },
"Failed to get reference value for `this` in async class factory"
)?;
check_status!(
unsafe { sys::napi_delete_reference(self.env, self.this_reference) },
"Failed to delete reference for `this` in async class factory"
)?;
}
___CALL_FROM_FACTORY.with(|s| s.store(true, Ordering::Relaxed)); ___CALL_FROM_FACTORY.with(|s| s.store(true, Ordering::Relaxed));
let status = let status =
unsafe { sys::napi_new_instance(self.env, this, 0, ptr::null_mut(), &mut instance) }; unsafe { sys::napi_new_instance(self.env, this, 0, ptr::null_mut(), &mut instance) };
@ -154,6 +182,7 @@ impl<const N: usize> CallbackInfo<N> {
unsafe { sys::napi_throw(self.env, exception) }; unsafe { sys::napi_throw(self.env, exception) };
return Ok((ptr::null_mut(), ptr::null_mut())); return Ok((ptr::null_mut(), ptr::null_mut()));
} }
check_status!(status, "Failed to create instance of class `{}`", js_name)?;
let obj = Box::new(obj); let obj = Box::new(obj);
let initial_finalize: Box<dyn FnOnce()> = Box::new(|| {}); let initial_finalize: Box<dyn FnOnce()> = Box::new(|| {});
let finalize_callbacks_ptr = Rc::into_raw(Rc::new(Cell::new(Box::into_raw(initial_finalize)))); let finalize_callbacks_ptr = Rc::into_raw(Rc::new(Cell::new(Box::into_raw(initial_finalize))));
@ -164,7 +193,7 @@ impl<const N: usize> CallbackInfo<N> {
sys::napi_wrap( sys::napi_wrap(
self.env, self.env,
instance, instance,
value_ref as *mut c_void, value_ref.cast(),
Some(raw_finalize_unchecked::<T>), Some(raw_finalize_unchecked::<T>),
ptr::null_mut(), ptr::null_mut(),
&mut object_ref, &mut object_ref,

View file

@ -1084,7 +1084,7 @@ impl Env {
T: 'static + Send, T: 'static + Send,
V: 'static + ToNapiValue, V: 'static + ToNapiValue,
F: 'static + Send + Future<Output = Result<T>>, F: 'static + Send + Future<Output = Result<T>>,
R: 'static + Send + Sync + FnOnce(&mut Env, T) -> Result<V>, R: 'static + Send + FnOnce(&mut Env, T) -> Result<V>,
>( >(
&self, &self,
fut: F, fut: F,

View file

@ -1,4 +1,4 @@
use std::{future::Future, sync::RwLock}; use std::{future::Future, marker::PhantomData, sync::RwLock};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -83,11 +83,43 @@ pub fn within_runtime_if_available<F: FnOnce() -> T, T>(f: F) -> T {
f() f()
} }
struct SendableResolver<
Data: 'static + Send,
R: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
> {
inner: R,
_data: PhantomData<Data>,
}
// the `SendableResolver` will be only called in the `threadsafe_function_call_js` callback
// which means it will be always called in the Node.js JavaScript thread
// so the inner function is not required to be `Send`
// but the `Send` bound is required by the `execute_tokio_future` function
unsafe impl<Data: 'static + Send, R: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>>
Send for SendableResolver<Data, R>
{
}
impl<Data: 'static + Send, R: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>>
SendableResolver<Data, R>
{
fn new(inner: R) -> Self {
Self {
inner,
_data: PhantomData,
}
}
fn resolve(self, env: sys::napi_env, data: Data) -> Result<sys::napi_value> {
(self.inner)(env, data)
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)] #[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn execute_tokio_future< pub fn execute_tokio_future<
Data: 'static + Send, Data: 'static + Send,
Fut: 'static + Send + Future<Output = Result<Data>>, Fut: 'static + Send + Future<Output = Result<Data>>,
Resolver: 'static + Send + Sync + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>, Resolver: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
>( >(
env: sys::napi_env, env: sys::napi_env,
fut: Fut, fut: Fut,
@ -95,10 +127,14 @@ pub fn execute_tokio_future<
) -> Result<sys::napi_value> { ) -> Result<sys::napi_value> {
let (deferred, promise) = JsDeferred::new(env)?; let (deferred, promise) = JsDeferred::new(env)?;
let inner = async move { let sendable_resolver = SendableResolver::new(resolver);
let inner = async {
match fut.await { match fut.await {
Ok(v) => deferred.resolve(|env| { Ok(v) => deferred.resolve(move |env| {
resolver(env.raw(), v).map(|v| unsafe { JsUnknown::from_raw_unchecked(env.raw(), v) }) sendable_resolver
.resolve(env.raw(), v)
.map(|v| unsafe { JsUnknown::from_raw_unchecked(env.raw(), v) })
}), }),
Err(e) => deferred.reject(e), Err(e) => deferred.reject(e),
} }

View file

@ -102,6 +102,8 @@ Generated by [AVA](https://avajs.dev).
export class ClassWithFactory {␊ export class ClassWithFactory {␊
name: string␊ name: string␊
static withName(name: string): ClassWithFactory␊ static withName(name: string): ClassWithFactory␊
static with4Name(name: string): Promise<ClassWithFactory>
static with4NameResult(name: string): Promise<ClassWithFactory>
setName(name: string): this␊ setName(name: string): this␊
}␊ }␊

View file

@ -265,6 +265,13 @@ test('class factory', (t) => {
) )
}) })
test('async class factory', async (t) => {
const instance = await ClassWithFactory.with4Name('foo')
t.is(instance.name, 'foo-4')
const instance2 = await ClassWithFactory.with4NameResult('foo')
t.is(instance2.name, 'foo-4')
})
test('class constructor return Result', (t) => { test('class constructor return Result', (t) => {
const c = new Context() const c = new Context()
t.is(c.method(), 'not empty') t.is(c.method(), 'not empty')

View file

@ -92,6 +92,8 @@ export type Blake2bKey = Blake2BKey
export class ClassWithFactory { export class ClassWithFactory {
name: string name: string
static withName(name: string): ClassWithFactory static withName(name: string): ClassWithFactory
static with4Name(name: string): Promise<ClassWithFactory>
static with4NameResult(name: string): Promise<ClassWithFactory>
setName(name: string): this setName(name: string): this
} }

View file

@ -1,3 +1,9 @@
use napi::Result;
async fn always_4() -> i32 {
4
}
#[napi] #[napi]
pub struct ClassWithFactory { pub struct ClassWithFactory {
pub name: String, pub name: String,
@ -10,6 +16,20 @@ impl ClassWithFactory {
Self { name } Self { name }
} }
#[napi(factory)]
pub async fn with_4_name(name: String) -> Self {
Self {
name: format!("{name}-{}", always_4().await),
}
}
#[napi(factory)]
pub async fn with_4_name_result(name: String) -> Result<Self> {
Ok(Self {
name: format!("{name}-{}", always_4().await),
})
}
#[napi] #[napi]
pub fn set_name(&mut self, name: String) -> &Self { pub fn set_name(&mut self, name: String) -> &Self {
self.name = name; self.name = name;