From e4f5360dcd7cfc5241c3bae3b46b9ae844ebf147 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 23:21:23 +0800 Subject: [PATCH] feat(napi): add node_api_create_object_with_properties support for enum creation (#2990) --- .github/workflows/asan.yml | 2 +- .github/workflows/test-release.yaml | 21 +- .github/workflows/zig.yaml | 4 +- cli/package.json | 9 +- crates/backend/src/codegen/enum.rs | 61 +-- crates/backend/src/codegen/struct.rs | 368 ++++++++++++++---- crates/napi/Cargo.toml | 2 +- .../napi/src/bindgen_runtime/js_values/map.rs | 56 ++- crates/napi/src/bindgen_runtime/mod.rs | 82 ++++ .../src/bindgen_runtime/module_register.rs | 45 ++- crates/sys/Cargo.toml | 5 +- crates/sys/src/functions.rs | 9 + crates/sys/src/lib.rs | 6 +- wasm-runtime/package.json | 4 +- yarn.lock | 36 +- 15 files changed, 517 insertions(+), 193 deletions(-) diff --git a/.github/workflows/asan.yml b/.github/workflows/asan.yml index 22c24456..99581ca2 100644 --- a/.github/workflows/asan.yml +++ b/.github/workflows/asan.yml @@ -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 diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index d2beef17..bab0cfd1 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -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: | diff --git a/.github/workflows/zig.yaml b/.github/workflows/zig.yaml index e2ca398c..9b158514 100644 --- a/.github/workflows/zig.yaml +++ b/.github/workflows/zig.yaml @@ -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: | diff --git a/cli/package.json b/cli/package.json index cd7fc4a0..ca5f7eb0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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": { diff --git a/crates/backend/src/codegen/enum.rs b/crates/backend/src/codegen/enum.rs index 7263b364..661a9cef 100644 --- a/crates/backend/src/codegen/enum.rs +++ b/crates/backend/src/codegen/enum.rs @@ -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) } diff --git a/crates/backend/src/codegen/struct.rs b/crates/backend/src/codegen/struct.rs index da123f04..c8767e8a 100644 --- a/crates/backend/src/codegen/struct.rs +++ b/crates/backend/src/codegen/struct.rs @@ -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 { - #[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 { - #[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) + } } } } diff --git a/crates/napi/Cargo.toml b/crates/napi/Cargo.toml index d497fcc1..8549ac2e 100644 --- a/crates/napi/Cargo.toml +++ b/crates/napi/Cargo.toml @@ -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"] diff --git a/crates/napi/src/bindgen_runtime/js_values/map.rs b/crates/napi/src/bindgen_runtime/js_values/map.rs index b56e1c09..0296f821 100644 --- a/crates/napi/src/bindgen_runtime/js_values/map.rs +++ b/crates/napi/src/bindgen_runtime/js_values/map.rs @@ -25,25 +25,31 @@ where { unsafe fn to_napi_value(raw_env: sys::napi_env, val: Self) -> Result { 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 { 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 { 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, V: ToNapiValue>( raw_env: sys::napi_env, diff --git a/crates/napi/src/bindgen_runtime/mod.rs b/crates/napi/src/bindgen_runtime/mod.rs index 6f60bbd5..63225f00 100644 --- a/crates/napi/src/bindgen_runtime/mod.rs +++ b/crates/napi/src/bindgen_runtime/mod.rs @@ -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 { + 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 = Vec::with_capacity(properties.len()); + let mut values: Vec = 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 { + Ok(std::ptr::null_mut()) +} diff --git a/crates/napi/src/bindgen_runtime/module_register.rs b/crates/napi/src/bindgen_runtime/module_register.rs index c2ec6cff..7cca7e78 100644 --- a/crates/napi/src/bindgen_runtime/module_register.rs +++ b/crates/napi/src/bindgen_runtime/module_register.rs @@ -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 = OnceLock::new(); #[repr(transparent)] pub(crate) struct PersistedPerInstanceHashMap(RefCell>); @@ -109,7 +109,11 @@ static MODULE_CLASS_PROPERTIES: LazyLock = 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 = RwLock::new(false); thread_local! { static REGISTERED_CLASSES: LazyCell = 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 { diff --git a/crates/sys/Cargo.toml b/crates/sys/Cargo.toml index e3fa54ce..5f9931e1 100644 --- a/crates/sys/Cargo.toml +++ b/crates/sys/Cargo.toml @@ -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 } diff --git a/crates/sys/src/functions.rs b/crates/sys/src/functions.rs index 1001f708..7a01a799 100644 --- a/crates/sys/src/functions.rs +++ b/crates/sys/src/functions.rs @@ -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; } ); } diff --git a/crates/sys/src/lib.rs b/crates/sys/src/lib.rs index 25e52a3c..f9ea57c1 100644 --- a/crates/sys/src/lib.rs +++ b/crates/sys/src/lib.rs @@ -54,10 +54,8 @@ macro_rules! generate { let symbol: Result $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 } } diff --git a/wasm-runtime/package.json b/wasm-runtime/package.json index 4c6730cc..066ea916 100644 --- a/wasm-runtime/package.json +++ b/wasm-runtime/package.json @@ -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": { diff --git a/yarn.lock b/yarn.lock index 2d60979c..6cfa9f85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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