feat!(napi): create function from #[napi] fn (#2757)

This commit is contained in:
LongYinan 2025-07-02 01:54:31 -07:00 committed by GitHub
parent 6222b39f5a
commit 7e34e30b66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 108 additions and 82 deletions

View File

@ -32,6 +32,7 @@ pub struct NapiFn {
pub catch_unwind: bool,
pub unsafe_: bool,
pub register_name: Ident,
pub no_export: bool,
}
#[derive(Debug, Clone)]

View File

@ -27,7 +27,7 @@ pub trait TryToTokens {
}
fn get_intermediate_ident(name: &str) -> Ident {
let new_name = format!("_napi_internal_register_{name}");
let new_name = format!("{name}_c_callback");
Ident::new(&new_name, Span::call_site())
}

View File

@ -726,10 +726,14 @@ impl NapiFn {
let module_register_name = &self.register_name;
let intermediate_ident = get_intermediate_ident(&name_str);
let js_mod_ident = js_mod_to_token_stream(self.js_mod.as_ref());
let cb_name = Ident::new(&format!("{name_str}_js_function"), Span::call_site());
let cb_name = Ident::new(
&format!("_napi_rs_internal_register_{name_str}"),
Span::call_site(),
);
if self.module_exports {
return quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(clippy::all)]
unsafe fn #cb_name(env: napi::bindgen_prelude::sys::napi_env, exports: napi::bindgen_prelude::sys::napi_value) -> napi::bindgen_prelude::Result<napi::bindgen_prelude::sys::napi_value> {
@ -737,6 +741,7 @@ impl NapiFn {
Ok(exports)
}
#[doc(hidden)]
#[allow(clippy::all)]
#[allow(non_snake_case)]
#[cfg(all(not(test), not(target_family = "wasm")))]
@ -755,7 +760,32 @@ impl NapiFn {
};
}
let register_module_export_tokens = if self.no_export {
quote! {}
} else {
quote! {
#[doc(hidden)]
#[allow(clippy::all)]
#[allow(non_snake_case)]
#[cfg(all(not(test), not(target_family = "wasm")))]
#[napi::ctor::ctor(crate_path=::napi::ctor)]
fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);
}
#[doc(hidden)]
#[allow(clippy::all)]
#[allow(non_snake_case)]
#[cfg(all(not(test), target_family = "wasm"))]
#[no_mangle]
extern "C" fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);
}
}
};
quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#[allow(clippy::all)]
unsafe fn #cb_name(env: napi::bindgen_prelude::sys::napi_env) -> napi::bindgen_prelude::Result<napi::bindgen_prelude::sys::napi_value> {
@ -773,25 +803,10 @@ impl NapiFn {
"Failed to register function `{}`",
#name_str,
)?;
napi::bindgen_prelude::register_js_function(#js_name, #cb_name, Some(#intermediate_ident));
Ok(fn_ptr)
}
#[allow(clippy::all)]
#[allow(non_snake_case)]
#[cfg(all(not(test), not(target_family = "wasm")))]
#[napi::ctor::ctor(crate_path=::napi::ctor)]
fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);
}
#[allow(clippy::all)]
#[allow(non_snake_case)]
#[cfg(all(not(test), target_family = "wasm"))]
#[no_mangle]
extern "C" fn #module_register_name() {
napi::bindgen_prelude::register_module_export(#js_mod_ident, #js_name, #cb_name);
}
#register_module_export_tokens
}
}
}

View File

@ -487,7 +487,7 @@ pub fn ty_to_ts_type(
Some((fill_ty(known_ty, union_args), false))
}
} else {
let filtered_args =
let mut filtered_args =
if let Some(arg_indices) = KNOWN_TYPES_IGNORE_ARG.get(rust_ty.as_str()) {
args
.enumerate()
@ -497,6 +497,9 @@ pub fn ty_to_ts_type(
} else {
args.collect::<Vec<_>>()
};
if rust_ty.starts_with("Function") && filtered_args.is_empty() {
filtered_args = vec!["arg?: unknown".to_owned(), "unknown".to_owned()];
}
Some((fill_ty(known_ty, filtered_args), false))
}

View File

@ -67,7 +67,7 @@ impl FromIterator<FnArg> for FnArgList {
impl ToTypeDef for NapiFn {
fn to_type_def(&self) -> Option<TypeDef> {
if self.skip_typescript || self.module_exports {
if self.skip_typescript || self.module_exports || self.no_export {
return None;
}

View File

@ -80,6 +80,7 @@ macro_rules! attrgen {
(discriminant, Discriminant(Span, String, Span)),
(transparent, Transparent(Span)),
(array, Array(Span)),
(no_export, NoExport(Span)),
// impl later
// (inspectable, Inspectable(Span)),

View File

@ -728,6 +728,13 @@ fn napi_fn_from_decl(
bail_span!(sig.ident, "module_exports fn can't have generic parameters");
}
if opts.no_export().is_some() {
bail_span!(
sig.ident,
"#[napi(no_export)] can not be used with module_exports attribute"
);
}
for arg in args.iter() {
match &arg.kind {
NapiFnArgKind::Callback(_) => {
@ -897,6 +904,7 @@ fn napi_fn_from_decl(
catch_unwind: opts.catch_unwind().is_some(),
unsafe_: sig.unsafety.is_some(),
register_name: get_register_ident(ident.to_string().as_str()),
no_export: opts.no_export().is_some(),
})
})
}
@ -964,6 +972,12 @@ impl ParseNapi for syn::ItemStruct {
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
if opts.object().is_some() && opts.custom_finalize().is_some() {
bail_span!(self, "Custom finalize is not supported for #[napi(object)]");
}
@ -999,6 +1013,12 @@ impl ParseNapi for syn::ItemImpl {
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
// #[napi] macro will be remove from impl items after converted to ast
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
@ -1031,6 +1051,12 @@ impl ParseNapi for syn::ItemEnum {
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
@ -1061,6 +1087,12 @@ impl ParseNapi for syn::ItemConst {
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi
@ -1090,6 +1122,12 @@ impl ParseNapi for syn::ItemType {
"#[napi(catch_unwind)] can only be applied to a function or method."
);
}
if opts.no_export().is_some() {
bail_span!(
self,
"#[napi(no_export)] can only be applied to a function."
);
}
let napi = self.convert_to_ast(opts);
self.to_tokens(tokens);
napi

View File

@ -94,12 +94,6 @@ impl ModuleClassProperty {
}
}
type FnRegisterMap = PersistedPerInstanceHashMap<
ExportRegisterCallback,
(sys::napi_callback, &'static str),
FxBuildHasher,
>;
#[cfg(not(feature = "noop"))]
static MODULE_REGISTER_CALLBACK: LazyLock<ModuleRegisterCallback> = LazyLock::new(Default::default);
#[cfg(not(feature = "noop"))]
@ -113,7 +107,6 @@ static MODULE_COUNT: AtomicUsize = AtomicUsize::new(0);
static FIRST_MODULE_REGISTERED: AtomicBool = AtomicBool::new(false);
thread_local! {
static REGISTERED_CLASSES: LazyCell<RegisteredClasses> = LazyCell::new(Default::default);
static FN_REGISTER_MAP: LazyCell<FnRegisterMap> = LazyCell::new(Default::default);
}
#[cfg(all(feature = "napi4", not(feature = "noop")))]
pub(crate) static CUSTOM_GC_TSFN: std::sync::atomic::AtomicPtr<sys::napi_threadsafe_function__> =
@ -194,19 +187,6 @@ pub fn register_module_export_hook(cb: ExportRegisterHookCallback) {
#[doc(hidden)]
pub fn register_module_export_hook(_cb: ExportRegisterHookCallback) {}
#[doc(hidden)]
pub fn register_js_function(
name: &'static str,
cb: ExportRegisterCallback,
c_fn: sys::napi_callback,
) {
FN_REGISTER_MAP.with(|cell| {
cell.borrow_mut(|inner| {
inner.insert(cb, (c_fn, name));
})
});
}
#[doc(hidden)]
pub fn get_class_constructor(js_name: &'static str) -> Option<sys::napi_ref> {
REGISTERED_CLASSES.with(|cell| cell.borrow_mut(|map| map.get(js_name).copied()))
@ -239,41 +219,6 @@ pub fn register_class(
) {
}
/// Get `C Callback` from defined Rust `fn`
/// ```rust
/// #[napi]
/// fn some_fn() -> u32 {
/// 1
/// }
///
/// #[napi]
/// fn create_obj(env: Env) -> Result<JsObject> {
/// let mut obj = env.create_object()?;
/// obj.define_property(&[Property::new("getter")?.with_getter(get_c_callback(some_fn_js_function)?)])?;
/// Ok(obj)
/// }
/// ```
///
/// ```js
/// console.log(createObj().getter) // 1
/// ```
///
pub fn get_c_callback(raw_fn: ExportRegisterCallback) -> Result<crate::Callback> {
FN_REGISTER_MAP.with(|cell| {
cell.borrow_mut(|inner| {
inner
.get(&raw_fn)
.and_then(|(cb, _name)| *cb)
.ok_or_else(|| {
crate::Error::new(
crate::Status::InvalidArg,
"JavaScript function does not exist".to_owned(),
)
})
})
})
}
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
#[no_mangle]
unsafe extern "C" fn napi_register_wasm_v1(

View File

@ -497,6 +497,8 @@ Generated by [AVA](https://avajs.dev).
export declare function createExternalTypedArray(): Uint32Array␊
export declare function createFunction(): (arg: number) => number␊
export declare function createObj(): object␊
export declare function createObjectRef(): object␊
@ -573,7 +575,7 @@ Generated by [AVA](https://avajs.dev).
export declare function either4(input: string | number | boolean | Obj): number␊
export declare function eitherBoolOrFunction(input: boolean | (any)): void␊
export declare function eitherBoolOrFunction(input: boolean | ((arg?: unknown) => unknown)): void␊
export declare function eitherBoolOrTuple(input: boolean | [boolean, string]): void␊
@ -934,7 +936,7 @@ Generated by [AVA](https://avajs.dev).
export declare function testSerdeRoundtrip(data: any): any␊
export declare function threadsafeFunctionBuildThrowErrorWithStatus(cb: any): void␊
export declare function threadsafeFunctionBuildThrowErrorWithStatus(cb: (arg?: unknown) => unknown): void␊
export declare function threadsafeFunctionClosureCapture(defaultValue: Animal, func: (arg: Animal) => void): void␊

View File

@ -196,6 +196,7 @@ import {
type AliasedStruct,
returnObjectOnlyToJs,
buildThreadsafeFunctionFromFunction,
buildThreadsafeFunctionFromFunctionCalleeHandle,
createOptionalExternal,
getOptionalExternal,
mutateOptionalExternal,
@ -255,6 +256,7 @@ import {
uint8ArrayFromExternal,
Thing,
ThingList,
createFunction,
} from '../index.cjs'
// import other stuff in `#[napi(module_exports)]`
import nativeAddon from '../index.cjs'
@ -424,6 +426,8 @@ test('function call', async (t) => {
referenceAsCallback((a, b) => a + b, 42, 10),
52,
)
const fn = createFunction()
t.is(fn(42), 242)
})
test('class', (t) => {
@ -1667,6 +1671,10 @@ Napi4Test('build ThreadsafeFunction from Function', (t) => {
buildThreadsafeFunctionFromFunction(fn)
t.notThrows(() => {
buildThreadsafeFunctionFromFunctionCalleeHandle(() => {})
})
return subject.pipe(take(3))
})

View File

@ -185,6 +185,7 @@ export const createExternalBufferSlice = __napiModule.exports.createExternalBuff
export const createExternalRef = __napiModule.exports.createExternalRef
export const createExternalString = __napiModule.exports.createExternalString
export const createExternalTypedArray = __napiModule.exports.createExternalTypedArray
export const createFunction = __napiModule.exports.createFunction
export const createObj = __napiModule.exports.createObj
export const createObjectRef = __napiModule.exports.createObjectRef
export const createObjectWithClassField = __napiModule.exports.createObjectWithClassField

View File

@ -209,6 +209,7 @@ module.exports.createExternalBufferSlice = __napiModule.exports.createExternalBu
module.exports.createExternalRef = __napiModule.exports.createExternalRef
module.exports.createExternalString = __napiModule.exports.createExternalString
module.exports.createExternalTypedArray = __napiModule.exports.createExternalTypedArray
module.exports.createFunction = __napiModule.exports.createFunction
module.exports.createObj = __napiModule.exports.createObj
module.exports.createObjectRef = __napiModule.exports.createObjectRef
module.exports.createObjectWithClassField = __napiModule.exports.createObjectWithClassField

View File

@ -500,6 +500,7 @@ module.exports.createExternalBufferSlice = nativeBinding.createExternalBufferSli
module.exports.createExternalRef = nativeBinding.createExternalRef
module.exports.createExternalString = nativeBinding.createExternalString
module.exports.createExternalTypedArray = nativeBinding.createExternalTypedArray
module.exports.createFunction = nativeBinding.createFunction
module.exports.createObj = nativeBinding.createObj
module.exports.createObjectRef = nativeBinding.createObjectRef
module.exports.createObjectWithClassField = nativeBinding.createObjectWithClassField

View File

@ -459,6 +459,8 @@ export declare function createExternalString(content: string): ExternalObject<st
export declare function createExternalTypedArray(): Uint32Array
export declare function createFunction(): (arg: number) => number
export declare function createObj(): object
export declare function createObjectRef(): object
@ -535,7 +537,7 @@ export declare function either3(input: string | number | boolean): number
export declare function either4(input: string | number | boolean | Obj): number
export declare function eitherBoolOrFunction(input: boolean | (any)): void
export declare function eitherBoolOrFunction(input: boolean | ((arg?: unknown) => unknown)): void
export declare function eitherBoolOrTuple(input: boolean | [boolean, string]): void
@ -896,7 +898,7 @@ export declare function testSerdeBufferBytes(obj: object): bigint
export declare function testSerdeRoundtrip(data: any): any
export declare function threadsafeFunctionBuildThrowErrorWithStatus(cb: any): void
export declare function threadsafeFunctionBuildThrowErrorWithStatus(cb: (arg?: unknown) => unknown): void
export declare function threadsafeFunctionClosureCapture(defaultValue: Animal, func: (arg: Animal) => void): void

View File

@ -1,5 +1,3 @@
#![allow(deprecated)]
use napi::{
bindgen_prelude::{ClassInstance, FnArgs, Function, FunctionRef, PromiseRaw},
threadsafe_function::ThreadsafeFunctionCallMode,
@ -140,3 +138,13 @@ pub fn build_threadsafe_function_from_function_callee_handle(
Ok(())
}
#[napi]
pub fn create_function(env: &Env) -> Result<Function<u32, u32>> {
env.create_function("customFunction", no_export_function_c_callback)
}
#[napi(no_export)]
pub fn no_export_function(input: u32) -> u32 {
input + 200
}

View File

@ -96,7 +96,7 @@ pub fn create_obj_with_property(env: &Env) -> Result<Object<'_>> {
.with_value(&arraybuffer),
Property::new()
.with_utf8_name("getter")?
.with_getter(get_c_callback(getter_from_obj_js_function)?),
.with_getter(getter_from_obj_c_callback),
])?;
Ok(obj)
}