mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary - Fixes #22596 where Nuxt crashes when building with rolldown-vite - Aligns Bun's NAPI GC safety checks with Node.js behavior by only enforcing them for experimental NAPI modules ## The Problem Bun was incorrectly enforcing GC safety checks (`NAPI_CHECK_ENV_NOT_IN_GC`) for ALL NAPI modules, regardless of version. This caused crashes when regular production NAPI modules called `napi_reference_unref` from finalizers, which is a common pattern in the ecosystem (e.g., rolldown-vite). The crash manifested as: ``` panic: Aborted - napi.h:306: napi_reference_unref ``` ## Root Cause: What We Did Wrong Our previous implementation always enforced the GC check for all NAPI modules: **Before (incorrect):** ```cpp // src/bun.js/bindings/napi.h:304-311 void checkGC() const { NAPI_RELEASE_ASSERT(!inGC(), "Attempted to call a non-GC-safe function inside a NAPI finalizer..."); // This was called for ALL modules, not just experimental ones } ``` This was overly restrictive and didn't match Node.js's behavior, causing legitimate use cases to crash. ## The Correct Solution: How Node.js Does It After investigating Node.js source code, we found that Node.js **only enforces GC safety checks for experimental NAPI modules**. Regular production modules are allowed to call functions like `napi_reference_unref` from finalizers for backward compatibility. ### Evidence from Node.js Source Code **1. The CheckGCAccess implementation** (`vendor/node/src/js_native_api_v8.h:132-143`): ```cpp void CheckGCAccess() { if (module_api_version == NAPI_VERSION_EXPERIMENTAL && in_gc_finalizer) { // Only fails if BOTH conditions are true: // 1. Module is experimental (version 2147483647) // 2. Currently in GC finalizer v8impl::OnFatalError(...); } } ``` **2. NAPI_VERSION_EXPERIMENTAL definition** (`vendor/node/src/js_native_api.h:9`): ```cpp #define NAPI_VERSION_EXPERIMENTAL 2147483647 // INT_MAX ``` **3. How it's used in napi_reference_unref** (`vendor/node/src/js_native_api_v8.cc:2814-2819`): ```cpp napi_status NAPI_CDECL napi_reference_unref(napi_env env, napi_ref ref, uint32_t* result) { CHECK_ENV_NOT_IN_GC(env); // This check only fails for experimental modules // ... rest of implementation } ``` ## Our Fix: Match Node.js Behavior Exactly **After (correct):** ```cpp // src/bun.js/bindings/napi.h:304-315 void checkGC() const { // Only enforce GC checks for experimental NAPI versions, matching Node.js behavior // See: https://github.com/nodejs/node/blob/main/src/js_native_api_v8.h#L132-L143 if (m_napiModule.nm_version == NAPI_VERSION_EXPERIMENTAL) { NAPI_RELEASE_ASSERT(!inGC(), ...); } // Regular modules (version <= 8) can call napi_reference_unref from finalizers } ``` This change means: - **Regular NAPI modules** (version 8 and below): ✅ Can call `napi_reference_unref` from finalizers - **Experimental NAPI modules** (version 2147483647): ❌ Cannot call `napi_reference_unref` from finalizers ## Why This Matters Many existing NAPI modules in the ecosystem were written before the stricter GC rules and rely on being able to call functions like `napi_reference_unref` from finalizers. Node.js maintains backward compatibility by only enforcing the stricter rules for modules that explicitly opt into experimental features. By not matching this behavior, Bun was breaking existing packages that work fine in Node.js. ## Test Plan Added comprehensive tests that verify both scenarios: ### 1. test_reference_unref_in_finalizer.c (Regular Module) - Uses default NAPI version (8) - Creates 100 objects with finalizers that call `napi_reference_unref` - **Expected:** Works without crashing - **Result:** ✅ Passes with both Node.js and Bun (with fix) ### 2. test_reference_unref_in_finalizer_experimental.c (Experimental Module) - Uses `NAPI_VERSION_EXPERIMENTAL` (2147483647) - Creates objects with finalizers that call `napi_reference_unref` - **Expected:** Crashes with GC safety assertion - **Result:** ✅ Correctly fails with both Node.js and Bun (with fix) ## Verification The tests prove our fix is correct: ```bash # Regular module - should work $ bun-debug --expose-gc main.js test_reference_unref_in_finalizer '[]' ✅ SUCCESS: napi_reference_unref worked in finalizers without crashing # Experimental module - should fail $ bun-debug --expose-gc main.js test_reference_unref_in_finalizer_experimental '[]' ✅ ASSERTION FAILED: Attempted to call a non-GC-safe function inside a NAPI finalizer ``` Both behaviors now match Node.js exactly. ## Impact This fix: 1. Resolves crashes with rolldown-vite and similar packages 2. Maintains backward compatibility with the Node.js ecosystem 3. Still enforces safety for experimental NAPI features 4. Aligns Bun's behavior with Node.js's intentional design decisions 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Zack Radisic <zack@theradisic.com>
845 lines
26 KiB
JavaScript
845 lines
26 KiB
JavaScript
const assert = require("node:assert");
|
|
const nativeTests = require("./build/Debug/napitests.node");
|
|
const secondAddon = require("./build/Debug/second_addon.node");
|
|
const asyncFinalizeAddon = require("./build/Debug/async_finalize_addon.node");
|
|
const testReferenceUnrefInFinalizer = require("./build/Debug/test_reference_unref_in_finalizer.node");
|
|
const testReferenceUnrefInFinalizerExperimental = require("./build/Debug/test_reference_unref_in_finalizer_experimental.node");
|
|
|
|
async function gcUntil(fn) {
|
|
const MAX = 100;
|
|
for (let i = 0; i < MAX; i++) {
|
|
await new Promise(resolve => {
|
|
setTimeout(resolve, 1);
|
|
});
|
|
if (typeof Bun == "object") {
|
|
Bun.gc(true);
|
|
} else {
|
|
// if this fails, you need to pass --expose-gc to node
|
|
global.gc();
|
|
}
|
|
if (fn()) {
|
|
return;
|
|
}
|
|
}
|
|
throw new Error(`Condition was not met after ${MAX} GC attempts`);
|
|
}
|
|
|
|
nativeTests.test_napi_class_constructor_handle_scope = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
const x = new NapiClass();
|
|
console.log("x.foo =", x.foo);
|
|
};
|
|
|
|
nativeTests.test_napi_handle_scope_finalizer = async () => {
|
|
// Create a weak reference, which will be collected eventually
|
|
// Pass false in Node.js so it does not create a handle scope
|
|
nativeTests.create_ref_with_finalizer(Boolean(process.isBun));
|
|
|
|
// Wait until it actually has been collected by ticking the event loop and forcing GC
|
|
await gcUntil(() => nativeTests.was_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_execute_null_check = () => {
|
|
const res = nativeTests.create_async_work_with_null_execute();
|
|
if (res) {
|
|
console.log("success!");
|
|
} else {
|
|
console.log("failure!");
|
|
}
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_complete_null_check = async () => {
|
|
nativeTests.create_async_work_with_null_complete();
|
|
await gcUntil(() => true);
|
|
};
|
|
|
|
nativeTests.test_napi_async_work_cancel = () => {
|
|
// UV_THREADPOOL_SIZE is set to 2, create two blocking tasks,
|
|
// then create another and cancel it, ensuring the work is not
|
|
// scheduled before `napi_cancel_async_work` is called
|
|
const res = nativeTests.test_cancel_async_work(result => {
|
|
if (result) {
|
|
console.log("success!");
|
|
} else {
|
|
console.log("failure!");
|
|
}
|
|
});
|
|
|
|
if (!res) {
|
|
console.log("failure!");
|
|
}
|
|
};
|
|
|
|
nativeTests.test_promise_with_threadsafe_function = async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 1));
|
|
// create_promise_with_threadsafe_function returns a promise that calls our function from another
|
|
// thread (via napi_threadsafe_function) and resolves with its return value
|
|
return await nativeTests.create_promise_with_threadsafe_function(() => 1234);
|
|
};
|
|
|
|
nativeTests.test_get_exception = (_, value) => {
|
|
function thrower() {
|
|
throw value;
|
|
}
|
|
try {
|
|
const result = nativeTests.call_and_get_exception(thrower);
|
|
console.log("got same exception back?", result === value);
|
|
} catch (e) {
|
|
console.log("native module threw", typeof e, e);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
nativeTests.test_get_property = () => {
|
|
const objects = [
|
|
{},
|
|
{ foo: "bar" },
|
|
{
|
|
get foo() {
|
|
throw new Error("get foo");
|
|
},
|
|
},
|
|
{
|
|
set foo(newValue) {},
|
|
},
|
|
new Proxy(
|
|
{},
|
|
{
|
|
get(_target, key) {
|
|
throw new Error(`proxy get ${key}`);
|
|
},
|
|
},
|
|
),
|
|
5,
|
|
"hello",
|
|
null,
|
|
undefined,
|
|
];
|
|
const keys = [
|
|
"foo",
|
|
{
|
|
toString() {
|
|
throw new Error("toString");
|
|
},
|
|
},
|
|
{
|
|
[Symbol.toPrimitive]() {
|
|
throw new Error("Symbol.toPrimitive");
|
|
},
|
|
},
|
|
"toString",
|
|
"slice",
|
|
];
|
|
|
|
for (const object of objects) {
|
|
for (const key of keys) {
|
|
try {
|
|
const ret = nativeTests.perform_get(object, key);
|
|
console.log("native function returned", ret);
|
|
} catch (e) {
|
|
console.log("threw", e.name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_set_property = () => {
|
|
const objects = [
|
|
{},
|
|
{ foo: "bar" },
|
|
{
|
|
set foo(value) {
|
|
throw new Error(`set foo to ${value}`);
|
|
},
|
|
},
|
|
{
|
|
// getter but no setter
|
|
get foo() {},
|
|
},
|
|
new Proxy(
|
|
{},
|
|
{
|
|
set(_target, key, value) {
|
|
throw new Error(`proxy set ${key} to ${value}`);
|
|
},
|
|
},
|
|
),
|
|
null,
|
|
undefined,
|
|
];
|
|
const keys = [
|
|
"foo",
|
|
{
|
|
toString() {
|
|
throw new Error("toString");
|
|
},
|
|
},
|
|
{
|
|
[Symbol.toPrimitive]() {
|
|
throw new Error("Symbol.toPrimitive");
|
|
},
|
|
},
|
|
];
|
|
|
|
for (const object of objects) {
|
|
for (const key of keys) {
|
|
console.log(objects.indexOf(object) + ", " + keys.indexOf(key));
|
|
try {
|
|
const ret = nativeTests.perform_set(object, key, 42);
|
|
console.log("native function returned", ret);
|
|
if (object[key] != 42) {
|
|
throw new Error("setting property did not throw an error, but the property was not actually set");
|
|
}
|
|
} catch (e) {
|
|
console.log("threw", e.name);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_number_integer_conversions_from_js = () => {
|
|
const i32 = { min: -(2 ** 31), max: 2 ** 31 - 1 };
|
|
const u32Max = 2 ** 32 - 1;
|
|
// this is not the actual max value for i64, but rather the highest double that is below the true max value
|
|
const i64 = { min: -(2 ** 63), max: 2 ** 63 - 1024 };
|
|
|
|
const i32Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
[-1.0, -1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, -1],
|
|
// limits
|
|
[i32.min, i32.min],
|
|
[i32.max, i32.max],
|
|
// wrap around
|
|
[i32.min - 1.0, i32.max],
|
|
[i32.max + 1.0, i32.min],
|
|
[i32.min - 2.0, i32.max - 1],
|
|
[i32.max + 2.0, i32.min + 1],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of i32Cases) {
|
|
const actualOutput = nativeTests.double_to_i32(input);
|
|
console.log(`${input} as i32 => ${actualOutput}`);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
|
|
const u32Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, u32Max],
|
|
// limits
|
|
[u32Max, u32Max],
|
|
// wrap around
|
|
[-1.0, u32Max],
|
|
[u32Max + 1.0, 0],
|
|
[-2.0, u32Max - 1],
|
|
[u32Max + 2.0, 1],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of u32Cases) {
|
|
const actualOutput = nativeTests.double_to_u32(input);
|
|
console.log(`${input} as u32 => ${actualOutput}`);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
|
|
const i64Cases = [
|
|
// special values
|
|
[Infinity, 0],
|
|
[-Infinity, 0],
|
|
[NaN, 0],
|
|
// normal
|
|
[0.0, 0],
|
|
[1.0, 1],
|
|
[-1.0, -1],
|
|
// truncation
|
|
[1.25, 1],
|
|
[-1.25, -1],
|
|
// limits
|
|
[i64.min, i64.min],
|
|
[i64.max, i64.max],
|
|
// clamp
|
|
[i64.min - 4096.0, i64.min],
|
|
// this one clamps to the exact max value of i64 (2**63 - 1), which is then rounded
|
|
// to exactly 2**63 since that's the closest double that can be represented
|
|
[i64.max + 4096.0, 2 ** 63],
|
|
// type errors
|
|
["5", undefined],
|
|
[new Number(5), undefined],
|
|
];
|
|
|
|
for (const [input, expectedOutput] of i64Cases) {
|
|
const actualOutput = nativeTests.double_to_i64(input);
|
|
console.log(
|
|
`${typeof input == "number" ? input.toFixed(2) : input} as i64 => ${typeof actualOutput == "number" ? actualOutput.toFixed(2) : actualOutput}`,
|
|
);
|
|
assert(actualOutput === expectedOutput);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_create_array_with_length = () => {
|
|
for (const size of [0, 5]) {
|
|
const array = nativeTests.make_empty_array(size);
|
|
console.log("length =", array.length);
|
|
// should be 0 as array contains empty slots
|
|
console.log("number of keys =", Object.keys(array).length);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_throw_functions_exhaustive = () => {
|
|
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
|
|
for (const code of [undefined, "", "error code"]) {
|
|
for (const msg of [undefined, "", "error message"]) {
|
|
try {
|
|
nativeTests.throw_error(code, msg, errorKind);
|
|
console.log(`napi_throw_${errorKind}(${code ?? "nullptr"}, ${msg ?? "nullptr"}) did not throw`);
|
|
} catch (e) {
|
|
console.log(
|
|
`napi_throw_${errorKind} threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_create_error_functions_exhaustive = () => {
|
|
for (const errorKind of ["error", "type_error", "range_error", "syntax_error"]) {
|
|
// null (JavaScript null) is changed to nullptr by the native function
|
|
for (const code of [undefined, null, "", 42, "error code"]) {
|
|
for (const msg of [undefined, null, "", 42, "error message"]) {
|
|
try {
|
|
nativeTests.create_and_throw_error(code, msg, errorKind);
|
|
console.log(
|
|
`napi_create_${errorKind}(${code === null ? "nullptr" : code}, ${msg === null ? "nullptr" : msg}) did not make an error`,
|
|
);
|
|
} catch (e) {
|
|
console.log(
|
|
`create_and_throw_error(${errorKind}) threw ${e.name}: message ${JSON.stringify(e.message)}, code ${JSON.stringify(e.code)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_type_tag = () => {
|
|
const o1 = {};
|
|
const o2 = {};
|
|
|
|
nativeTests.add_tag(o1, 1, 2);
|
|
|
|
try {
|
|
// re-tag
|
|
nativeTests.add_tag(o1, 1, 2);
|
|
} catch (e) {
|
|
console.log("tagging already-tagged object threw", e.toString());
|
|
}
|
|
|
|
console.log("tagging non-object succeeds: ", !nativeTests.try_add_tag(null, 0, 0));
|
|
|
|
nativeTests.add_tag(o2, 3, 4);
|
|
console.log("o1 matches o1:", nativeTests.check_tag(o1, 1, 2));
|
|
console.log("o1 matches o2:", nativeTests.check_tag(o1, 3, 4));
|
|
console.log("o2 matches o1:", nativeTests.check_tag(o2, 1, 2));
|
|
console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4));
|
|
};
|
|
|
|
nativeTests.test_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
const instance = new NapiClass();
|
|
console.log("static data =", NapiClass.getStaticData());
|
|
console.log("static getter =", NapiClass.getter);
|
|
console.log("foo =", instance.foo);
|
|
console.log("data =", instance.getData());
|
|
};
|
|
|
|
nativeTests.test_subclass_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
class Subclass extends NapiClass {}
|
|
const instance = new Subclass();
|
|
console.log("subclass static data =", Subclass.getStaticData());
|
|
console.log("subclass static getter =", Subclass.getter);
|
|
console.log("subclass foo =", instance.foo);
|
|
console.log("subclass data =", instance.getData());
|
|
};
|
|
|
|
nativeTests.test_napi_class_non_constructor_call = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
console.log("non-constructor call NapiClass() =", NapiClass());
|
|
console.log("global foo set to ", typeof foo != "undefined" ? foo : undefined);
|
|
};
|
|
|
|
nativeTests.test_reflect_construct_napi_class = () => {
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
let instance = Reflect.construct(NapiClass, [], Object);
|
|
console.log("reflect constructed foo =", instance.foo);
|
|
console.log("reflect constructed data =", instance.getData?.());
|
|
class Foo {}
|
|
instance = Reflect.construct(NapiClass, [], Foo);
|
|
console.log("reflect constructed foo =", instance.foo);
|
|
console.log("reflect constructed data =", instance.getData?.());
|
|
};
|
|
|
|
nativeTests.test_reflect_construct_no_prototype_crash = () => {
|
|
// This test verifies the fix for jsDynamicCast being called on JSValue(0)
|
|
// when a NAPI class constructor is called via Reflect.construct with a
|
|
// newTarget that has no prototype property.
|
|
|
|
const NapiClass = nativeTests.get_class_with_constructor();
|
|
|
|
// Test 1: Constructor function with deleted prototype property
|
|
// This case should work without crashing
|
|
function ConstructorWithoutPrototype() {}
|
|
delete ConstructorWithoutPrototype.prototype;
|
|
|
|
try {
|
|
const instance1 = Reflect.construct(NapiClass, [], ConstructorWithoutPrototype);
|
|
console.log("constructor without prototype: success - no crash");
|
|
} catch (e) {
|
|
console.log("constructor without prototype error:", e.message);
|
|
}
|
|
|
|
// Test 2: Regular constructor (control test)
|
|
// This should always work
|
|
function NormalConstructor() {}
|
|
|
|
try {
|
|
const instance2 = Reflect.construct(NapiClass, [], NormalConstructor);
|
|
console.log("normal constructor: success - no crash");
|
|
} catch (e) {
|
|
console.log("normal constructor error:", e.message);
|
|
}
|
|
|
|
// Test 3: Reflect.construct with Proxy newTarget (prototype returns undefined)
|
|
function ProxyObject() {}
|
|
|
|
const proxyTarget = new Proxy(ProxyObject, {
|
|
get(target, prop) {
|
|
if (prop === "prototype") {
|
|
return undefined;
|
|
}
|
|
return target[prop];
|
|
},
|
|
});
|
|
const instance3 = Reflect.construct(NapiClass, [], proxyTarget);
|
|
console.log("✓ Success - no crash!");
|
|
};
|
|
|
|
nativeTests.test_napi_wrap = () => {
|
|
const values = [
|
|
{},
|
|
{}, // should be able to be wrapped differently than the distinct empty object above
|
|
5,
|
|
new Number(5),
|
|
"abc",
|
|
new String("abc"),
|
|
null,
|
|
Symbol("abc"),
|
|
Symbol.for("abc"),
|
|
new (nativeTests.get_class_with_constructor())(),
|
|
new Proxy(
|
|
{},
|
|
Object.fromEntries(
|
|
[
|
|
"apply",
|
|
"construct",
|
|
"defineProperty",
|
|
"deleteProperty",
|
|
"get",
|
|
"getOwnPropertyDescriptor",
|
|
"getPrototypeOf",
|
|
"has",
|
|
"isExtensible",
|
|
"ownKeys",
|
|
"preventExtensions",
|
|
"set",
|
|
"setPrototypeOf",
|
|
].map(name => [
|
|
name,
|
|
() => {
|
|
throw new Error("oops");
|
|
},
|
|
]),
|
|
),
|
|
),
|
|
];
|
|
const wrapSuccess = Array(values.length).fill(false);
|
|
for (const [i, v] of values.entries()) {
|
|
wrapSuccess[i] = nativeTests.try_wrap(v, i + 1);
|
|
console.log(`${typeof v} did wrap: `, wrapSuccess[i]);
|
|
}
|
|
|
|
for (const [i, v] of values.entries()) {
|
|
if (wrapSuccess[i]) {
|
|
if (nativeTests.try_unwrap(v) !== i + 1) {
|
|
throw new Error("could not unwrap same value");
|
|
}
|
|
} else {
|
|
if (nativeTests.try_unwrap(v) !== undefined) {
|
|
throw new Error("value unwraps without being successfully wrapped");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_proxy = () => {
|
|
const target = {};
|
|
const proxy = new Proxy(target, {});
|
|
assert(nativeTests.try_wrap(target, 5));
|
|
assert(nativeTests.try_wrap(proxy, 6));
|
|
console.log(nativeTests.try_unwrap(target), nativeTests.try_unwrap(proxy));
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_cross_addon = () => {
|
|
const wrapped = {};
|
|
console.log("wrap succeeds:", nativeTests.try_wrap(wrapped, 42));
|
|
console.log("unwrapped from other addon", secondAddon.try_unwrap(wrapped));
|
|
};
|
|
|
|
nativeTests.test_napi_wrap_prototype = () => {
|
|
class Foo {}
|
|
console.log("wrap prototype succeeds:", nativeTests.try_wrap(Foo.prototype, 42));
|
|
// wrapping should not look at prototype chain
|
|
console.log("unwrap instance:", nativeTests.try_unwrap(new Foo()));
|
|
};
|
|
|
|
nativeTests.test_napi_remove_wrap = () => {
|
|
const targets = [{}, new (nativeTests.get_class_with_constructor())()];
|
|
for (const t of targets) {
|
|
const target = {};
|
|
// fails
|
|
assert(nativeTests.try_remove_wrap(target) === undefined);
|
|
// wrap it
|
|
assert(nativeTests.try_wrap(target, 5));
|
|
// remove yields the wrapped value
|
|
assert(nativeTests.try_remove_wrap(target) === 5);
|
|
// neither remove nor unwrap work anymore
|
|
assert(nativeTests.try_unwrap(target) === undefined);
|
|
assert(nativeTests.try_remove_wrap(target) === undefined);
|
|
// can re-wrap
|
|
assert(nativeTests.try_wrap(target, 6));
|
|
assert(nativeTests.try_unwrap(target) === 6);
|
|
}
|
|
};
|
|
|
|
// parameters to create_wrap are: object, ask_for_ref, strong
|
|
const createWrapWithoutRef = o => nativeTests.create_wrap(o, false, false);
|
|
const createWrapWithWeakRef = o => nativeTests.create_wrap(o, true, false);
|
|
const createWrapWithStrongRef = o => nativeTests.create_wrap(o, true, true);
|
|
|
|
nativeTests.test_wrap_lifetime_without_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithoutRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
object = undefined;
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_wrap_lifetime_with_weak_ref = async () => {
|
|
// this looks the same as test_wrap_lifetime_without_ref because it is -- these cases should behave the same
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
object = undefined;
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_wrap_lifetime_with_strong_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithStrongRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
object = undefined;
|
|
// still referenced by native module so this should fail
|
|
try {
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
throw new Error("object was garbage collected while still referenced by native code");
|
|
} catch (e) {
|
|
if (!e.toString().includes("Condition was not met")) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// can still get the value using the ref
|
|
assert(nativeTests.get_wrap_data_from_ref() === 42);
|
|
|
|
// now we free it
|
|
nativeTests.unref_wrapped_value();
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called());
|
|
};
|
|
|
|
nativeTests.test_remove_wrap_lifetime_with_weak_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
nativeTests.remove_wrap(object);
|
|
assert(nativeTests.get_wrap_data(object) === undefined);
|
|
assert(nativeTests.get_wrap_data_from_ref() === undefined);
|
|
assert(nativeTests.get_object_from_ref() === object);
|
|
|
|
object = undefined;
|
|
|
|
// ref will stop working once the object is collected
|
|
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
|
|
|
|
// finalizer shouldn't have been called
|
|
assert(nativeTests.was_wrap_finalize_called() === false);
|
|
};
|
|
|
|
nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithStrongRef(object) === object);
|
|
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
|
|
nativeTests.remove_wrap(object);
|
|
assert(nativeTests.get_wrap_data(object) === undefined);
|
|
assert(nativeTests.get_wrap_data_from_ref() === undefined);
|
|
assert(nativeTests.get_object_from_ref() === object);
|
|
|
|
object = undefined;
|
|
|
|
// finalizer should not be called and object should not be freed
|
|
try {
|
|
await gcUntil(() => nativeTests.was_wrap_finalize_called() || nativeTests.get_object_from_ref() === undefined);
|
|
throw new Error("finalizer ran");
|
|
} catch (e) {
|
|
if (!e.toString().includes("Condition was not met")) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// native code can still get the object
|
|
assert(JSON.stringify(nativeTests.get_object_from_ref()) === `{"foo":"bar"}`);
|
|
|
|
// now it gets deleted
|
|
nativeTests.unref_wrapped_value();
|
|
await gcUntil(() => nativeTests.get_object_from_ref() === undefined);
|
|
};
|
|
|
|
nativeTests.test_ref_deleted_in_cleanup = () => {
|
|
let object = { foo: "bar" };
|
|
assert(createWrapWithWeakRef(object) === object);
|
|
assert(nativeTests.get_wrap_data(object) === 42);
|
|
};
|
|
|
|
nativeTests.test_ref_deleted_in_async_finalize = () => {
|
|
asyncFinalizeAddon.create_ref();
|
|
};
|
|
|
|
nativeTests.test_reference_unref_in_finalizer = async (gc) => {
|
|
// Create objects with finalizers that will call napi_reference_unref when GC'd
|
|
let objects = testReferenceUnrefInFinalizer.test_reference_unref_in_finalizer();
|
|
|
|
// Clear the reference to allow GC
|
|
objects = null;
|
|
|
|
// Force GC multiple times to ensure finalizers run
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Allocate large ArrayBuffers to trigger GC pressure
|
|
const buffers = [];
|
|
for (let i = 0; i < 100; i++) {
|
|
buffers.push(new ArrayBuffer(10 * 1024 * 1024)); // 10MB each
|
|
if (gc && i % 10 === 0) {
|
|
gc();
|
|
}
|
|
}
|
|
|
|
// Wait for async operations
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
|
// Force final GC
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Get stats to verify finalizers were called
|
|
const stats = testReferenceUnrefInFinalizer.get_stats();
|
|
console.log(`Finalizers called: ${stats.finalizersCalled}, Unrefs succeeded: ${stats.unrefsSucceeded}`);
|
|
|
|
if (stats.finalizersCalled === 0) {
|
|
throw new Error("No finalizers were called - test did not properly trigger GC");
|
|
}
|
|
|
|
if (stats.unrefsSucceeded === 0) {
|
|
throw new Error("No napi_reference_unref calls succeeded");
|
|
}
|
|
|
|
console.log("SUCCESS: napi_reference_unref worked in finalizers without crashing");
|
|
};
|
|
|
|
nativeTests.test_reference_unref_in_finalizer_experimental = async (gc) => {
|
|
// This test is expected to CRASH when the finalizer runs
|
|
// The experimental NAPI module enforces GC checks and will abort the process
|
|
console.log("WARNING: This test will crash the process - this is expected behavior!");
|
|
|
|
// Create objects with finalizers that will call napi_reference_unref when GC'd
|
|
let objects = testReferenceUnrefInFinalizerExperimental.test_reference_unref_in_finalizer_experimental();
|
|
|
|
// Clear the reference to allow GC
|
|
objects = null;
|
|
|
|
// Force GC to trigger the finalizers - this should crash the process
|
|
if (gc) {
|
|
gc();
|
|
gc();
|
|
}
|
|
|
|
// Allocate memory to ensure GC runs
|
|
for (let i = 0; i < 5; i++) {
|
|
new ArrayBuffer(10 * 1024 * 1024);
|
|
if (gc) gc();
|
|
}
|
|
|
|
// If we get here, the test has FAILED - the process should have crashed
|
|
console.log("ERROR: Process did not crash as expected!");
|
|
console.log("ERROR: The GC check for experimental modules is NOT working!");
|
|
throw new Error("Test FAILED: napi_reference_unref should have aborted for experimental module");
|
|
};
|
|
|
|
nativeTests.test_create_bigint_words = () => {
|
|
console.log(nativeTests.create_weird_bigints());
|
|
};
|
|
|
|
nativeTests.test_bigint_word_count = () => {
|
|
// Test with a 2-word BigInt
|
|
const bigint = 0x123456789ABCDEF0123456789ABCDEFn;
|
|
const result = nativeTests.test_bigint_actual_word_count(bigint);
|
|
|
|
console.log(`BigInt: ${bigint.toString(16)}`);
|
|
console.log(`Queried word count: ${result.queriedWordCount}`);
|
|
console.log(`Actual word count: ${result.actualWordCount}`);
|
|
console.log(`Sign bit: ${result.signBit}`);
|
|
|
|
// Both counts should be 2 for this BigInt
|
|
if (result.queriedWordCount === 2 && result.actualWordCount === 2) {
|
|
console.log("✅ PASS: Word count correctly returns 2");
|
|
} else {
|
|
console.log(`❌ FAIL: Expected word count 2, got queried=${result.queriedWordCount}, actual=${result.actualWordCount}`);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_ref_unref_underflow = () => {
|
|
// Test that napi_reference_unref properly handles refCount == 0
|
|
const obj = { test: "value" };
|
|
const result = nativeTests.test_reference_unref_underflow(obj);
|
|
|
|
console.log(`First unref count: ${result.firstUnrefCount}`);
|
|
console.log(`Second unref status: ${result.secondUnrefStatus}`);
|
|
|
|
// First unref should succeed and return count of 0
|
|
// Second unref should fail with napi_generic_failure (status = 1)
|
|
if (result.firstUnrefCount === 0 && result.secondUnrefStatus === 1) {
|
|
console.log("✅ PASS: Reference unref correctly prevents underflow");
|
|
} else {
|
|
console.log(`❌ FAIL: Expected firstUnrefCount=0, secondUnrefStatus=1, got ${result.firstUnrefCount}, ${result.secondUnrefStatus}`);
|
|
}
|
|
};
|
|
|
|
nativeTests.test_get_value_string = () => {
|
|
function to16Bit(string) {
|
|
if (typeof Bun != "object") return string;
|
|
const jsc = require("bun:jsc");
|
|
const codeUnits = new DataView(new ArrayBuffer(2 * string.length));
|
|
for (let i = 0; i < string.length; i++) {
|
|
codeUnits.setUint16(2 * i, string.charCodeAt(i), true);
|
|
}
|
|
const decoder = new TextDecoder("utf-16le");
|
|
const string16Bit = decoder.decode(codeUnits);
|
|
// make sure we succeeded in making a UTF-16 string
|
|
assert(jsc.jscDescribe(string16Bit).includes("8Bit:(0)"));
|
|
return string16Bit;
|
|
}
|
|
function assert8Bit(string) {
|
|
if (typeof Bun != "object") return string;
|
|
const jsc = require("bun:jsc");
|
|
// make sure we succeeded in making a Latin-1 string
|
|
assert(jsc.jscDescribe(string).includes("8Bit:(1)"));
|
|
return string;
|
|
}
|
|
// test all of our get_value_string_XXX functions on a variety of inputs
|
|
for (const [string, description] of [
|
|
["hello", "simple latin-1"],
|
|
[to16Bit("hello"), "16-bit encoded with only BMP characters"],
|
|
[assert8Bit("café"), "8-bit with non-ascii characters"],
|
|
[to16Bit("café"), "16-bit with non-ascii but latin-1 characters"],
|
|
["你好小圆面包", "16-bit, all BMP, all outside latin-1"],
|
|
["🐱🏳️⚧️", "16-bit with many surrogate pairs"],
|
|
// TODO(@190n) handle these correctly
|
|
// ["\ud801", "unpaired high surrogate"],
|
|
// ["\udc02", "unpaired low surrogate"],
|
|
]) {
|
|
console.log(`test napi_get_value_string on ${string} (${description})`);
|
|
for (const encoding of ["latin1", "utf8", "utf16"]) {
|
|
console.log(encoding);
|
|
const fn = nativeTests[`test_get_value_string_${encoding}`];
|
|
fn(string);
|
|
}
|
|
}
|
|
};
|
|
|
|
nativeTests.test_constructor_order = () => {
|
|
require("./build/Debug/constructor_order_addon.node");
|
|
};
|
|
|
|
// Cleanup hook tests
|
|
nativeTests.test_cleanup_hook_order = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_order.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_remove_nonexistent = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_remove_nonexistent.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_async_cleanup_hook_remove_nonexistent = () => {
|
|
const addon = require("./build/Debug/test_async_cleanup_hook_remove_nonexistent.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_duplicates = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_duplicates.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_mixed_order = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_mixed_order.node");
|
|
addon.test();
|
|
};
|
|
|
|
nativeTests.test_cleanup_hook_modification_during_iteration = () => {
|
|
const addon = require("./build/Debug/test_cleanup_hook_modification_during_iteration.node");
|
|
addon.test();
|
|
};
|
|
|
|
module.exports = nativeTests;
|