feat(napi): add node_api_create_object_with_properties support for enum creation (#2990)

This commit is contained in:
Copilot 2025-11-27 23:21:23 +08:00 committed by GitHub
parent 94a6d37436
commit e4f5360dcd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 517 additions and 193 deletions

View File

@ -32,7 +32,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
# Linux-specific setup

View File

@ -29,7 +29,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
@ -301,7 +301,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Setup OpenHarmony SDK
@ -354,7 +354,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
@ -408,12 +408,11 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
uses: dtolnay/rust-toolchain@stable
if: matrix.settings.host != 'windows-11-arm'
with:
targets: ${{ matrix.settings.target }}
@ -506,7 +505,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- uses: actions/download-artifact@v6
with:
@ -555,7 +554,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-and-test-linux-armv7:
name: stable - armv7-unknown-linux-gnueabihf - node@20
name: stable - armv7-unknown-linux-gnueabihf - node@22
runs-on: ubuntu-latest
steps:
@ -612,7 +611,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
@ -744,7 +743,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Install
@ -784,7 +783,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
uses: dtolnay/rust-toolchain@stable
@ -824,7 +823,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Publish
run: |

View File

@ -36,7 +36,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install
uses: dtolnay/rust-toolchain@stable
@ -118,7 +118,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
cache: 'yarn'
- name: Install dependencies
run: |

View File

@ -66,7 +66,7 @@
"@octokit/rest": "^22.0.1",
"clipanion": "^4.0.0-rc.4",
"colorette": "^2.0.20",
"emnapi": "^1.7.0",
"emnapi": "^1.7.1",
"es-toolkit": "^1.41.0",
"js-yaml": "^4.1.0",
"obug": "^2.0.0",
@ -74,7 +74,7 @@
"typanion": "^3.14.0"
},
"devDependencies": {
"@emnapi/runtime": "^1.7.0",
"@emnapi/runtime": "^1.7.1",
"@oxc-node/core": "^0.0.34",
"@std/toml": "npm:@jsr/std__toml@^1.0.11",
"@types/inquirer": "^9.0.9",
@ -90,14 +90,11 @@
"typescript": "^5.9.3"
},
"peerDependencies": {
"@emnapi/runtime": "^1.7.0"
"@emnapi/runtime": "^1.7.1"
},
"peerDependenciesMeta": {
"@emnapi/runtime": {
"optional": true
},
"emnapi": {
"optional": true
}
},
"funding": {

View File

@ -223,26 +223,35 @@ impl NapiEnum {
let js_name_lit = Literal::string(&format!("{}\0", &self.js_name));
let register_name = &self.register_name;
let mut define_properties = vec![];
let mut value_conversions = vec![];
let mut property_descriptors = vec![];
let mut value_names = vec![];
for variant in self.variants.iter() {
for (idx, variant) in self.variants.iter().enumerate() {
let name_lit = Literal::string(&format!("{}\0", variant.name));
let val_lit: Literal = (&variant.val).into();
let value_var = Ident::new(&format!("__enum_value_{}", idx), Span::call_site());
define_properties.push(quote! {
{
let name = std::ffi::CStr::from_bytes_with_nul_unchecked(#name_lit.as_bytes());
napi::bindgen_prelude::check_status!(
napi::bindgen_prelude::sys::napi_set_named_property(
env,
obj_ptr, name.as_ptr(),
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #val_lit)?
),
"Failed to defined enum `{}`",
#js_name_lit
)?;
};
})
value_names.push(value_var.clone());
// Convert the value first
value_conversions.push(quote! {
let #value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #val_lit)?;
});
// Create property descriptor using the pre-computed value
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#name_lit.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::default,
data: std::ptr::null_mut(),
}
});
}
let callback_name = Ident::new(
@ -252,6 +261,17 @@ impl NapiEnum {
let js_mod_ident = js_mod_to_token_stream(self.js_mod.as_ref());
let object_creation = quote! {
// Convert all values first, so error handling works correctly
#(#value_conversions)*
let properties = [
#(#property_descriptors),*
];
let obj_ptr = napi::bindgen_prelude::create_object_with_properties(env, &properties)?;
};
quote! {
#[allow(non_snake_case)]
#[allow(clippy::all)]
@ -259,14 +279,7 @@ impl NapiEnum {
use std::ffi::CString;
use std::ptr;
let mut obj_ptr = ptr::null_mut();
napi::bindgen_prelude::check_status!(
napi::bindgen_prelude::sys::napi_create_object(env, &mut obj_ptr),
"Failed to create napi object"
)?;
#(#define_properties)*
#object_creation
Ok(obj_ptr)
}

View File

@ -482,12 +482,18 @@ impl NapiStruct {
let name = &self.name;
let name_str = self.name.to_string();
let mut obj_field_setters = vec![];
let mut obj_field_getters = vec![];
let mut field_destructions = vec![];
for field in obj.fields.iter() {
// For optimized object creation: separate always-set fields from conditionally-set fields
let mut value_conversions = vec![];
let mut property_descriptors = vec![];
let mut conditional_setters = vec![];
let mut value_names = vec![];
for (idx, field) in obj.fields.iter().enumerate() {
let field_js_name = &field.js_name;
let field_js_name_lit = Literal::string(&format!("{}\0", field.js_name));
let mut ty = field.ty.clone();
remove_lifetime_in_type(&mut ty);
let is_optional_field = if let syn::Type::Path(syn::TypePath {
@ -503,28 +509,60 @@ impl NapiStruct {
} else {
false
};
// Determine if this field is always set or conditionally set
let is_always_set = !is_optional_field || self.use_nullable;
match &field.name {
syn::Member::Named(ident) => {
let alias_ident = format_ident!("{}_", ident);
field_destructions.push(quote! { #ident: #alias_ident });
if is_optional_field {
obj_field_setters.push(match self.use_nullable {
false => quote! {
if #alias_ident.is_some() {
obj.set(#field_js_name, #alias_ident)?;
}
},
true => quote! {
if let Some(#alias_ident) = #alias_ident {
obj.set(#field_js_name, #alias_ident)?;
if is_always_set {
// This field is always set - use batched approach
let value_var = Ident::new(&format!("__obj_value_{}", idx), Span::call_site());
value_names.push(value_var.clone());
if is_optional_field {
// Optional with use_nullable=true: set to value or null
value_conversions.push(quote! {
let #value_var = if let Some(inner) = #alias_ident {
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, inner)?
} else {
obj.set(#field_js_name, napi::bindgen_prelude::Null)?;
}
},
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, napi::bindgen_prelude::Null)?
};
});
} else {
// Non-optional: always set
value_conversions.push(quote! {
let #value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #alias_ident)?;
});
}
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#field_js_name_lit.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::writable
| napi::bindgen_prelude::sys::PropertyAttributes::enumerable
| napi::bindgen_prelude::sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
}
});
} else {
obj_field_setters.push(quote! { obj.set(#field_js_name, #alias_ident)?; });
// Optional with use_nullable=false: conditionally set
conditional_setters.push(quote! {
if #alias_ident.is_some() {
obj.set(#field_js_name, #alias_ident)?;
}
});
}
// Getters remain the same
if is_optional_field && !self.use_nullable {
obj_field_getters.push(quote! {
let #alias_ident: #ty = obj.get(#field_js_name).map_err(|mut err| {
@ -547,24 +585,52 @@ impl NapiStruct {
syn::Member::Unnamed(i) => {
let arg_name = format_ident!("arg{}", i);
field_destructions.push(quote! { #arg_name });
if is_optional_field {
obj_field_setters.push(match self.use_nullable {
false => quote! {
if #arg_name.is_some() {
obj.set(#field_js_name, #arg_name)?;
}
},
true => quote! {
if let Some(#arg_name) = #arg_name {
obj.set(#field_js_name, #arg_name)?;
if is_always_set {
// This field is always set - use batched approach
let value_var = Ident::new(&format!("__obj_value_{}", idx), Span::call_site());
value_names.push(value_var.clone());
if is_optional_field {
// Optional with use_nullable=true: set to value or null
value_conversions.push(quote! {
let #value_var = if let Some(inner) = #arg_name {
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, inner)?
} else {
obj.set(#field_js_name, napi::bindgen_prelude::Null)?;
}
},
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, napi::bindgen_prelude::Null)?
};
});
} else {
// Non-optional: always set
value_conversions.push(quote! {
let #value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #arg_name)?;
});
}
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#field_js_name_lit.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::writable
| napi::bindgen_prelude::sys::PropertyAttributes::enumerable
| napi::bindgen_prelude::sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
}
});
} else {
obj_field_setters.push(quote! { obj.set(#field_js_name, #arg_name)?; });
// Optional with use_nullable=false: conditionally set
conditional_setters.push(quote! {
if #arg_name.is_some() {
obj.set(#field_js_name, #arg_name)?;
}
});
}
// Getters remain the same
if is_optional_field && !self.use_nullable {
obj_field_getters.push(quote! { let #arg_name: #ty = obj.get(#field_js_name)?; });
} else {
@ -611,20 +677,48 @@ impl NapiStruct {
)
};
// Generate object creation code
let object_creation = if conditional_setters.is_empty() {
// All fields are always set - use fully batched approach
quote! {
// Convert all values first, so error handling works correctly
#(#value_conversions)*
let properties = [
#(#property_descriptors),*
];
let obj_ptr = napi::bindgen_prelude::create_object_with_properties(env, &properties)?;
Ok(obj_ptr)
}
} else {
// Some fields are conditionally set - use batched for always-set, then add conditionals
quote! {
// Convert all always-set values first
#(#value_conversions)*
let properties = [
#(#property_descriptors),*
];
let obj_ptr = napi::bindgen_prelude::create_object_with_properties(env, &properties)?;
// Wrap in Object for conditional field setters
let mut obj = napi::bindgen_prelude::Object::from_raw(env, obj_ptr);
#(#conditional_setters)*
Ok(obj_ptr)
}
};
let to_napi_value = if obj.object_to_js {
quote! {
#[automatically_derived]
#to_napi_value_impl {
unsafe fn to_napi_value(env: napi::bindgen_prelude::sys::napi_env, val: #name_with_lifetime) -> napi::bindgen_prelude::Result<napi::bindgen_prelude::sys::napi_value> {
#[allow(unused_variables)]
let env_wrapper = napi::bindgen_prelude::Env::from(env);
#[allow(unused_mut)]
let mut obj = napi::bindgen_prelude::Object::new(&env_wrapper)?;
let #destructed_fields = val;
#(#obj_field_setters)*
napi::bindgen_prelude::Object::to_napi_value(env, obj)
#object_creation
}
}
}
@ -865,6 +959,7 @@ impl NapiStruct {
let name = &self.name;
let name_str = self.name.to_string();
let discriminant = structured_enum.discriminant.as_str();
let discriminant_null_terminated = format!("{}\0", discriminant);
let mut variant_arm_setters = vec![];
let mut variant_arm_getters = vec![];
@ -875,13 +970,38 @@ impl NapiStruct {
if let Some(case) = structured_enum.discriminant_case {
variant_name_str = to_case(variant_name_str, case);
}
let mut obj_field_setters = vec![quote! {
obj.set(#discriminant, #variant_name_str)?;
}];
let mut obj_field_getters = vec![];
let mut field_destructions = vec![];
for field in variant.fields.iter() {
// For optimized object creation
let mut value_conversions = vec![];
let mut property_descriptors = vec![];
let mut conditional_setters = vec![];
// First property is always the discriminant
let discriminant_value_var = Ident::new("__discriminant_value", Span::call_site());
value_conversions.push(quote! {
let #discriminant_value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #variant_name_str)?;
});
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#discriminant_null_terminated.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #discriminant_value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::writable
| napi::bindgen_prelude::sys::PropertyAttributes::enumerable
| napi::bindgen_prelude::sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
}
});
for (idx, field) in variant.fields.iter().enumerate() {
let field_js_name = &field.js_name;
let field_js_name_lit = Literal::string(&format!("{}\0", field.js_name));
let mut ty = field.ty.clone();
remove_lifetime_in_type(&mut ty);
let is_optional_field = if let syn::Type::Path(syn::TypePath {
@ -897,28 +1017,59 @@ impl NapiStruct {
} else {
false
};
// Determine if this field is always set or conditionally set
let is_always_set = !is_optional_field || self.use_nullable;
match &field.name {
syn::Member::Named(ident) => {
let alias_ident = format_ident!("{}_", ident);
field_destructions.push(quote! { #ident: #alias_ident });
if is_optional_field {
obj_field_setters.push(match self.use_nullable {
false => quote! {
if #alias_ident.is_some() {
obj.set(#field_js_name, #alias_ident)?;
}
},
true => quote! {
if let Some(#alias_ident) = #alias_ident {
obj.set(#field_js_name, #alias_ident)?;
if is_always_set {
// This field is always set - use batched approach
let value_var = Ident::new(&format!("__variant_value_{}", idx), Span::call_site());
if is_optional_field {
// Optional with use_nullable=true: set to value or null
value_conversions.push(quote! {
let #value_var = if let Some(inner) = #alias_ident {
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, inner)?
} else {
obj.set(#field_js_name, napi::bindgen_prelude::Null)?;
}
},
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, napi::bindgen_prelude::Null)?
};
});
} else {
// Non-optional: always set
value_conversions.push(quote! {
let #value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #alias_ident)?;
});
}
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#field_js_name_lit.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::writable
| napi::bindgen_prelude::sys::PropertyAttributes::enumerable
| napi::bindgen_prelude::sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
}
});
} else {
obj_field_setters.push(quote! { obj.set(#field_js_name, #alias_ident)?; });
// Optional with use_nullable=false: conditionally set
conditional_setters.push(quote! {
if #alias_ident.is_some() {
obj.set(#field_js_name, #alias_ident)?;
}
});
}
// Getters remain the same
if is_optional_field && !self.use_nullable {
obj_field_getters.push(quote! {
let #alias_ident: #ty = obj.get(#field_js_name).map_err(|mut err| {
@ -941,33 +1092,60 @@ impl NapiStruct {
syn::Member::Unnamed(i) => {
let arg_name = format_ident!("arg{}", i);
field_destructions.push(quote! { #arg_name });
if is_optional_field {
obj_field_setters.push(match self.use_nullable {
false => quote! {
if #arg_name.is_some() {
obj.set(#field_js_name, #arg_name)?;
}
},
true => quote! {
if let Some(#arg_name) = #arg_name {
obj.set(#field_js_name, #arg_name)?;
if is_always_set {
// This field is always set - use batched approach
let value_var = Ident::new(&format!("__variant_value_{}", idx), Span::call_site());
if is_optional_field {
// Optional with use_nullable=true: set to value or null
value_conversions.push(quote! {
let #value_var = if let Some(inner) = #arg_name {
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, inner)?
} else {
obj.set(#field_js_name, napi::bindgen_prelude::Null)?;
}
},
napi::bindgen_prelude::ToNapiValue::to_napi_value(env, napi::bindgen_prelude::Null)?
};
});
} else {
// Non-optional: always set
value_conversions.push(quote! {
let #value_var = napi::bindgen_prelude::ToNapiValue::to_napi_value(env, #arg_name)?;
});
}
property_descriptors.push(quote! {
napi::bindgen_prelude::sys::napi_property_descriptor {
utf8name: std::ffi::CStr::from_bytes_with_nul_unchecked(#field_js_name_lit.as_bytes()).as_ptr(),
name: std::ptr::null_mut(),
method: None,
getter: None,
setter: None,
value: #value_var,
attributes: napi::bindgen_prelude::sys::PropertyAttributes::writable
| napi::bindgen_prelude::sys::PropertyAttributes::enumerable
| napi::bindgen_prelude::sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
}
});
} else {
obj_field_setters.push(quote! { obj.set(#field_js_name, #arg_name)?; });
// Optional with use_nullable=false: conditionally set
conditional_setters.push(quote! {
if #arg_name.is_some() {
obj.set(#field_js_name, #arg_name)?;
}
});
}
// Getters remain the same
if is_optional_field && !self.use_nullable {
obj_field_getters.push(quote! { let #arg_name: #ty = obj.get(#field_js_name)?; });
} else {
obj_field_getters.push(quote! {
let #arg_name: #ty = obj.get(#field_js_name)?.ok_or_else(|| napi::bindgen_prelude::Error::new(
napi::bindgen_prelude::Status::InvalidArg,
format!("Missing field `{}`", #field_js_name),
))?;
});
let #arg_name: #ty = obj.get(#field_js_name)?.ok_or_else(|| napi::bindgen_prelude::Error::new(
napi::bindgen_prelude::Status::InvalidArg,
format!("Missing field `{}`", #field_js_name),
))?;
});
}
}
}
@ -983,9 +1161,39 @@ impl NapiStruct {
}
};
// Generate object creation for this variant
let variant_object_creation = if conditional_setters.is_empty() {
// All fields are always set - use fully batched approach
quote! {
#(#value_conversions)*
let properties = [
#(#property_descriptors),*
];
napi::bindgen_prelude::create_object_with_properties(env, &properties)
}
} else {
// Some fields are conditionally set
quote! {
#(#value_conversions)*
let properties = [
#(#property_descriptors),*
];
let obj_ptr = napi::bindgen_prelude::create_object_with_properties(env, &properties)?;
let mut obj = napi::bindgen_prelude::Object::from_raw(env, obj_ptr);
#(#conditional_setters)*
Ok(obj_ptr)
}
};
variant_arm_setters.push(quote! {
#destructed_fields => {
#(#obj_field_setters)*
#variant_object_creation
},
});
@ -1001,15 +1209,9 @@ impl NapiStruct {
quote! {
impl napi::bindgen_prelude::ToNapiValue for #name {
unsafe fn to_napi_value(env: napi::bindgen_prelude::sys::napi_env, val: #name) -> napi::bindgen_prelude::Result<napi::bindgen_prelude::sys::napi_value> {
#[allow(unused_variables)]
let env_wrapper = napi::bindgen_prelude::Env::from(env);
#[allow(unused_mut)]
let mut obj = napi::bindgen_prelude::Object::new(&env_wrapper)?;
match val {
#(#variant_arm_setters)*
};
napi::bindgen_prelude::Object::to_napi_value(env, obj)
}
}
}
}

View File

@ -25,7 +25,7 @@ async = ["tokio_rt"]
chrono_date = ["chrono", "napi5"]
# Enable deprecated types and traits for compatibility
compat-mode = []
default = ["napi4"]
default = ["napi4", "dyn-symbols"]
deferred_trace = ["napi4"]
error_anyhow = ["anyhow"]
experimental = ["napi-sys/experimental"]

View File

@ -25,25 +25,31 @@ where
{
unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
let env = Env::from(raw_env);
#[cfg_attr(feature = "experimental", allow(unused_mut))]
#[cfg_attr(feature = "napi10", allow(unused_mut))]
let mut obj = Object::new(&env)?;
#[cfg(all(
feature = "napi10",
feature = "node_version_detect",
feature = "dyn-symbols"
))]
let node_version = NODE_VERSION.get().unwrap();
for (k, v) in val.into_iter() {
#[cfg(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
))]
{
if NODE_VERSION_MAJOR >= 20 && NODE_VERSION_MINOR >= 18 {
if node_version.major >= 20 && node_version.minor >= 18 {
fast_set_property(raw_env, obj.0.value, k, v)?;
} else {
obj.set(k.as_ref(), v)?;
}
}
#[cfg(not(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
)))]
obj.set(k.as_ref(), v)?;
}
@ -91,25 +97,31 @@ where
{
unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
let env = Env::from(raw_env);
#[cfg_attr(feature = "experimental", allow(unused_mut))]
#[cfg_attr(feature = "napi10", allow(unused_mut))]
let mut obj = Object::new(&env)?;
#[cfg(all(
feature = "napi10",
feature = "node_version_detect",
feature = "dyn-symbols"
))]
let node_version = NODE_VERSION.get().unwrap();
for (k, v) in val.into_iter() {
#[cfg(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
))]
{
if crate::bindgen_runtime::NODE_VERSION_MAJOR >= 20 && NODE_VERSION_MINOR >= 18 {
if node_version.major >= 20 && node_version.minor >= 18 {
fast_set_property(raw_env, obj.0.value, k, v)?;
} else {
obj.set(k.as_ref(), v)?;
}
}
#[cfg(not(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
)))]
obj.set(k.as_ref(), v)?;
}
@ -159,16 +171,22 @@ where
{
unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
let env = Env::from(raw_env);
#[cfg_attr(feature = "experimental", allow(unused_mut))]
#[cfg_attr(feature = "napi10", allow(unused_mut))]
let mut obj = Object::new(&env)?;
#[cfg(all(
feature = "napi10",
feature = "node_version_detect",
feature = "dyn-symbols"
))]
let node_version = NODE_VERSION.get().unwrap();
for (k, v) in val.into_iter() {
#[cfg(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
))]
{
if crate::bindgen_runtime::NODE_VERSION_MAJOR >= 20 && NODE_VERSION_MINOR >= 18 {
if node_version.major >= 20 && node_version.minor >= 18 {
fast_set_property(raw_env, obj.0.value, k, v)?;
} else {
obj.set(k.as_ref(), v)?;
@ -177,7 +195,7 @@ where
#[cfg(not(all(
feature = "experimental",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
)))]
obj.set(k.as_ref(), v)?;
}
@ -207,9 +225,9 @@ where
}
#[cfg(all(
feature = "experimental",
feature = "napi10",
feature = "node_version_detect",
any(all(target_os = "linux", feature = "dyn-symbols"), target_os = "macos")
feature = "dyn-symbols"
))]
fn fast_set_property<K: AsRef<str>, V: ToNapiValue>(
raw_env: sys::napi_env,

View File

@ -113,3 +113,85 @@ pub unsafe extern "C" fn drop_buffer_slice(
drop(Vec::from_raw_parts(finalize_data, len, cap));
}
}
/// Create an object with properties
///
/// When the `experimental` feature is enabled, uses `napi_create_object_with_properties`
/// which creates the object with all properties in a single optimized call.
/// Otherwise falls back to `napi_create_object` + `napi_define_properties`.
#[doc(hidden)]
#[cfg(not(feature = "noop"))]
#[inline]
pub unsafe fn create_object_with_properties(
env: sys::napi_env,
properties: &[sys::napi_property_descriptor],
) -> Result<sys::napi_value> {
use crate::check_status;
let mut obj_ptr = std::ptr::null_mut();
#[cfg(all(
feature = "experimental",
feature = "node_version_detect",
not(target_family = "wasm")
))]
{
let node_version = NODE_VERSION.get().unwrap();
if !properties.is_empty()
&& ((node_version.major == 25 && node_version.minor >= 2) || node_version.major > 25)
{
// Convert property names from C strings to napi_value
let mut names: Vec<sys::napi_value> = Vec::with_capacity(properties.len());
let mut values: Vec<sys::napi_value> = Vec::with_capacity(properties.len());
for prop in properties {
let mut name_value = std::ptr::null_mut();
// utf8name is a null-terminated C string, use -1 to auto-detect length
check_status!(
sys::napi_create_string_utf8(env, prop.utf8name, -1, &mut name_value),
"Failed to create property name string",
)?;
names.push(name_value);
values.push(prop.value);
}
let mut result_obj = std::ptr::null_mut();
check_status!(
sys::napi_create_object_with_properties(
env,
std::ptr::null_mut(), // prototype_or_null
names.as_ptr(),
values.as_ptr(),
properties.len(),
&mut result_obj,
),
"Failed to create object with properties",
)?;
return Ok(result_obj);
}
}
// Fallback: create object then define properties
check_status!(
sys::napi_create_object(env, &mut obj_ptr),
"Failed to create object",
)?;
if !properties.is_empty() {
check_status!(
sys::napi_define_properties(env, obj_ptr, properties.len(), properties.as_ptr()),
"Failed to define properties",
)?;
}
Ok(obj_ptr)
}
#[doc(hidden)]
#[cfg(feature = "noop")]
pub unsafe fn create_object_with_properties(
_env: sys::napi_env,
_properties: &[sys::napi_property_descriptor],
) -> Result<sys::napi_value> {
Ok(std::ptr::null_mut())
}

View File

@ -9,6 +9,8 @@ use std::ffi::CStr;
use std::mem::MaybeUninit;
#[cfg(not(feature = "noop"))]
use std::ptr;
#[cfg(all(not(feature = "noop"), feature = "node_version_detect"))]
use std::sync::OnceLock;
#[cfg(not(feature = "noop"))]
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
@ -18,6 +20,8 @@ use std::{any::TypeId, collections::HashMap};
use rustc_hash::FxBuildHasher;
#[cfg(all(not(feature = "noop"), feature = "node_version_detect"))]
use crate::NodeVersion;
#[cfg(not(feature = "noop"))]
use crate::{check_status, check_status_or_throw, JsError};
use crate::{sys, Property, Result};
@ -30,12 +34,8 @@ pub type ExportRegisterHookCallback =
pub type ModuleExportsCallback =
unsafe fn(env: sys::napi_env, exports: sys::napi_value) -> Result<()>;
#[cfg(feature = "node_version_detect")]
pub static mut NODE_VERSION_MAJOR: u32 = 0;
#[cfg(feature = "node_version_detect")]
pub static mut NODE_VERSION_MINOR: u32 = 0;
#[cfg(feature = "node_version_detect")]
pub static mut NODE_VERSION_PATCH: u32 = 0;
#[cfg(all(not(feature = "noop"), feature = "node_version_detect"))]
pub static NODE_VERSION: OnceLock<NodeVersion> = OnceLock::new();
#[repr(transparent)]
pub(crate) struct PersistedPerInstanceHashMap<K, V, S>(RefCell<HashMap<K, V, S>>);
@ -109,7 +109,11 @@ static MODULE_CLASS_PROPERTIES: LazyLock<ModuleClassProperty> = LazyLock::new(De
static MODULE_COUNT: AtomicUsize = AtomicUsize::new(0);
#[cfg(not(feature = "noop"))]
static FIRST_MODULE_REGISTERED: AtomicBool = AtomicBool::new(false);
#[cfg(all(feature = "tokio_rt", not(feature = "noop")))]
#[cfg(all(
feature = "tokio_rt",
not(target_family = "wasm"),
not(feature = "noop")
))]
static ENV_CLEANUP_HOOK_ADDED: RwLock<bool> = RwLock::new(false);
thread_local! {
static REGISTERED_CLASSES: LazyCell<RegisteredClasses> = LazyCell::new(Default::default);
@ -253,18 +257,21 @@ pub unsafe extern "C" fn napi_register_module_v1(
}
#[cfg(feature = "node_version_detect")]
{
let mut node_version = MaybeUninit::uninit();
check_status_or_throw!(
env,
unsafe { sys::napi_get_node_version(env, node_version.as_mut_ptr()) },
"Failed to get node version"
);
let node_version = *node_version.assume_init();
unsafe {
NODE_VERSION_MAJOR = node_version.major;
NODE_VERSION_MINOR = node_version.minor;
NODE_VERSION_PATCH = node_version.patch;
};
NODE_VERSION.get_or_init(|| {
let mut node_version = MaybeUninit::uninit();
check_status_or_throw!(
env,
unsafe { sys::napi_get_node_version(env, node_version.as_mut_ptr()) },
"Failed to get node version"
);
let node_version = *node_version.assume_init();
NodeVersion {
major: node_version.major,
minor: node_version.minor,
patch: node_version.patch,
release: unsafe { CStr::from_ptr(node_version.release).to_str().unwrap() },
}
});
}
if MODULE_COUNT.fetch_add(1, Ordering::SeqCst) != 0 {

View File

@ -12,7 +12,8 @@ rust-version.workspace = true
version = "3.1.1"
[features]
dyn-symbols = [] # Deprecated feature
default = ["dyn-symbols"]
dyn-symbols = ["dep:libloading"]
experimental = []
napi1 = []
napi2 = ["napi1"]
@ -29,4 +30,4 @@ napi10 = ["napi9"]
independent = true
[dependencies]
libloading = { version = "0.9" }
libloading = { version = "0.9", optional = true }

View File

@ -802,6 +802,15 @@ mod experimental {
finalize_data: *mut c_void,
finalize_hint: *mut c_void,
) -> napi_status;
fn napi_create_object_with_properties(
env: napi_env,
prototype_or_null: napi_value,
property_names: *const napi_value,
property_values: *const napi_value,
property_count: usize,
result: *mut napi_value,
) -> napi_status;
}
);
}

View File

@ -54,10 +54,8 @@ macro_rules! generate {
let symbol: Result<libloading::Symbol<unsafe extern "C" fn ($(_: $ptype,)*)$( -> $rtype)*>, libloading::Error> = host.get(stringify!($name).as_bytes());
match symbol {
Ok(f) => *f,
Err(e) => {
#[cfg(debug_assertions)] {
eprintln!("Load Node-API [{}] from host runtime failed: {}", stringify!($name), e);
}
Err(_) => {
// ignore error, use the stub function
NAPI.$name
}
}

View File

@ -43,8 +43,8 @@
"tslib": "^2.8.1"
},
"dependencies": {
"@emnapi/core": "^1.7.0",
"@emnapi/runtime": "^1.7.0",
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"scripts": {

View File

@ -104,22 +104,22 @@ __metadata:
languageName: node
linkType: hard
"@emnapi/core@npm:^1.1.0, @emnapi/core@npm:^1.7.0":
version: 1.7.0
resolution: "@emnapi/core@npm:1.7.0"
"@emnapi/core@npm:^1.1.0, @emnapi/core@npm:^1.7.0, @emnapi/core@npm:^1.7.1":
version: 1.7.1
resolution: "@emnapi/core@npm:1.7.1"
dependencies:
"@emnapi/wasi-threads": "npm:1.1.0"
tslib: "npm:^2.4.0"
checksum: 10c0/ea57802079fda31f87506bba63f1299f0fa60546c1a1a424d2d5926f98f1ffc4a94ae3c885155f4a60114c19d314addb45d94dc0e427ac1594cbfca7cd910a31
checksum: 10c0/f3740be23440b439333e3ae3832163f60c96c4e35337f3220ceba88f36ee89a57a871d27c94eb7a9ff98a09911ed9a2089e477ab549f4d30029f8b907f84a351
languageName: node
linkType: hard
"@emnapi/runtime@npm:^1.1.0, @emnapi/runtime@npm:^1.7.0":
version: 1.7.0
resolution: "@emnapi/runtime@npm:1.7.0"
"@emnapi/runtime@npm:^1.1.0, @emnapi/runtime@npm:^1.7.1":
version: 1.7.1
resolution: "@emnapi/runtime@npm:1.7.1"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/b99334582effe146e9fb5cd9e7f866c6c7047a8576f642456d56984b574b40b2ba14e4aede26217fcefa1372ddd1e098a19912f17033a9ae469928b0dc65a682
checksum: 10c0/26b851cd3e93877d8732a985a2ebf5152325bbacc6204ef5336a47359dedcc23faeb08cdfcb8bb389b5401b3e894b882bc1a1e55b4b7c1ed1e67c991a760ddd5
languageName: node
linkType: hard
@ -1366,7 +1366,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@napi-rs/cli@workspace:cli"
dependencies:
"@emnapi/runtime": "npm:^1.7.0"
"@emnapi/runtime": "npm:^1.7.1"
"@inquirer/prompts": "npm:^8.0.0"
"@napi-rs/cross-toolchain": "npm:^1.0.3"
"@napi-rs/wasm-tools": "npm:^1.0.1"
@ -1380,7 +1380,7 @@ __metadata:
ava: "npm:^6.4.1"
clipanion: "npm:^4.0.0-rc.4"
colorette: "npm:^2.0.20"
emnapi: "npm:^1.7.0"
emnapi: "npm:^1.7.1"
empathic: "npm:^2.0.0"
env-paths: "npm:^3.0.0"
es-toolkit: "npm:^1.41.0"
@ -1393,12 +1393,10 @@ __metadata:
typanion: "npm:^3.14.0"
typescript: "npm:^5.9.3"
peerDependencies:
"@emnapi/runtime": ^1.7.0
"@emnapi/runtime": ^1.7.1
peerDependenciesMeta:
"@emnapi/runtime":
optional: true
emnapi:
optional: true
bin:
napi: ./dist/cli.js
napi-raw: ./cli.mjs
@ -1826,8 +1824,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@napi-rs/wasm-runtime@workspace:wasm-runtime"
dependencies:
"@emnapi/core": "npm:^1.7.0"
"@emnapi/runtime": "npm:^1.7.0"
"@emnapi/core": "npm:^1.7.1"
"@emnapi/runtime": "npm:^1.7.1"
"@rollup/plugin-alias": "npm:^6.0.0"
"@rollup/plugin-commonjs": "npm:^29.0.0"
"@rollup/plugin-inject": "npm:^5.0.5"
@ -6480,15 +6478,15 @@ __metadata:
languageName: node
linkType: hard
"emnapi@npm:^1.7.0":
version: 1.7.0
resolution: "emnapi@npm:1.7.0"
"emnapi@npm:^1.7.1":
version: 1.7.1
resolution: "emnapi@npm:1.7.1"
peerDependencies:
node-addon-api: ">= 6.1.0"
peerDependenciesMeta:
node-addon-api:
optional: true
checksum: 10c0/a09c849f46022bc42f971a7cd21f4bd9a98546d5cf655e8377ecdbfd909c9e4601e45d23e7d56174a5eb1cfc7db80e9565299d89bbe353e123b4a4e6463396af
checksum: 10c0/e3b223cb75bed94a8d11a3fa4c1b621bf32556e3c31239c589593fc275df80630af1a113fb2598436c2b08d7d8fd0099d9d24a3fc08cabddca5b0aa044a14cf2
languageName: node
linkType: hard