diff --git a/PORTING.md b/PORTING.md index 5a63cf6..74f06f6 100644 --- a/PORTING.md +++ b/PORTING.md @@ -56,7 +56,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h | `test_date` | Ported ✅ | Easy | | `test_error` | Ported ✅ | Medium | | `test_exception` | Not ported | Medium | -| `test_finalizer` | Not ported | Medium | +| `test_finalizer` | Ported ✅ | Medium | | `test_function` | Ported ✅ | Medium | | `test_general` | Not ported | Hard | | `test_handle_scope` | Ported ✅ | Easy | diff --git a/tests/js-native-api/test_finalizer/CMakeLists.txt b/tests/js-native-api/test_finalizer/CMakeLists.txt new file mode 100644 index 0000000..7eac315 --- /dev/null +++ b/tests/js-native-api/test_finalizer/CMakeLists.txt @@ -0,0 +1,6 @@ +# The reference suite already defines a CMake target named "test_finalizer" +# (tests/js-native-api/test_reference/test_finalizer.c), and target names must +# be globally unique. Use a distinct target name here but keep the output binary +# named test_finalizer.node so loadAddon('test_finalizer') resolves in this dir. +add_node_api_cts_experimental_addon(test_finalizer_standalone test_finalizer.c) +set_target_properties(test_finalizer_standalone PROPERTIES OUTPUT_NAME test_finalizer) diff --git a/tests/js-native-api/test_finalizer/test.js b/tests/js-native-api/test_finalizer/test.js new file mode 100644 index 0000000..6219a49 --- /dev/null +++ b/tests/js-native-api/test_finalizer/test.js @@ -0,0 +1,41 @@ +'use strict'; + +// node_api_post_finalizer and node_api_basic_env are both experimental. +if (!experimentalFeatures.postFinalizer) { + skipTest(); +} + +const test_finalizer = loadAddon('test_finalizer'); + +// The original asserts the finalizer runs in the same tick as GC; the harness +// only exposes async gcUntil, so we settle for observing that it fires. +{ + (() => { + const obj = {}; + test_finalizer.addFinalizer(obj); + })(); + + await gcUntil( + 'pure finalizer', + () => test_finalizer.getFinalizerCallCount() === 1, + ); + assert.strictEqual(test_finalizer.getFinalizerCallCount(), 1); +} + +// A finalizer that calls into JS runs later via node_api_post_finalizer, so +// it is observed asynchronously. +{ + let js_is_called = false; + (() => { + const obj = {}; + test_finalizer.addFinalizerWithJS(obj, () => { + js_is_called = true; + }); + })(); + + await gcUntil( + 'JS-calling finalizer', + () => test_finalizer.getFinalizerCallCount() === 2, + ); + assert.strictEqual(js_is_called, true); +} diff --git a/tests/js-native-api/test_finalizer/testFatalFinalize.js b/tests/js-native-api/test_finalizer/testFatalFinalize.js new file mode 100644 index 0000000..898b956 --- /dev/null +++ b/tests/js-native-api/test_finalizer/testFatalFinalize.js @@ -0,0 +1,19 @@ +'use strict'; + +// A fatal finalizer aborts the process, so it runs in a spawned child +// (testFatalFinalize_child.mjs) rather than tearing down the test runner. +// Needs the postFinalizer API and the ability to spawn. +if (!experimentalFeatures.postFinalizer || !runtimeFeatures.spawn) { + skipTest(); +} + +const result = await spawnTest('testFatalFinalize_child.mjs'); + +// `aborted` covers however the runtime surfaces the abort (a fatal signal on +// POSIX, one of a few exit codes on Windows); the stderr match is the real +// specificity guard. +assert.ok(result.aborted, `Expected child to abort, got status=${result.status}`); +assert.match( + result.stderr, + /Finalizer is calling a function that may affect GC state/, +); diff --git a/tests/js-native-api/test_finalizer/testFatalFinalize_child.mjs b/tests/js-native-api/test_finalizer/testFatalFinalize_child.mjs new file mode 100644 index 0000000..069954f --- /dev/null +++ b/tests/js-native-api/test_finalizer/testFatalFinalize_child.mjs @@ -0,0 +1,13 @@ +// Child of testFatalFinalize.js: a finalizer that illegally calls into JS from +// a basic finalizer context, which makes Node-API abort the process. +const test_finalizer = loadAddon('test_finalizer'); + +(() => { + const obj = {}; + test_finalizer.addFinalizerFailOnJS(obj); +})(); + +// The abort happens inside the finalizer, so this never returns; the counter +// guard only matters if the illegal call is somehow accepted. +let gcCount = 10; +await gcUntil('fatal finalize', () => --gcCount <= 0); diff --git a/tests/js-native-api/test_finalizer/test_finalizer.c b/tests/js-native-api/test_finalizer/test_finalizer.c new file mode 100644 index 0000000..721ca12 --- /dev/null +++ b/tests/js-native-api/test_finalizer/test_finalizer.c @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include "../common.h" +#include "../entry_point.h" + +typedef struct { + int32_t finalize_count; + napi_ref js_func; +} FinalizerData; + +static void finalizerOnlyCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + FinalizerData* data = (FinalizerData*)finalize_data; + int32_t count = ++data->finalize_count; + + // It is safe to access instance data + NODE_API_BASIC_CALL_RETURN_VOID(env, + napi_get_instance_data(env, (void**)&data)); + NODE_API_BASIC_ASSERT_RETURN_VOID(count = data->finalize_count, + "Expected to be the same FinalizerData"); +} + +static void finalizerCallingJSCallback(napi_env env, + void* finalize_data, + void* finalize_hint) { + napi_value js_func, undefined; + FinalizerData* data = (FinalizerData*)finalize_data; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, data->js_func, &js_func)); + NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, undefined, js_func, 0, NULL, NULL)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_func)); + data->js_func = NULL; + ++data->finalize_count; +} + +// Schedule async finalizer to run JavaScript-touching code. +static void finalizerWithJSCallback(node_api_basic_env env, + void* finalize_data, + void* finalize_hint) { + NODE_API_BASIC_CALL_RETURN_VOID( + env, + node_api_post_finalizer( + env, finalizerCallingJSCallback, finalize_data, finalize_hint)); +} + +static void finalizerWithFailedJSCallback(node_api_basic_env basic_env, + void* finalize_data, + void* finalize_hint) { + // Intentionally cast to a napi_env to test the fatal failure. + napi_env env = (napi_env)basic_env; + napi_value obj; + FinalizerData* data = (FinalizerData*)finalize_data; + ++data->finalize_count; + NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &obj)); +} + +static napi_value addFinalizer(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerOnlyCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and succeed. +static napi_value addFinalizerWithJS(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2] = {0}; + napi_valuetype arg_type; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_typeof(env, argv[1], &arg_type)); + NODE_API_ASSERT( + env, arg_type == napi_function, "Expected function as the second arg"); + NODE_API_CALL(env, napi_create_reference(env, argv[1], 1, &data->js_func)); + NODE_API_CALL(env, + napi_add_finalizer( + env, argv[0], data, finalizerWithJSCallback, NULL, NULL)); + return NULL; +} + +// This finalizer is going to call JavaScript from finalizer and fail. +static napi_value addFinalizerFailOnJS(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1] = {0}; + FinalizerData* data; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL( + env, + napi_add_finalizer( + env, argv[0], data, finalizerWithFailedJSCallback, NULL, NULL)); + return NULL; +} + +static napi_value getFinalizerCallCount(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + FinalizerData* data; + napi_value result; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&data)); + NODE_API_CALL(env, napi_create_int32(env, data->finalize_count, &result)); + return result; +} + +static void finalizeData(napi_env env, void* data, void* hint) { + free(data); +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + FinalizerData* data = (FinalizerData*)malloc(sizeof(FinalizerData)); + NODE_API_ASSERT(env, data != NULL, "Failed to allocate memory"); + memset(data, 0, sizeof(FinalizerData)); + NODE_API_CALL(env, napi_set_instance_data(env, data, finalizeData, NULL)); + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("addFinalizer", addFinalizer), + DECLARE_NODE_API_PROPERTY("addFinalizerWithJS", addFinalizerWithJS), + DECLARE_NODE_API_PROPERTY("addFinalizerFailOnJS", addFinalizerFailOnJS), + DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", + getFinalizerCallCount)}; + + NODE_API_CALL( + env, + napi_define_properties(env, + exports, + sizeof(descriptors) / sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END