diff --git a/implementors/node/on-uncaught-exception.js b/implementors/node/on-uncaught-exception.js new file mode 100644 index 0000000..86684a5 --- /dev/null +++ b/implementors/node/on-uncaught-exception.js @@ -0,0 +1,5 @@ +const onUncaughtException = (cb) => { + process.on('uncaughtException', cb); +}; + +Object.assign(globalThis, { onUncaughtException }); diff --git a/implementors/node/tests.ts b/implementors/node/tests.ts index 50b5dfb..f0cb3cb 100644 --- a/implementors/node/tests.ts +++ b/implementors/node/tests.ts @@ -35,6 +35,12 @@ const MUST_CALL_MODULE_PATH = path.join( 'node', 'must-call.js', ); +const ON_UNCAUGHT_EXCEPTION_MODULE_PATH = path.join( + ROOT_PATH, + 'implementors', + 'node', + 'on-uncaught-exception.js', +); const SKIP_TEST_MODULE_PATH = path.join( ROOT_PATH, 'implementors', @@ -88,6 +94,8 @@ export function runFileInSubprocess( '--import', 'file://' + MUST_CALL_MODULE_PATH, '--import', + 'file://' + ON_UNCAUGHT_EXCEPTION_MODULE_PATH, + '--import', 'file://' + SKIP_TEST_MODULE_PATH, '--import', 'file://' + NAPI_VERSION_MODULE_PATH, diff --git a/tests/harness/on-uncaught-exception.js b/tests/harness/on-uncaught-exception.js new file mode 100644 index 0000000..99bc24f --- /dev/null +++ b/tests/harness/on-uncaught-exception.js @@ -0,0 +1,10 @@ +if (typeof onUncaughtException !== 'function') { + throw new Error('Expected a global onUncaughtException function'); +} + +const expected = new Error('expected uncaught'); +onUncaughtException(mustCall((err) => { + assert.strictEqual(err, expected); +})); + +throw expected; diff --git a/tests/js-native-api/test_reference/CMakeLists.txt b/tests/js-native-api/test_reference/CMakeLists.txt new file mode 100644 index 0000000..5e66738 --- /dev/null +++ b/tests/js-native-api/test_reference/CMakeLists.txt @@ -0,0 +1,2 @@ +add_node_api_cts_addon(test_reference test_reference.c) +add_node_api_cts_addon(test_finalizer test_finalizer.c) diff --git a/tests/js-native-api/test_reference/test.js b/tests/js-native-api/test_reference/test.js new file mode 100644 index 0000000..640dec0 --- /dev/null +++ b/tests/js-native-api/test_reference/test.js @@ -0,0 +1,167 @@ +'use strict'; + +const test_reference = loadAddon('test_reference'); + +// This test script uses external values with finalizer callbacks +// in order to track when values get garbage-collected. Each invocation +// of a finalizer callback increments the finalizeCount property. +assert.strictEqual(test_reference.finalizeCount, 0); + +// Run each test function in sequence, +// with an async delay and GC call between each. +async function runTests() { + (() => { + const symbol = test_reference.createSymbol('testSym'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('testSymFor')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + assert.throws( + () => test_reference.createSymbolForIncorrectLength(), + /Invalid argument/, + ); + + (() => { + const value = test_reference.createExternal(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil( + 'External value without a finalizer', + () => test_reference.finalizeCount === 0, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + assert.strictEqual(typeof value, 'object'); + test_reference.checkExternal(value); + })(); + await gcUntil( + 'External value with a finalizer', + () => test_reference.finalizeCount === 1, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 0); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should be GC'd because there is only a weak ref + await gcUntil( + 'Weak reference', + () => + test_reference.referenceValue === undefined && + test_reference.finalizeCount === 1, + ); + test_reference.deleteReference(); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + assert.strictEqual(test_reference.referenceValue, value); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil('Strong reference', () => test_reference.finalizeCount === 0); + test_reference.deleteReference(); + await gcUntil( + 'Strong reference (cont.d)', + () => test_reference.finalizeCount === 1, + ); + + (() => { + const value = test_reference.createExternalWithFinalize(); + assert.strictEqual(test_reference.finalizeCount, 0); + test_reference.createReference(value, 1); + })(); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference', + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.incrementRefcount(), 2); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-1)', + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.decrementRefcount(), 1); + // Value should NOT be GC'd because there is a strong ref + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-2)', + () => test_reference.finalizeCount === 0, + ); + assert.strictEqual(test_reference.decrementRefcount(), 0); + // Value should be GC'd because the ref is now weak! + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-3)', + () => test_reference.finalizeCount === 1, + ); + test_reference.deleteReference(); + // Value was already GC'd + await gcUntil( + 'Strong reference, increment then decrement to weak reference (cont.d-4)', + () => test_reference.finalizeCount === 1, + ); +} +runTests(); + +// This test creates a napi_ref on an object that has +// been wrapped by napi_wrap and for which the finalizer +// for the wrap calls napi_delete_ref on that napi_ref. +// +// Since both the wrap and the reference use the same +// object the finalizer for the wrap and reference +// may run in the same gc and in any order. +// +// It does that to validate that napi_delete_ref can be +// called before the finalizer has been run for the +// reference (there is a finalizer behind the scenes even +// though it cannot be passed to napi_create_reference). +// +// Since the order is not guaranteed, run the +// test a number of times maximize the chance that we +// get a run with the desired order for the test. +// +// 1000 reliably recreated the problem without the fix +// required to ensure delete could be called before +// the finalizer in manual testing. +for (let i = 0; i < 1000; i++) { + const wrapObject = new Object(); + test_reference.validateDeleteBeforeFinalize(wrapObject); + gc(); +} diff --git a/tests/js-native-api/test_reference/test_finalizer.c b/tests/js-native-api/test_reference/test_finalizer.c new file mode 100644 index 0000000..51492d9 --- /dev/null +++ b/tests/js-native-api/test_reference/test_finalizer.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; + +static void FinalizeExternalCallJs(napi_env env, void* data, void* hint) { + int* actual_value = data; + NODE_API_ASSERT_RETURN_VOID( + env, + actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + + napi_ref finalizer_ref = (napi_ref)hint; + napi_value js_finalizer; + napi_value recv; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, finalizer_ref, &js_finalizer)); + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, recv, js_finalizer, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, finalizer_ref)); +} + +static napi_value CreateExternalWithJsFinalize(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 1, "Wrong number of arguments"); + napi_value finalizer = args[0]; + napi_valuetype finalizer_valuetype; + NODE_API_CALL(env, napi_typeof(env, finalizer, &finalizer_valuetype)); + NODE_API_ASSERT(env, + finalizer_valuetype == napi_function, + "Wrong type of first argument"); + napi_ref finalizer_ref; + NODE_API_CALL(env, napi_create_reference(env, finalizer, 1, &finalizer_ref)); + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternalCallJs, + finalizer_ref, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("createExternalWithJsFinalize", + CreateExternalWithJsFinalize), + }; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/tests/js-native-api/test_reference/test_finalizer.js b/tests/js-native-api/test_reference/test_finalizer.js new file mode 100644 index 0000000..efa33c7 --- /dev/null +++ b/tests/js-native-api/test_reference/test_finalizer.js @@ -0,0 +1,23 @@ +'use strict'; +// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy + +const binding = loadAddon('test_finalizer'); + +onUncaughtException( + mustCall((err) => { + assert.throws(() => { + throw err; + }, /finalizer error/); + }), +); + +(async function() { + { + binding.createExternalWithJsFinalize( + mustCall(() => { + throw new Error('finalizer error'); + }), + ); + } + gc(); +})().then(mustCall()); diff --git a/tests/js-native-api/test_reference/test_reference.c b/tests/js-native-api/test_reference/test_reference.c new file mode 100644 index 0000000..058be07 --- /dev/null +++ b/tests/js-native-api/test_reference/test_reference.c @@ -0,0 +1,252 @@ +#define NAPI_VERSION 9 +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 1; +static int finalize_count = 0; +static napi_ref test_reference = NULL; + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, napi_create_int32(env, finalize_count, &result)); + return result; +} + +static void FinalizeExternal(napi_env env, void* data, void* hint) { + int *actual_value = data; + NODE_API_ASSERT_RETURN_VOID(env, actual_value == &test_value, + "The correct pointer was passed to the finalizer"); + finalize_count++; +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + int* data = &test_value; + + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + data, + NULL, /* finalize_cb */ + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CreateSymbol(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + napi_value result_symbol; + + NODE_API_CALL(env, napi_create_symbol(env, args[0], &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolFor(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + + char description[256]; + size_t description_length; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT( + env, argc == 1, "Expect one argument only (symbol description)"); + + NODE_API_CALL( + env, + napi_get_value_string_utf8( + env, args[0], description, sizeof(description), &description_length)); + NODE_API_ASSERT(env, + description_length <= 255, + "Cannot accommodate descriptions longer than 255 bytes"); + + napi_value result_symbol; + + NODE_API_CALL(env, + node_api_symbol_for( + env, description, description_length, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForEmptyString(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 0, &result_symbol)); + return result_symbol; +} + +static napi_value CreateSymbolForIncorrectLength(napi_env env, napi_callback_info info) { + napi_value result_symbol; + NODE_API_CALL(env, node_api_symbol_for(env, NULL, 5, &result_symbol)); + return result_symbol; +} + +static napi_value +CreateExternalWithFinalize(napi_env env, napi_callback_info info) { + napi_value result; + NODE_API_CALL(env, + napi_create_external(env, + &test_value, + FinalizeExternal, + NULL, /* finalize_hint */ + &result)); + + finalize_count = 0; + return result; +} + +static napi_value CheckExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 1, "Expected one argument."); + + napi_valuetype argtype; + NODE_API_CALL(env, napi_typeof(env, arg, &argtype)); + + NODE_API_ASSERT(env, argtype == napi_external, "Expected an external value."); + + void* data; + NODE_API_CALL(env, napi_get_value_external(env, arg, &data)); + + NODE_API_ASSERT(env, data != NULL && *(int*)data == test_value, + "An external data value of 1 was expected."); + + return NULL; +} + +static napi_value CreateReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference == NULL, + "The test allows only one reference at a time."); + + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Expected two arguments."); + + uint32_t initial_refcount; + NODE_API_CALL(env, napi_get_value_uint32(env, args[1], &initial_refcount)); + + NODE_API_CALL(env, + napi_create_reference(env, args[0], initial_refcount, &test_reference)); + + NODE_API_ASSERT(env, test_reference != NULL, + "A reference should have been created."); + + return NULL; +} + +static napi_value DeleteReference(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + NODE_API_CALL(env, napi_delete_reference(env, test_reference)); + test_reference = NULL; + return NULL; +} + +static napi_value IncrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_ref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value DecrementRefcount(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + uint32_t refcount; + NODE_API_CALL(env, napi_reference_unref(env, test_reference, &refcount)); + + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, refcount, &result)); + return result; +} + +static napi_value GetReferenceValue(napi_env env, napi_callback_info info) { + NODE_API_ASSERT(env, test_reference != NULL, + "A reference must have been created."); + + napi_value result; + NODE_API_CALL(env, napi_get_reference_value(env, test_reference, &result)); + return result; +} + +static void DeleteBeforeFinalizeFinalizer( + napi_env env, void* finalize_data, void* finalize_hint) { + napi_ref* ref = (napi_ref*)finalize_data; + napi_value value; + assert(napi_get_reference_value(env, *ref, &value) == napi_ok); + assert(value == NULL); + napi_delete_reference(env, *ref); + free(ref); +} + +static napi_value ValidateDeleteBeforeFinalize(napi_env env, napi_callback_info info) { + napi_value wrapObject; + size_t argc = 1; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &wrapObject, NULL, NULL)); + + napi_ref* ref_t = malloc(sizeof(napi_ref)); + NODE_API_CALL(env, + napi_wrap( + env, wrapObject, ref_t, DeleteBeforeFinalizeFinalizer, NULL, NULL)); + + // Create a reference that will be eligible for collection at the same + // time as the wrapped object by passing in the same wrapObject. + // This means that the FinalizeOrderValidation callback may be run + // before the finalizer for the newly created reference (there is a finalizer + // behind the scenes even though it cannot be passed to napi_create_reference) + // The Finalizer for the wrap (which is different than the finalizer + // for the reference) calls napi_delete_reference validating that + // napi_delete_reference can be called before the finalizer for the + // reference runs. + NODE_API_CALL(env, napi_create_reference(env, wrapObject, 0, ref_t)); + return wrapObject; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_GETTER("finalizeCount", GetFinalizeCount), + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createExternalWithFinalize", + CreateExternalWithFinalize), + DECLARE_NODE_API_PROPERTY("checkExternal", CheckExternal), + DECLARE_NODE_API_PROPERTY("createReference", CreateReference), + DECLARE_NODE_API_PROPERTY("createSymbol", CreateSymbol), + DECLARE_NODE_API_PROPERTY("createSymbolFor", CreateSymbolFor), + DECLARE_NODE_API_PROPERTY("createSymbolForEmptyString", + CreateSymbolForEmptyString), + DECLARE_NODE_API_PROPERTY("createSymbolForIncorrectLength", + CreateSymbolForIncorrectLength), + DECLARE_NODE_API_PROPERTY("deleteReference", DeleteReference), + DECLARE_NODE_API_PROPERTY("incrementRefcount", IncrementRefcount), + DECLARE_NODE_API_PROPERTY("decrementRefcount", DecrementRefcount), + DECLARE_NODE_API_GETTER("referenceValue", GetReferenceValue), + DECLARE_NODE_API_PROPERTY("validateDeleteBeforeFinalize", + ValidateDeleteBeforeFinalize), + }; + + NODE_API_CALL(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); + + return exports; +} +EXTERN_C_END