diff --git a/.cargo/config.toml b/.cargo/config.toml index 969c2747..1a562eb6 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,9 @@ # https://github.com/rust-lang/rust/issues/134820 # pthread_key_create() destructors and segfault after a DSO unloading [target.'cfg(any(all(target_env = "gnu", not(target_os = "windows")), target_os = "freebsd"))'] -rustflags = ["-C", "link-args=-Wl,-z,nodelete"] +rustflags = [ + "-C", + "link-args=-Wl,--warn-unresolved-symbols", + "-C", + "link-args=-Wl,-z,nodelete", +] diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 2ccf73f1..bc8f1540 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -91,7 +91,7 @@ jobs: yarn test:cli yarn test yarn tsc -p examples/napi/tsconfig.json --noEmit --skipLibCheck - yarn test:macro + RUSTFLAGS="-C link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains" yarn test:macro toolchain: stable - host: windows-latest target: x86_64-pc-windows-msvc diff --git a/examples/napi-shared/src/lib.rs b/examples/napi-shared/src/lib.rs index ef4b2251..788218ab 100644 --- a/examples/napi-shared/src/lib.rs +++ b/examples/napi-shared/src/lib.rs @@ -1,6 +1,75 @@ +use napi::{bindgen_prelude::ClassInstance, Either}; use napi_derive::napi; #[napi(object)] pub struct Shared { pub value: u32, } + +// Test fixture for GitHub issue #2722: Complex struct with constructor and multiple methods +#[napi] +pub struct ComplexClass { + pub value: String, + pub number: i32, +} + +impl From<(String, i32)> for ComplexClass { + fn from(value: (String, i32)) -> Self { + ComplexClass { + value: value.0, + number: value.1, + } + } +} + +impl<'env> From, String>> for ComplexClass { + fn from(value: Either, String>) -> Self { + match value { + Either::A(instance) => ComplexClass { + value: (&*instance).value.clone(), + number: instance.number, + }, + Either::B(value) => ComplexClass { value, number: 0 }, + } + } +} + +#[napi] +impl ComplexClass { + #[napi(constructor)] + pub fn new(value: Either>, number: i32) -> Self { + let value_str = match value { + Either::A(s) => s, + Either::B(instance) => format!("cloned:{}", (&*instance).value), + }; + ComplexClass { + value: value_str, + number, + } + } + + #[napi] + pub fn method_one(&self) -> String { + format!("method_one: {}", self.value) + } + + #[napi] + pub fn method_two(&self) -> i32 { + self.number * 2 + } + + #[napi] + pub fn method_three(&self) -> String { + format!("method_three: {} - {}", self.value, self.number) + } + + #[napi] + pub fn method_four(&self) -> bool { + self.number > 0 + } + + #[napi] + pub fn method_five(&self) -> String { + self.value.to_uppercase() + } +} diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.md b/examples/napi/__tests__/__snapshots__/values.spec.ts.md index 249eb8a9..7328dfae 100644 --- a/examples/napi/__tests__/__snapshots__/values.spec.ts.md +++ b/examples/napi/__tests__/__snapshots__/values.spec.ts.md @@ -1050,6 +1050,17 @@ Generated by [AVA](https://avajs.dev). export function xxh128(input: Buffer): bigint␊ export function xxh3_64(input: Buffer): bigint␊ }␊ + export declare class ComplexClass {␊ + value: string␊ + number: number␊ + constructor(value: string | ComplexClass, number: number)␊ + methodOne(): string␊ + methodTwo(): number␊ + methodThree(): string␊ + methodFour(): boolean␊ + methodFive(): string␊ + }␊ + ␊ export interface Shared {␊ value: number␊ }␊ diff --git a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap index b79a6336..d5127807 100644 Binary files a/examples/napi/__tests__/__snapshots__/values.spec.ts.snap and b/examples/napi/__tests__/__snapshots__/values.spec.ts.snap differ diff --git a/examples/napi/__tests__/values.spec.ts b/examples/napi/__tests__/values.spec.ts index ccae19ca..68d97510 100644 --- a/examples/napi/__tests__/values.spec.ts +++ b/examples/napi/__tests__/values.spec.ts @@ -243,6 +243,7 @@ import { JSOnlyMethodsClass, RustOnlyMethodsClass, OriginalRustNameForJsNamedStruct, + ComplexClass, } from '../index.cjs' // import other stuff in `#[napi(module_exports)]` import nativeAddon from '../index.cjs' @@ -1841,3 +1842,45 @@ test('escapable handle scope', (t) => { shorterEscapableScope(makeIterFunction()) }) }) + +test('complex class with multiple methods - issue #2722', (t) => { + // Test creating instance of re-exported class with constructor (Either>) + t.notThrows(() => { + const complex = new ComplexClass('test_value', 42) + + // Test that constructor worked + t.is(complex.value, 'test_value') + t.is(complex.number, 42) + + // Test all methods work + t.is(complex.methodOne(), 'method_one: test_value') + t.is(complex.methodTwo(), 84) + t.is(complex.methodThree(), 'method_three: test_value - 42') + t.is(complex.methodFour(), true) + t.is(complex.methodFive(), 'TEST_VALUE') + }) + + // Test with Either::B variant (ClassInstance instead of string) + t.notThrows(() => { + const original = new ComplexClass('original', 100) + const complex2 = new ComplexClass(original, -10) + t.is(complex2.value, 'cloned:original') // Should clone the value + t.is(complex2.methodFour(), false) + }) + + // Test that we can create multiple instances (stress test with Either) + t.notThrows(() => { + const baseInstance = new ComplexClass('base', 999) + for (let i = 0; i < 10; i++) { + // Alternate between string and ClassInstance for Either parameter + const instance = + i % 2 === 0 + ? new ComplexClass(`test${i}`, i) + : new ComplexClass(baseInstance, i) + + const expectedValue = i % 2 === 0 ? `test${i}` : 'cloned:base' + t.is(instance.value, expectedValue) + t.is(instance.number, i) + } + }) +}) diff --git a/examples/napi/example.wasi-browser.js b/examples/napi/example.wasi-browser.js index 034e4c29..94fcfaee 100644 --- a/examples/napi/example.wasi-browser.js +++ b/examples/napi/example.wasi-browser.js @@ -365,3 +365,4 @@ export const withoutAbortController = __napiModule.exports.withoutAbortControlle export const xxh64Alias = __napiModule.exports.xxh64Alias export const xxh2 = __napiModule.exports.xxh2 export const xxh3 = __napiModule.exports.xxh3 +export const ComplexClass = __napiModule.exports.ComplexClass diff --git a/examples/napi/example.wasi.cjs b/examples/napi/example.wasi.cjs index b4f0b65f..17277f68 100644 --- a/examples/napi/example.wasi.cjs +++ b/examples/napi/example.wasi.cjs @@ -389,3 +389,4 @@ module.exports.withoutAbortController = __napiModule.exports.withoutAbortControl module.exports.xxh64Alias = __napiModule.exports.xxh64Alias module.exports.xxh2 = __napiModule.exports.xxh2 module.exports.xxh3 = __napiModule.exports.xxh3 +module.exports.ComplexClass = __napiModule.exports.ComplexClass diff --git a/examples/napi/index.cjs b/examples/napi/index.cjs index 0d35d555..f256a085 100644 --- a/examples/napi/index.cjs +++ b/examples/napi/index.cjs @@ -680,3 +680,4 @@ module.exports.withoutAbortController = nativeBinding.withoutAbortController module.exports.xxh64Alias = nativeBinding.xxh64Alias module.exports.xxh2 = nativeBinding.xxh2 module.exports.xxh3 = nativeBinding.xxh3 +module.exports.ComplexClass = nativeBinding.ComplexClass diff --git a/examples/napi/index.d.cts b/examples/napi/index.d.cts index 8d05b8d1..650c0a73 100644 --- a/examples/napi/index.d.cts +++ b/examples/napi/index.d.cts @@ -1012,6 +1012,17 @@ export declare namespace xxh3 { export function xxh128(input: Buffer): bigint export function xxh3_64(input: Buffer): bigint } +export declare class ComplexClass { + value: string + number: number + constructor(value: string | ComplexClass, number: number) + methodOne(): string + methodTwo(): number + methodThree(): string + methodFour(): boolean + methodFive(): string +} + export interface Shared { value: number } diff --git a/examples/napi/src/lib.rs b/examples/napi/src/lib.rs index 9ce5cad7..b18a795b 100644 --- a/examples/napi/src/lib.rs +++ b/examples/napi/src/lib.rs @@ -8,6 +8,7 @@ #[cfg(not(target_family = "wasm"))] use napi::bindgen_prelude::create_custom_tokio_runtime; use napi::bindgen_prelude::{JsObjectValue, Object, Result, Symbol}; +pub use napi_shared::*; #[macro_use] extern crate napi_derive;