diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index f82d7e3e..64677f82 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -81,6 +81,7 @@ pub struct NapiStruct { pub js_mod: Option, pub comments: Vec, pub implement_iterator: bool, + pub use_custom_finalize: bool, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/backend/src/codegen/struct.rs b/crates/backend/src/codegen/struct.rs index 38a82fe6..411825db 100644 --- a/crates/backend/src/codegen/struct.rs +++ b/crates/backend/src/codegen/struct.rs @@ -278,6 +278,11 @@ impl NapiStruct { let js_name_raw = &self.js_name; let js_name_str = format!("{}\0", js_name_raw); let iterator_implementation = self.gen_iterator_property(name); + let finalize_trait = if self.use_custom_finalize { + quote! {} + } else { + quote! { impl napi::bindgen_prelude::ObjectFinalize for #name {} } + }; quote! { impl napi::bindgen_prelude::ToNapiValue for #name { unsafe fn to_napi_value( @@ -297,6 +302,8 @@ impl NapiStruct { } } + #finalize_trait + impl #name { pub fn into_reference(val: #name, env: napi::Env) -> napi::Result> { if let Some(ctor_ref) = napi::bindgen_prelude::get_class_constructor(#js_name_str) { @@ -420,6 +427,12 @@ impl NapiStruct { } }; + let finalize_trait = if self.use_custom_finalize { + quote! {} + } else { + quote! { impl napi::bindgen_prelude::ObjectFinalize for #name {} } + }; + quote! { impl napi::bindgen_prelude::ToNapiValue for #name { unsafe fn to_napi_value( @@ -453,6 +466,8 @@ impl NapiStruct { } } } + + #finalize_trait } } diff --git a/crates/macro/src/parser/attrs.rs b/crates/macro/src/parser/attrs.rs index 3b3b0418..f7e2a7e0 100644 --- a/crates/macro/src/parser/attrs.rs +++ b/crates/macro/src/parser/attrs.rs @@ -55,6 +55,7 @@ macro_rules! attrgen { (strict, Strict(Span)), (return_if_invalid, ReturnIfInvalid(Span)), (object, Object(Span)), + (custom_finalize, CustomFinalize(Span)), (namespace, Namespace(Span, String, Span)), (iterator, Iterator(Span)), (ts_args_type, TsArgsType(Span, String, Span)), diff --git a/crates/macro/src/parser/mod.rs b/crates/macro/src/parser/mod.rs index 5450ff1b..ec91abfd 100644 --- a/crates/macro/src/parser/mod.rs +++ b/crates/macro/src/parser/mod.rs @@ -741,22 +741,39 @@ impl ParseNapi for syn::ItemStruct { "#[napi] can't be applied to a struct with #[napi(ts_args_type)], #[napi(ts_return_type)], #[napi(skip_typescript)] or #[napi(ts_type)]" ); } + if opts.return_if_invalid().is_some() { + bail_span!( + self, + "#[napi(return_if_invalid)] can only be applied to a function or method." + ); + } + if opts.object().is_some() && opts.custom_finalize().is_some() { + bail_span!(self, "Custom finalize is not supported for #[napi(object)]"); + } let napi = self.convert_to_ast(opts); self.to_tokens(tokens); napi } } + impl ParseNapi for syn::ItemImpl { fn parse_napi(&mut self, tokens: &mut TokenStream, opts: BindgenAttrs) -> BindgenResult { if opts.ts_args_type().is_some() || opts.ts_return_type().is_some() || opts.skip_typescript().is_some() || opts.ts_type().is_some() + || opts.custom_finalize().is_some() { bail_span!( self, - "#[napi] can't be applied to impl with #[napi(ts_args_type)], #[napi(ts_return_type)], #[napi(skip_typescript)] or #[napi(ts_type)]" + "#[napi] can't be applied to impl with #[napi(ts_args_type)], #[napi(ts_return_type)], #[napi(skip_typescript)] or #[napi(ts_type)] or #[napi(custom_finalize)]" + ); + } + if opts.return_if_invalid().is_some() { + bail_span!( + self, + "#[napi(return_if_invalid)] can only be applied to a function or method." ); } // #[napi] macro will be remove from impl items after converted to ast @@ -766,13 +783,23 @@ impl ParseNapi for syn::ItemImpl { napi } } + impl ParseNapi for syn::ItemEnum { fn parse_napi(&mut self, tokens: &mut TokenStream, opts: BindgenAttrs) -> BindgenResult { - if opts.ts_args_type().is_some() || opts.ts_return_type().is_some() || opts.ts_type().is_some() + if opts.ts_args_type().is_some() + || opts.ts_return_type().is_some() + || opts.ts_type().is_some() + || opts.custom_finalize().is_some() { bail_span!( self, - "#[napi] can't be applied to a enum with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)]" + "#[napi] can't be applied to a enum with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)] or #[napi(custom_finalize)]" + ); + } + if opts.return_if_invalid().is_some() { + bail_span!( + self, + "#[napi(return_if_invalid)] can only be applied to a function or method." ); } let napi = self.convert_to_ast(opts); @@ -783,11 +810,20 @@ impl ParseNapi for syn::ItemEnum { } impl ParseNapi for syn::ItemConst { fn parse_napi(&mut self, tokens: &mut TokenStream, opts: BindgenAttrs) -> BindgenResult { - if opts.ts_args_type().is_some() || opts.ts_return_type().is_some() || opts.ts_type().is_some() + if opts.ts_args_type().is_some() + || opts.ts_return_type().is_some() + || opts.ts_type().is_some() + || opts.custom_finalize().is_some() { bail_span!( self, - "#[napi] can't be applied to a const with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)]" + "#[napi] can't be applied to a const with #[napi(ts_args_type)], #[napi(ts_return_type)] or #[napi(ts_type)] or #[napi(custom_finalize)]" + ); + } + if opts.return_if_invalid().is_some() { + bail_span!( + self, + "#[napi(return_if_invalid)] can only be applied to a function or method." ); } let napi = self.convert_to_ast(opts); @@ -930,6 +966,7 @@ impl ConvertToAST for syn::ItemStruct { js_mod: namespace, comments: extract_doc_comments(&self.attrs), implement_iterator, + use_custom_finalize: opts.custom_finalize().is_some(), }), }) } diff --git a/crates/napi/src/bindgen_runtime/callback_info.rs b/crates/napi/src/bindgen_runtime/callback_info.rs index d869050f..08363745 100644 --- a/crates/napi/src/bindgen_runtime/callback_info.rs +++ b/crates/napi/src/bindgen_runtime/callback_info.rs @@ -67,7 +67,11 @@ impl CallbackInfo { self.this } - fn _construct(&self, js_name: &str, obj: T) -> Result<(sys::napi_value, *mut T)> { + fn _construct( + &self, + js_name: &str, + obj: T, + ) -> Result<(sys::napi_value, *mut T)> { let obj = Box::new(obj); let this = self.this(); let value_ref = Box::into_raw(obj); @@ -96,11 +100,15 @@ impl CallbackInfo { Ok((this, value_ref)) } - pub fn construct(&self, js_name: &str, obj: T) -> Result { + pub fn construct( + &self, + js_name: &str, + obj: T, + ) -> Result { self._construct(js_name, obj).map(|(v, _)| v) } - pub fn construct_generator( + pub fn construct_generator( &self, js_name: &str, obj: T, @@ -110,11 +118,15 @@ impl CallbackInfo { Ok(instance) } - pub fn factory(&self, js_name: &str, obj: T) -> Result { + pub fn factory( + &self, + js_name: &str, + obj: T, + ) -> Result { self._factory(js_name, obj).map(|(value, _)| value) } - pub fn generator_factory( + pub fn generator_factory( &self, js_name: &str, obj: T, @@ -124,7 +136,11 @@ impl CallbackInfo { Ok(instance) } - fn _factory(&self, js_name: &str, obj: T) -> Result<(sys::napi_value, *mut T)> { + fn _factory( + &self, + js_name: &str, + obj: T, + ) -> Result<(sys::napi_value, *mut T)> { let this = self.this(); let mut instance = ptr::null_mut(); let inner = ___CALL_FROM_FACTORY.get_or_default(); diff --git a/crates/napi/src/bindgen_runtime/mod.rs b/crates/napi/src/bindgen_runtime/mod.rs index 60630802..b007201e 100644 --- a/crates/napi/src/bindgen_runtime/mod.rs +++ b/crates/napi/src/bindgen_runtime/mod.rs @@ -11,7 +11,7 @@ pub use js_values::*; pub use module_register::*; use super::sys; -use crate::Status; +use crate::{JsError, Result, Status}; mod callback_info; mod env; @@ -20,16 +20,27 @@ pub mod iterator; mod js_values; mod module_register; +pub trait ObjectFinalize: Sized { + #[allow(unused)] + fn finalize(self, env: Env) -> Result<()> { + Ok(()) + } +} + /// # Safety /// /// called when node wrapper objects destroyed #[doc(hidden)] -pub unsafe extern "C" fn raw_finalize_unchecked( +pub unsafe extern "C" fn raw_finalize_unchecked( env: sys::napi_env, finalize_data: *mut c_void, _finalize_hint: *mut c_void, ) { - unsafe { Box::from_raw(finalize_data as *mut T) }; + let data = *unsafe { Box::from_raw(finalize_data as *mut T) }; + if let Err(err) = data.finalize(unsafe { Env::from_raw(env) }) { + let e: JsError = err.into(); + unsafe { e.throw_into(env) }; + } if let Some((_, ref_val, finalize_callbacks_ptr)) = REFERENCE_MAP.borrow_mut(|reference_map| reference_map.remove(&finalize_data)) { diff --git a/examples/napi/__test__/typegen.spec.ts.md b/examples/napi/__test__/typegen.spec.ts.md index bfa3b602..335a908a 100644 --- a/examples/napi/__test__/typegen.spec.ts.md +++ b/examples/napi/__test__/typegen.spec.ts.md @@ -294,6 +294,9 @@ Generated by [AVA](https://avajs.dev). constructor(name: string)␊ setName(name: string): void␊ }␊ + export class CustomFinalize {␊ + constructor(width: number, height: number)␊ + }␊ 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 8a9b5be2..77b9d6a1 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 459e4c68..e688c6f7 100644 --- a/examples/napi/__test__/values.spec.ts +++ b/examples/napi/__test__/values.spec.ts @@ -107,6 +107,7 @@ import { useTokioWithoutAsync, getNumArr, getNestedNumArr, + CustomFinalize, } from '../' test('export const', (t) => { @@ -241,6 +242,10 @@ test('class in object field', (t) => { t.is(receiveObjectWithClassField(obj), obj.bird) }) +test('custom finalize class', (t) => { + t.notThrows(() => new CustomFinalize(200, 200)) +}) + test('should be able to create object reference and shared reference', (t) => { const repo = new JsRepo('.') t.is(repo.remote().name(), 'origin') diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index f653f1bb..ca259111 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -284,6 +284,9 @@ export class NotWritableClass { constructor(name: string) setName(name: string): void } +export class CustomFinalize { + constructor(width: number, height: number) +} 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 a1ae310c..01ed6b6d 100644 --- a/examples/napi/src/class.rs +++ b/examples/napi/src/class.rs @@ -1,5 +1,5 @@ use napi::{ - bindgen_prelude::{Buffer, ClassInstance, This, Uint8Array}, + bindgen_prelude::{Buffer, ClassInstance, ObjectFinalize, This, Uint8Array}, Env, Result, }; @@ -361,3 +361,32 @@ impl NotWritableClass { self.name = name; } } + +#[napi(custom_finalize)] +pub struct CustomFinalize { + width: u32, + height: u32, + inner: Vec, +} + +#[napi] +impl CustomFinalize { + #[napi(constructor)] + pub fn new(mut env: Env, width: u32, height: u32) -> Result { + let inner = vec![0; (width * height * 4) as usize]; + let inner_size = inner.len(); + env.adjust_external_memory(inner_size as i64)?; + Ok(Self { + width, + height, + inner, + }) + } +} + +impl ObjectFinalize for CustomFinalize { + fn finalize(self, mut env: Env) -> Result<()> { + env.adjust_external_memory(-(self.inner.len() as i64))?; + Ok(()) + } +}