Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
6 changes: 6 additions & 0 deletions tests/js-native-api/test_finalizer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
41 changes: 41 additions & 0 deletions tests/js-native-api/test_finalizer/test.js
Original file line number Diff line number Diff line change
@@ -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);
}
19 changes: 19 additions & 0 deletions tests/js-native-api/test_finalizer/testFatalFinalize.js
Original file line number Diff line number Diff line change
@@ -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/,
);
13 changes: 13 additions & 0 deletions tests/js-native-api/test_finalizer/testFatalFinalize_child.mjs
Original file line number Diff line number Diff line change
@@ -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);
148 changes: 148 additions & 0 deletions tests/js-native-api/test_finalizer/test_finalizer.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#include <js_native_api.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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
Loading