diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index bc0f35b2..cb7421dd 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -34,6 +34,7 @@ pub enum NapiFnArgKind { pub enum FnKind { Normal, Constructor, + Factory, Getter, Setter, } diff --git a/crates/backend/src/codegen/fn.rs b/crates/backend/src/codegen/fn.rs index 8eb50426..930dab6d 100644 --- a/crates/backend/src/codegen/fn.rs +++ b/crates/backend/src/codegen/fn.rs @@ -39,17 +39,33 @@ impl TryToTokens for NapiFn { } }; - let function_call = - if args_len == 0 && self.fn_self.is_none() && self.kind != FnKind::Constructor { - quote! { #native_call } - } else { - quote! { - CallbackInfo::<#args_len>::new(env, cb, None).and_then(|mut cb| { - #(#arg_conversions)* - #native_call - }) + let function_call = if args_len == 0 + && self.fn_self.is_none() + && self.kind != FnKind::Constructor + && self.kind != FnKind::Factory + { + quote! { #native_call } + } else if self.kind == FnKind::Constructor { + quote! { + let call_from_factory = ___CALL_FROM_FACTORY.load(std::sync::atomic::Ordering::Relaxed); + // constructor function is called from class `factory` + // so we should skip the original `constructor` logic + if call_from_factory { + return std::ptr::null_mut(); } - }; + CallbackInfo::<#args_len>::new(env, cb, None).and_then(|mut cb| { + #(#arg_conversions)* + #native_call + }) + } + } else { + quote! { + CallbackInfo::<#args_len>::new(env, cb, None).and_then(|mut cb| { + #(#arg_conversions)* + #native_call + }) + } + }; (quote! { #(#attrs)* @@ -229,6 +245,8 @@ impl NapiFn { if let Some(ty) = &self.ret { if self.kind == FnKind::Constructor { quote! { cb.construct(#js_name, #ret) } + } else if self.kind == FnKind::Factory { + quote! { cb.factory(#js_name, #ret) } } else if self.is_ret_result { if self.is_async { quote! { diff --git a/crates/backend/src/typegen/fn.rs b/crates/backend/src/typegen/fn.rs index 32f15f39..9d7c719a 100644 --- a/crates/backend/src/typegen/fn.rs +++ b/crates/backend/src/typegen/fn.rs @@ -74,6 +74,7 @@ impl NapiFn { Some(_) => "", None => "static", }, + crate::FnKind::Factory => "static", crate::FnKind::Constructor => "", crate::FnKind::Getter => "get", crate::FnKind::Setter => "set", diff --git a/crates/macro/src/parser/attrs.rs b/crates/macro/src/parser/attrs.rs index d3a98d1b..a8660405 100644 --- a/crates/macro/src/parser/attrs.rs +++ b/crates/macro/src/parser/attrs.rs @@ -44,6 +44,7 @@ macro_rules! attrgen { $mac! { (js_name, JsName(Span, String, Span)), (constructor, Constructor(Span)), + (factory, Factory(Span)), (getter, Getter(Span, Option)), (setter, Setter(Span, Option)), (readonly, Readonly(Span)), diff --git a/crates/macro/src/parser/mod.rs b/crates/macro/src/parser/mod.rs index cf002a7f..6708c032 100644 --- a/crates/macro/src/parser/mod.rs +++ b/crates/macro/src/parser/mod.rs @@ -641,6 +641,10 @@ fn fn_kind(opts: &BindgenAttrs) -> FnKind { kind = FnKind::Constructor; } + if opts.factory().is_some() { + kind = FnKind::Factory; + } + kind } diff --git a/crates/napi/src/bindgen_runtime/callback_info.rs b/crates/napi/src/bindgen_runtime/callback_info.rs index a7726bea..0e68c7c0 100644 --- a/crates/napi/src/bindgen_runtime/callback_info.rs +++ b/crates/napi/src/bindgen_runtime/callback_info.rs @@ -1,10 +1,19 @@ +use std::ffi::c_void; +use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; + use crate::{bindgen_prelude::*, check_status, sys, Result}; -use std::{ffi::c_void, ptr}; + +#[doc(hidden)] +/// Determined is `constructor` called from Class `factory` +/// Ugly but works +/// We can even be more ugly without `atomic` +pub static ___CALL_FROM_FACTORY: AtomicBool = AtomicBool::new(false); pub struct CallbackInfo { env: sys::napi_env, this: sys::napi_value, - args: [sys::napi_value; N], + pub args: [sys::napi_value; N], } impl CallbackInfo { @@ -76,6 +85,39 @@ impl CallbackInfo { Ok(this) } + pub fn factory(&self, js_name: &str, obj: T) -> Result { + let obj = Box::new(obj); + let this = self.this(); + let mut instance = ptr::null_mut(); + unsafe { + ___CALL_FROM_FACTORY.store(true, Ordering::Relaxed); + let status = sys::napi_new_instance(self.env, this, 0, ptr::null_mut(), &mut instance); + ___CALL_FROM_FACTORY.store(false, Ordering::Relaxed); + // Error thrown in `constructor` + if status == sys::Status::napi_pending_exception { + let mut exception = ptr::null_mut(); + sys::napi_get_and_clear_last_exception(self.env, &mut exception); + sys::napi_throw(self.env, exception); + return Ok(ptr::null_mut()); + } + + check_status!( + sys::napi_wrap( + self.env, + instance, + Box::into_raw(obj) as *mut std::ffi::c_void, + Some(raw_finalize_unchecked::), + ptr::null_mut(), + &mut std::ptr::null_mut() + ), + "Failed to initialize class `{}`", + js_name, + )?; + }; + + Ok(instance) + } + pub fn unwrap_borrow_mut(&mut self) -> Result<&'static mut T> where T: FromNapiMutRef + TypeName, diff --git a/crates/napi/src/bindgen_runtime/module_register.rs b/crates/napi/src/bindgen_runtime/module_register.rs index 3632a0d6..77597a7a 100644 --- a/crates/napi/src/bindgen_runtime/module_register.rs +++ b/crates/napi/src/bindgen_runtime/module_register.rs @@ -79,10 +79,10 @@ unsafe extern "C" fn napi_register_module_v1( let (ctor, props): (Vec<_>, Vec<_>) = props.into_iter().partition(|prop| prop.is_ctor); // one or more or zero? // zero is for `#[napi(task)]` - if ctor.is_empty() { + if ctor.is_empty() && props.is_empty() { continue; } - let ctor = ctor[0].raw().method.unwrap(); + let ctor = ctor.get(0).map(|c| c.raw().method.unwrap()).unwrap_or(noop); let raw_props: Vec<_> = props.iter().map(|prop| prop.raw()).collect(); let js_class_name = CString::new(js_name).unwrap(); @@ -143,3 +143,10 @@ unsafe extern "C" fn napi_register_module_v1( exports } + +pub(crate) unsafe extern "C" fn noop( + _env: sys::napi_env, + _info: sys::napi_callback_info, +) -> sys::napi_value { + ptr::null_mut() +} diff --git a/crates/napi/src/error.rs b/crates/napi/src/error.rs index 3d6201ce..ec45ba2d 100644 --- a/crates/napi/src/error.rs +++ b/crates/napi/src/error.rs @@ -161,9 +161,22 @@ macro_rules! impl_object_methods { /// /// This function is safety if env is not null ptr. pub unsafe fn throw_into(self, env: sys::napi_env) { + #[cfg(debug_assertions)] + let reason = self.0.reason.clone(); + #[cfg(debug_assertions)] + let status = self.0.status; let js_error = self.into_value(env); + #[cfg(debug_assertions)] let throw_status = sys::napi_throw(env, js_error); - debug_assert!(throw_status == sys::Status::napi_ok); + sys::napi_throw(env, js_error); + #[cfg(debug_assertions)] + assert!( + throw_status == sys::Status::napi_ok, + "Throw error failed, status: [{}], raw message: \"{}\", raw status: [{}]", + Status::from(throw_status), + reason, + Status::from(status) + ); } pub fn throw(&self, env: sys::napi_env) -> Result<()> { diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index f2b5d698..a1601ee9 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -48,9 +48,14 @@ Generated by [AVA](https://avajs.dev). export class Animal {␊ readonly kind: Kind␊ constructor(kind: Kind, name: string)␊ + static withKind(kind: Kind): Animal␊ get name(): string␊ set name(name: string)␊ whoami(): string␊ static getDogKind(): Kind␊ }␊ + export class ClassWithFactory {␊ + name: string␊ + static withName(name: string): ClassWithFactory␊ + }␊ ` diff --git a/examples/napi/__test__/typegen.spec.ts.snap b/examples/napi/__test__/typegen.spec.ts.snap index 6203a4e4..f7edba92 100644 Binary files a/examples/napi/__test__/typegen.spec.ts.snap and b/examples/napi/__test__/typegen.spec.ts.snap differ diff --git a/examples/napi/__test__/values.spec.ts b/examples/napi/__test__/values.spec.ts index 359b1b51..6ccb5afd 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -15,6 +15,7 @@ import { getCwd, Animal, Kind, + ClassWithFactory, CustomNumEnum, enumToI32, listObjKeys, @@ -81,6 +82,20 @@ test('class', (t) => { t.is(dog.name, '可乐') }) +test('class factory', (t) => { + const duck = ClassWithFactory.withName('Default') + t.is(duck.name, 'Default') + + duck.name = '周黑鸭' + t.is(duck.name, '周黑鸭') + + const doge = Animal.withKind(Kind.Dog) + t.is(doge.name, 'Default') + + doge.name = '旺财' + t.is(doge.name, '旺财') +}) + test('callback', (t) => { getCwd((cwd) => { t.is(cwd, process.cwd()) diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index eb922314..2f236609 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -38,8 +38,13 @@ export function getBuffer(): Buffer export class Animal { readonly kind: Kind constructor(kind: Kind, name: string) + static withKind(kind: Kind): Animal get name(): string set name(name: string) whoami(): string static getDogKind(): Kind } +export class ClassWithFactory { + name: string + static withName(name: string): ClassWithFactory +} diff --git a/examples/napi/src/class.rs b/examples/napi/src/class.rs index 444de5ba..9b05a683 100644 --- a/examples/napi/src/class.rs +++ b/examples/napi/src/class.rs @@ -19,6 +19,14 @@ impl Animal { Animal { kind, name } } + #[napi(factory)] + pub fn with_kind(kind: Kind) -> Self { + Animal { + kind, + name: "Default".to_owned(), + } + } + #[napi(getter)] pub fn get_name(&self) -> &str { self.name.as_str() diff --git a/examples/napi/src/class_factory.rs b/examples/napi/src/class_factory.rs new file mode 100644 index 00000000..d5c7076f --- /dev/null +++ b/examples/napi/src/class_factory.rs @@ -0,0 +1,14 @@ +use napi::bindgen_prelude::*; + +#[napi] +pub struct ClassWithFactory { + pub name: String, +} + +#[napi] +impl ClassWithFactory { + #[napi(factory)] + pub fn with_name(name: String) -> Self { + Self { name } + } +} diff --git a/examples/napi/src/lib.rs b/examples/napi/src/lib.rs index b99f81f2..5da94157 100644 --- a/examples/napi/src/lib.rs +++ b/examples/napi/src/lib.rs @@ -7,6 +7,7 @@ mod array; mod r#async; mod callback; mod class; +mod class_factory; mod either; mod r#enum; mod error;