Start fixing bugs discovered by Node.js's Node-API tests (#14501)

Co-authored-by: Kai Tamkun <kai@tamkun.io>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Ashcon Partovi <ashcon@partovi.net>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com>
Co-authored-by: 190n <190n@users.noreply.github.com>
This commit is contained in:
190n
2025-02-26 22:11:42 -08:00
committed by GitHub
parent 174a0f70df
commit efabdcbe1f
341 changed files with 26257 additions and 2540 deletions

View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,17 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
#include <string.h>
static napi_value Method(napi_env env, napi_callback_info info) {
napi_value world;
const char* str = "world";
size_t str_len = strlen(str);
NODE_API_CALL(env, napi_create_string_utf8(env, str, str_len, &world));
return world;
}
NAPI_MODULE_INIT() {
napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("hello", Method);
NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc));
return exports;
}

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "binding",
"sources": [ "binding.c" ]
}
]
}

View File

@@ -0,0 +1,22 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const { Worker } = require('worker_threads');
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
const binding = require(bindingPath);
assert.strictEqual(binding.hello(), 'world');
console.log('binding.hello() =', binding.hello());
// Test multiple loading of the same module.
delete require.cache[bindingPath];
const rebinding = require(bindingPath);
assert.strictEqual(rebinding.hello(), 'world');
assert.notStrictEqual(binding.hello, rebinding.hello);
// Test that workers can load addons declared using NAPI_MODULE_INIT().
new Worker(`
const { parentPort } = require('worker_threads');
const msg = require(${JSON.stringify(bindingPath)}).hello();
parentPort.postMessage(msg)`, { eval: true })
.on('message', common.mustCall((msg) => assert.strictEqual(msg, 'world')));

View File

@@ -0,0 +1,14 @@
prefix node-api
# To mark a test as flaky, list the test name in the appropriate section
# below, without ".js", followed by ": PASS,FLAKY". Example:
# sample-test : PASS,FLAKY
[true] # This section applies to all platforms
[$system==win32]
[$system==solaris] # Also applies to SmartOS
# https://github.com/nodejs/node/issues/43457
test_fatal/test_threads: PASS,FLAKY
test_fatal/test_threads_report: PASS,FLAKY

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_async",
"sources": [ "test_async.c" ]
}
]
}

View File

@@ -0,0 +1,60 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const test_async = require(`./build/${common.buildType}/test_async`);
const events = [];
let testId;
const initAsyncId = async_hooks.executionAsyncId();
async_hooks.createHook({
init(id, provider, triggerAsyncId, resource) {
if (provider === 'TestResource') {
testId = id;
events.push({ type: 'init', id, provider, triggerAsyncId, resource });
}
},
before(id) {
if (testId === id) {
events.push({ type: 'before', id });
}
},
after(id) {
if (testId === id) {
events.push({ type: 'after', id });
}
},
destroy(id) {
if (testId === id) {
events.push({ type: 'destroy', id });
}
},
}).enable();
const resource = { foo: 'foo' };
events.push({ type: 'start' });
test_async.Test(5, resource, common.mustCall(function(err, val) {
assert.strictEqual(err, null);
assert.strictEqual(val, 10);
events.push({ type: 'complete' });
process.nextTick(common.mustCall());
}));
events.push({ type: 'scheduled' });
process.on('exit', () => {
assert.deepStrictEqual(events, [
{ type: 'start' },
{ type: 'init',
id: testId,
provider: 'TestResource',
triggerAsyncId: initAsyncId,
resource },
{ type: 'scheduled' },
{ type: 'before', id: testId },
{ type: 'complete' },
{ type: 'after', id: testId },
{ type: 'destroy', id: testId },
]);
});

View File

@@ -0,0 +1,14 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const test_async = require(`./build/${common.buildType}/test_async`);
const iterations = 500;
let x = 0;
const workDone = common.mustCall((status) => {
assert.strictEqual(status, 0);
if (++x < iterations) {
setImmediate(() => test_async.DoRepeatedWork(workDone));
}
}, iterations);
test_async.DoRepeatedWork(workDone);

View File

@@ -0,0 +1,18 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const test_async = require(`./build/${common.buildType}/test_async`);
process.on('uncaughtException', common.mustCall(function(err) {
try {
throw new Error('should not fail');
} catch (err) {
assert.strictEqual(err.message, 'should not fail');
}
assert.strictEqual(err.message, 'uncaught');
}));
// Successful async execution and completion callback.
test_async.Test(5, {}, common.mustCall(function() {
throw new Error('uncaught');
}));

View File

@@ -0,0 +1,30 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');
const test_async = require(`./build/${common.buildType}/test_async`);
const testException = 'test_async_cb_exception';
// Exception thrown from async completion callback.
// (Tested in a spawned process because the exception is fatal.)
if (process.argv[2] === 'child') {
test_async.Test(1, {}, common.mustCall(function() {
throw new Error(testException);
}));
return;
}
const p = child_process.spawnSync(
process.execPath, [ __filename, 'child' ]);
assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(testException));
// Successful async execution and completion callback.
test_async.Test(5, {}, common.mustCall(function(err, val) {
assert.strictEqual(err, null);
assert.strictEqual(val, 10);
process.nextTick(common.mustCall());
}));
// Async work item cancellation with callback.
test_async.TestCancel(common.mustCall());

View File

@@ -0,0 +1,216 @@
#include <assert.h>
#include <stdio.h>
#include <node_api.h>
#include <uv.h>
#include "../../js-native-api/common.h"
// this needs to be greater than the thread pool size
#define MAX_CANCEL_THREADS 6
typedef struct {
int32_t _input;
int32_t _output;
napi_ref _callback;
napi_async_work _request;
} carrier;
static carrier the_carrier;
static carrier async_carrier[MAX_CANCEL_THREADS];
static void Execute(napi_env env, void* data) {
uv_sleep(1000);
carrier* c = (carrier*)(data);
assert(c == &the_carrier);
c->_output = c->_input * 2;
}
static void Complete(napi_env env, napi_status status, void* data) {
carrier* c = (carrier*)(data);
if (c != &the_carrier) {
napi_throw_type_error(env, NULL, "Wrong data parameter to Complete.");
return;
}
if (status != napi_ok) {
napi_throw_type_error(env, NULL, "Execute callback failed.");
return;
}
napi_value argv[2];
NODE_API_CALL_RETURN_VOID(env, napi_get_null(env, &argv[0]));
NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, c->_output, &argv[1]));
napi_value callback;
NODE_API_CALL_RETURN_VOID(env,
napi_get_reference_value(env, c->_callback, &callback));
napi_value global;
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global));
napi_value result;
NODE_API_CALL_RETURN_VOID(env,
napi_call_function(env, global, callback, 2, argv, &result));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback));
NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request));
}
static napi_value Test(napi_env env, napi_callback_info info) {
size_t argc = 3;
napi_value argv[3];
napi_value _this;
napi_value resource_name;
void* data;
NODE_API_CALL(env,
napi_get_cb_info(env, info, &argc, argv, &_this, &data));
NODE_API_ASSERT(env, argc >= 3, "Not enough arguments, expected 3.");
napi_valuetype t;
NODE_API_CALL(env, napi_typeof(env, argv[0], &t));
NODE_API_ASSERT(env, t == napi_number,
"Wrong first argument, integer expected.");
NODE_API_CALL(env, napi_typeof(env, argv[1], &t));
NODE_API_ASSERT(env, t == napi_object,
"Wrong second argument, object expected.");
NODE_API_CALL(env, napi_typeof(env, argv[2], &t));
NODE_API_ASSERT(env, t == napi_function,
"Wrong third argument, function expected.");
the_carrier._output = 0;
NODE_API_CALL(env,
napi_get_value_int32(env, argv[0], &the_carrier._input));
NODE_API_CALL(env,
napi_create_reference(env, argv[2], 1, &the_carrier._callback));
NODE_API_CALL(env, napi_create_string_utf8(
env, "TestResource", NAPI_AUTO_LENGTH, &resource_name));
NODE_API_CALL(env, napi_create_async_work(env, argv[1], resource_name,
Execute, Complete, &the_carrier, &the_carrier._request));
NODE_API_CALL(env,
napi_queue_async_work(env, the_carrier._request));
return NULL;
}
static void BusyCancelComplete(napi_env env, napi_status status, void* data) {
carrier* c = (carrier*)(data);
NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request));
}
static void CancelComplete(napi_env env, napi_status status, void* data) {
carrier* c = (carrier*)(data);
if (status == napi_cancelled) {
// ok we got the status we expected so make the callback to
// indicate the cancel succeeded.
napi_value callback;
NODE_API_CALL_RETURN_VOID(env,
napi_get_reference_value(env, c->_callback, &callback));
napi_value global;
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global));
napi_value result;
NODE_API_CALL_RETURN_VOID(env,
napi_call_function(env, global, callback, 0, NULL, &result));
}
NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback));
}
static void CancelExecute(napi_env env, void* data) {
uv_sleep(1000);
}
static napi_value TestCancel(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1];
napi_value _this;
napi_value resource_name;
void* data;
NODE_API_CALL(env, napi_create_string_utf8(
env, "TestResource", NAPI_AUTO_LENGTH, &resource_name));
// make sure the work we are going to cancel will not be
// able to start by using all the threads in the pool
for (int i = 1; i < MAX_CANCEL_THREADS; i++) {
NODE_API_CALL(env, napi_create_async_work(env, NULL, resource_name,
CancelExecute, BusyCancelComplete,
&async_carrier[i], &async_carrier[i]._request));
NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[i]._request));
}
// now queue the work we are going to cancel and then cancel it.
// cancel will fail if the work has already started, but
// we have prevented it from starting by consuming all of the
// workers above.
NODE_API_CALL(env,
napi_get_cb_info(env, info, &argc, argv, &_this, &data));
NODE_API_CALL(env, napi_create_async_work(env, NULL, resource_name,
CancelExecute, CancelComplete,
&async_carrier[0], &async_carrier[0]._request));
NODE_API_CALL(env,
napi_create_reference(env, argv[0], 1, &async_carrier[0]._callback));
NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[0]._request));
NODE_API_CALL(env, napi_cancel_async_work(env, async_carrier[0]._request));
return NULL;
}
struct {
napi_ref ref;
napi_async_work work;
} repeated_work_info = { NULL, NULL };
static void RepeatedWorkerThread(napi_env env, void* data) {}
static void RepeatedWorkComplete(napi_env env, napi_status status, void* data) {
napi_value cb, js_status;
NODE_API_CALL_RETURN_VOID(env,
napi_get_reference_value(env, repeated_work_info.ref, &cb));
NODE_API_CALL_RETURN_VOID(env,
napi_delete_async_work(env, repeated_work_info.work));
NODE_API_CALL_RETURN_VOID(env,
napi_delete_reference(env, repeated_work_info.ref));
repeated_work_info.work = NULL;
repeated_work_info.ref = NULL;
NODE_API_CALL_RETURN_VOID(env,
napi_create_uint32(env, (uint32_t)status, &js_status));
NODE_API_CALL_RETURN_VOID(env,
napi_call_function(env, cb, cb, 1, &js_status, NULL));
}
static napi_value DoRepeatedWork(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value cb, name;
NODE_API_ASSERT(env, repeated_work_info.ref == NULL,
"Reference left over from previous work");
NODE_API_ASSERT(env, repeated_work_info.work == NULL,
"Work pointer left over from previous work");
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &cb, NULL, NULL));
NODE_API_CALL(env, napi_create_reference(env, cb, 1, &repeated_work_info.ref));
NODE_API_CALL(env,
napi_create_string_utf8(env, "Repeated Work", NAPI_AUTO_LENGTH, &name));
NODE_API_CALL(env,
napi_create_async_work(env, NULL, name, RepeatedWorkerThread,
RepeatedWorkComplete, &repeated_work_info, &repeated_work_info.work));
NODE_API_CALL(env, napi_queue_async_work(env, repeated_work_info.work));
return NULL;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("Test", Test),
DECLARE_NODE_API_PROPERTY("TestCancel", TestCancel),
DECLARE_NODE_API_PROPERTY("DoRepeatedWork", DoRepeatedWork),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,101 @@
#include "node_api.h"
#include "assert.h"
#include "uv.h"
#include <stdlib.h>
#include "../../js-native-api/common.h"
static int cleanup_hook_count = 0;
static void MustNotCall(napi_async_cleanup_hook_handle hook, void* arg) {
assert(0);
}
struct AsyncData {
uv_async_t async;
napi_env env;
napi_async_cleanup_hook_handle handle;
};
static struct AsyncData* CreateAsyncData() {
struct AsyncData* data = (struct AsyncData*) malloc(sizeof(struct AsyncData));
data->handle = NULL;
return data;
}
static void AfterCleanupHookTwo(uv_handle_t* handle) {
cleanup_hook_count++;
struct AsyncData* data = (struct AsyncData*) handle->data;
napi_status status = napi_remove_async_cleanup_hook(data->handle);
assert(status == napi_ok);
free(data);
}
static void AfterCleanupHookOne(uv_async_t* async) {
cleanup_hook_count++;
uv_close((uv_handle_t*) async, AfterCleanupHookTwo);
}
static void AsyncCleanupHook(napi_async_cleanup_hook_handle handle, void* arg) {
cleanup_hook_count++;
struct AsyncData* data = (struct AsyncData*) arg;
uv_loop_t* loop;
napi_status status = napi_get_uv_event_loop(data->env, &loop);
assert(status == napi_ok);
int err = uv_async_init(loop, &data->async, AfterCleanupHookOne);
assert(err == 0);
data->async.data = data;
data->handle = handle;
uv_async_send(&data->async);
}
static void ObjectFinalizer(napi_env env, void* data, void* hint) {
// AsyncCleanupHook and its subsequent callbacks are called twice.
assert(cleanup_hook_count == 6);
napi_ref* ref = data;
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *ref));
free(ref);
}
static void CreateObjectWrap(napi_env env) {
napi_value js_obj;
napi_ref* ref = malloc(sizeof(*ref));
NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &js_obj));
NODE_API_CALL_RETURN_VOID(
env, napi_wrap(env, js_obj, ref, ObjectFinalizer, NULL, ref));
// create a strong reference so that the finalizer is called at shutdown.
NODE_API_CALL_RETURN_VOID(env, napi_reference_ref(env, *ref, NULL));
}
static napi_value Init(napi_env env, napi_value exports) {
// Reinitialize the static variable to be compatible with musl libc.
cleanup_hook_count = 0;
// Create object wrap before cleanup hooks.
CreateObjectWrap(env);
{
struct AsyncData* data = CreateAsyncData();
data->env = env;
napi_add_async_cleanup_hook(env, AsyncCleanupHook, data, &data->handle);
}
{
struct AsyncData* data = CreateAsyncData();
data->env = env;
napi_add_async_cleanup_hook(env, AsyncCleanupHook, data, NULL);
}
{
napi_async_cleanup_hook_handle must_not_call_handle;
napi_add_async_cleanup_hook(
env, MustNotCall, NULL, &must_not_call_handle);
napi_remove_async_cleanup_hook(must_not_call_handle);
}
// Create object wrap after cleanup hooks.
CreateObjectWrap(env);
return NULL;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,8 @@
'use strict';
const common = require('../../common');
const path = require('path');
const { Worker } = require('worker_threads');
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
w.on('exit', common.mustCall(() => require(binding)));

View File

@@ -0,0 +1,129 @@
#include <node_api.h>
#include <assert.h>
#include "../../js-native-api/common.h"
#define MAX_ARGUMENTS 10
#define RESERVED_ARGS 3
static napi_value MakeCallback(napi_env env, napi_callback_info info) {
size_t argc = MAX_ARGUMENTS;
size_t n;
napi_value args[MAX_ARGUMENTS];
// NOLINTNEXTLINE (readability/null_usage)
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
NODE_API_ASSERT(env, argc > 0, "Wrong number of arguments");
napi_value async_context_wrap = args[0];
napi_value recv = args[1];
napi_value func = args[2];
napi_value argv[MAX_ARGUMENTS - RESERVED_ARGS];
for (n = RESERVED_ARGS; n < argc; n += 1) {
argv[n - RESERVED_ARGS] = args[n];
}
napi_valuetype func_type;
NODE_API_CALL(env, napi_typeof(env, func, &func_type));
napi_async_context context;
NODE_API_CALL(env, napi_unwrap(env, async_context_wrap, (void**)&context));
napi_value result;
if (func_type == napi_function) {
NODE_API_CALL(env, napi_make_callback(
env, context, recv, func, argc - RESERVED_ARGS, argv, &result));
} else {
NODE_API_ASSERT(env, false, "Unexpected argument type");
}
return result;
}
static void AsyncDestroyCb(napi_env env, void* data, void* hint) {
napi_status status = napi_async_destroy(env, (napi_async_context)data);
// We cannot use NODE_API_ASSERT_RETURN_VOID because we need to have a JS
// stack below in order to use exceptions.
assert(status == napi_ok);
}
#define CREATE_ASYNC_RESOURCE_ARGC 2
static napi_value CreateAsyncResource(napi_env env, napi_callback_info info) {
napi_value async_context_wrap;
NODE_API_CALL(env, napi_create_object(env, &async_context_wrap));
size_t argc = CREATE_ASYNC_RESOURCE_ARGC;
napi_value args[CREATE_ASYNC_RESOURCE_ARGC];
// NOLINTNEXTLINE (readability/null_usage)
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
napi_value resource = args[0];
napi_value js_destroy_on_finalizer = args[1];
napi_valuetype resource_type;
NODE_API_CALL(env, napi_typeof(env, resource, &resource_type));
if (resource_type != napi_object) {
resource = NULL;
}
napi_value resource_name;
NODE_API_CALL(env, napi_create_string_utf8(
env, "test_async", NAPI_AUTO_LENGTH, &resource_name));
napi_async_context context;
NODE_API_CALL(env, napi_async_init(env, resource, resource_name, &context));
bool destroy_on_finalizer = true;
if (argc == 2) {
NODE_API_CALL(env, napi_get_value_bool(env, js_destroy_on_finalizer, &destroy_on_finalizer));
}
if (resource_type == napi_object && destroy_on_finalizer) {
NODE_API_CALL(env, napi_add_finalizer(
env, resource, (void*)context, AsyncDestroyCb, NULL, NULL));
}
NODE_API_CALL(env, napi_wrap(env, async_context_wrap, context, NULL, NULL, NULL));
return async_context_wrap;
}
#define DESTROY_ASYNC_RESOURCE_ARGC 1
static napi_value DestroyAsyncResource(napi_env env, napi_callback_info info) {
size_t argc = DESTROY_ASYNC_RESOURCE_ARGC;
napi_value args[DESTROY_ASYNC_RESOURCE_ARGC];
// NOLINTNEXTLINE (readability/null_usage)
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 async_context_wrap = args[0];
napi_async_context async_context;
NODE_API_CALL(env,
napi_remove_wrap(env, async_context_wrap, (void**)&async_context));
NODE_API_CALL(env, napi_async_destroy(env, async_context));
return async_context_wrap;
}
static
napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
NODE_API_CALL(env, napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env, NULL, NAPI_AUTO_LENGTH, MakeCallback, NULL, &fn));
NODE_API_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn));
NODE_API_CALL(env, napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env, NULL, NAPI_AUTO_LENGTH, CreateAsyncResource, NULL, &fn));
NODE_API_CALL(env, napi_set_named_property(
env, exports, "createAsyncResource", fn));
NODE_API_CALL(env, napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env, NULL, NAPI_AUTO_LENGTH, DestroyAsyncResource, NULL, &fn));
NODE_API_CALL(env, napi_set_named_property(
env, exports, "destroyAsyncResource", fn));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,65 @@
'use strict';
// Flags: --gc-interval=100 --gc-global
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const {
createAsyncResource,
destroyAsyncResource,
makeCallback,
} = require(`./build/${common.buildType}/binding`);
// Test for https://github.com/nodejs/node/issues/27218:
// napi_async_destroy() can be called during a regular garbage collection run.
const hook_result = {
id: null,
init_called: false,
destroy_called: false,
};
const test_hook = async_hooks.createHook({
init: (id, type) => {
if (type === 'test_async') {
hook_result.id = id;
hook_result.init_called = true;
}
},
destroy: (id) => {
if (id === hook_result.id) hook_result.destroy_called = true;
},
});
test_hook.enable();
const asyncResource = createAsyncResource(
{ foo: 'bar' },
/* destroy_on_finalizer */false,
);
// Trigger GC. This does *not* use global.gc(), because what we want to verify
// is that `napi_async_destroy()` can be called when there is no JS context
// on the stack at the time of GC.
// Currently, using --gc-interval=100 + 1M elements seems to work fine for this.
const arr = new Array(1024 * 1024);
for (let i = 0; i < arr.length; i++)
arr[i] = {};
assert.strictEqual(hook_result.destroy_called, false);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, false);
makeCallback(asyncResource, process, () => {
const executionAsyncResource = async_hooks.executionAsyncResource();
// Assuming the executionAsyncResource was created for the absence of the
// initial `{ foo: 'bar' }`.
// This is the worst path of `napi_async_context` related API of
// recovering from the condition and not break the executionAsyncResource
// shape, although the executionAsyncResource might not be correct.
assert.strictEqual(typeof executionAsyncResource, 'object');
assert.strictEqual(executionAsyncResource.foo, undefined);
destroyAsyncResource(asyncResource);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, true);
});
});
});

View File

@@ -0,0 +1,44 @@
'use strict';
// Flags: --gc-interval=100 --gc-global
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const { createAsyncResource } = require(`./build/${common.buildType}/binding`);
// Test for https://github.com/nodejs/node/issues/27218:
// napi_async_destroy() can be called during a regular garbage collection run.
const hook_result = {
id: null,
init_called: false,
destroy_called: false,
};
const test_hook = async_hooks.createHook({
init: (id, type) => {
if (type === 'test_async') {
hook_result.id = id;
hook_result.init_called = true;
}
},
destroy: (id) => {
if (id === hook_result.id) hook_result.destroy_called = true;
},
});
test_hook.enable();
createAsyncResource({});
// Trigger GC. This does *not* use global.gc(), because what we want to verify
// is that `napi_async_destroy()` can be called when there is no JS context
// on the stack at the time of GC.
// Currently, using --gc-interval=100 + 1M elements seems to work fine for this.
const arr = new Array(1024 * 1024);
for (let i = 0; i < arr.length; i++)
arr[i] = {};
assert.strictEqual(hook_result.destroy_called, false);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, true);
});

View File

@@ -0,0 +1,63 @@
'use strict';
// Flags: --gc-interval=100 --gc-global
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const {
makeCallback,
createAsyncResource,
destroyAsyncResource,
} = require(`./build/${common.buildType}/binding`);
const hook_result = {
id: null,
resource: null,
init_called: false,
destroy_called: false,
};
const test_hook = async_hooks.createHook({
init: (id, type, triggerAsyncId, resource) => {
if (type === 'test_async') {
hook_result.id = id;
hook_result.init_called = true;
hook_result.resource = resource;
}
},
destroy: (id) => {
if (id === hook_result.id) hook_result.destroy_called = true;
},
});
test_hook.enable();
const resourceWrap = createAsyncResource(
/**
* set resource to NULL to generate a managed resource object
*/
undefined,
);
assert.strictEqual(hook_result.destroy_called, false);
const recv = {};
makeCallback(resourceWrap, recv, function callback() {
assert.strictEqual(hook_result.destroy_called, false);
assert.strictEqual(
hook_result.resource,
async_hooks.executionAsyncResource(),
);
assert.strictEqual(this, recv);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, false);
assert.notStrictEqual(
hook_result.resource,
async_hooks.executionAsyncResource(),
);
destroyAsyncResource(resourceWrap);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, true);
});
});
});

View File

@@ -0,0 +1,15 @@
{
"targets": [
{
"target_name": "test_buffer",
"defines": [
'NAPI_EXPERIMENTAL'
],
"sources": [ "test_buffer.c" ]
},
{
"target_name": "test_finalizer",
"sources": [ "test_finalizer.c" ]
}
]
}

View File

@@ -0,0 +1,14 @@
'use strict';
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_buffer`);
const assert = require('assert');
// Regression test for https://github.com/nodejs/node/issues/31134
// Buffers with finalizers are allowed to remain alive until
// Environment cleanup without crashing the process.
// The test stores the buffer on `process` to make sure it lives as long as
// the Context does.
process.externalBuffer = binding.newExternalBuffer();
assert.strictEqual(process.externalBuffer.toString(), binding.theText);

View File

@@ -0,0 +1,34 @@
'use strict';
// Flags: --expose-gc --no-concurrent-array-buffer-sweeping
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_buffer`);
const assert = require('assert');
const tick = require('util').promisify(require('../../common/tick'));
(async function() {
assert.strictEqual(binding.newBuffer().toString(), binding.theText);
assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText);
console.log('gc1');
global.gc();
assert.strictEqual(binding.getDeleterCallCount(), 0);
await tick(10);
assert.strictEqual(binding.getDeleterCallCount(), 1);
assert.strictEqual(binding.copyBuffer().toString(), binding.theText);
let buffer = binding.staticBuffer();
assert.strictEqual(binding.bufferHasInstance(buffer), true);
assert.strictEqual(binding.bufferInfo(buffer), true);
buffer = null;
global.gc();
assert.strictEqual(binding.getDeleterCallCount(), 1);
await tick(10);
console.log('gc2');
assert.strictEqual(binding.getDeleterCallCount(), 2);
// To test this doesn't crash
binding.invalidObjectAsBuffer({});
const testBuffer = binding.bufferFromArrayBuffer();
assert(testBuffer instanceof Buffer, 'Expected a Buffer');
})().then(common.mustCall());

View File

@@ -0,0 +1,189 @@
#include <stdlib.h>
#include <string.h>
#include <node_api.h>
#include "../../js-native-api/common.h"
static const char theText[] =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
static int deleterCallCount = 0;
static void deleteTheText(node_api_basic_env env,
void* data,
void* finalize_hint) {
NODE_API_BASIC_ASSERT_RETURN_VOID(data != NULL && strcmp(data, theText) == 0,
"invalid data");
(void)finalize_hint;
free(data);
deleterCallCount++;
}
static void noopDeleter(node_api_basic_env env,
void* data,
void* finalize_hint) {
NODE_API_BASIC_ASSERT_RETURN_VOID(data != NULL && strcmp(data, theText) == 0,
"invalid data");
(void)finalize_hint;
deleterCallCount++;
}
static napi_value newBuffer(napi_env env, napi_callback_info info) {
napi_value theBuffer;
char* theCopy;
const unsigned int kBufferSize = sizeof(theText);
NODE_API_CALL(env,
napi_create_buffer(
env, sizeof(theText), (void**)(&theCopy), &theBuffer));
NODE_API_ASSERT(env, theCopy, "Failed to copy static text for newBuffer");
memcpy(theCopy, theText, kBufferSize);
return theBuffer;
}
static napi_value newExternalBuffer(napi_env env, napi_callback_info info) {
napi_value theBuffer;
char* theCopy = strdup(theText);
NODE_API_ASSERT(
env, theCopy, "Failed to copy static text for newExternalBuffer");
NODE_API_CALL(env,
napi_create_external_buffer(env,
sizeof(theText),
theCopy,
deleteTheText,
NULL /* finalize_hint */,
&theBuffer));
return theBuffer;
}
static napi_value getDeleterCallCount(napi_env env, napi_callback_info info) {
napi_value callCount;
NODE_API_CALL(env, napi_create_int32(env, deleterCallCount, &callCount));
return callCount;
}
static napi_value copyBuffer(napi_env env, napi_callback_info info) {
napi_value theBuffer;
NODE_API_CALL(env, napi_create_buffer_copy(
env, sizeof(theText), theText, NULL, &theBuffer));
return theBuffer;
}
static napi_value bufferHasInstance(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 theBuffer = args[0];
bool hasInstance;
napi_valuetype theType;
NODE_API_CALL(env, napi_typeof(env, theBuffer, &theType));
NODE_API_ASSERT(env,
theType == napi_object, "bufferHasInstance: instance is not an object");
NODE_API_CALL(env, napi_is_buffer(env, theBuffer, &hasInstance));
NODE_API_ASSERT(env, hasInstance, "bufferHasInstance: instance is not a buffer");
napi_value returnValue;
NODE_API_CALL(env, napi_get_boolean(env, hasInstance, &returnValue));
return returnValue;
}
static napi_value bufferInfo(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 theBuffer = args[0];
char *bufferData;
napi_value returnValue;
size_t bufferLength;
NODE_API_CALL(env,
napi_get_buffer_info(
env, theBuffer, (void**)(&bufferData), &bufferLength));
NODE_API_CALL(env, napi_get_boolean(env,
!strcmp(bufferData, theText) && bufferLength == sizeof(theText),
&returnValue));
return returnValue;
}
static napi_value staticBuffer(napi_env env, napi_callback_info info) {
napi_value theBuffer;
NODE_API_CALL(env,
napi_create_external_buffer(env,
sizeof(theText),
(void*)theText,
noopDeleter,
NULL /* finalize_hint */,
&theBuffer));
return theBuffer;
}
static napi_value invalidObjectAsBuffer(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 notTheBuffer = args[0];
napi_status status = napi_get_buffer_info(env, notTheBuffer, NULL, NULL);
NODE_API_ASSERT(env,
status == napi_invalid_arg,
"napi_get_buffer_info: should fail with napi_invalid_arg "
"when passed non buffer");
return notTheBuffer;
}
static napi_value bufferFromArrayBuffer(napi_env env, napi_callback_info info) {
napi_status status;
napi_value arraybuffer;
napi_value buffer;
size_t byte_length = 1024;
void* data = NULL;
size_t buffer_length = 0;
void* buffer_data = NULL;
status = napi_create_arraybuffer(env, byte_length, &data, &arraybuffer);
NODE_API_ASSERT(env, status == napi_ok, "Failed to create arraybuffer");
status = node_api_create_buffer_from_arraybuffer(
env, arraybuffer, 0, byte_length, &buffer);
NODE_API_ASSERT(
env, status == napi_ok, "Failed to create buffer from arraybuffer");
status = napi_get_buffer_info(env, buffer, &buffer_data, &buffer_length);
NODE_API_ASSERT(env, status == napi_ok, "Failed to get buffer info");
NODE_API_ASSERT(env, buffer_length == byte_length, "Buffer length mismatch");
return buffer;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_value theValue;
NODE_API_CALL(env,
napi_create_string_utf8(env, theText, sizeof(theText), &theValue));
NODE_API_CALL(env,
napi_set_named_property(env, exports, "theText", theValue));
napi_property_descriptor methods[] = {
DECLARE_NODE_API_PROPERTY("newBuffer", newBuffer),
DECLARE_NODE_API_PROPERTY("newExternalBuffer", newExternalBuffer),
DECLARE_NODE_API_PROPERTY("getDeleterCallCount", getDeleterCallCount),
DECLARE_NODE_API_PROPERTY("copyBuffer", copyBuffer),
DECLARE_NODE_API_PROPERTY("bufferHasInstance", bufferHasInstance),
DECLARE_NODE_API_PROPERTY("bufferInfo", bufferInfo),
DECLARE_NODE_API_PROPERTY("staticBuffer", staticBuffer),
DECLARE_NODE_API_PROPERTY("invalidObjectAsBuffer", invalidObjectAsBuffer),
DECLARE_NODE_API_PROPERTY("bufferFromArrayBuffer", bufferFromArrayBuffer),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(methods) / sizeof(methods[0]), methods));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,61 @@
#include <node_api.h>
#include <stdlib.h>
#include <string.h>
#include "../../js-native-api/common.h"
static const char theText[] =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
static void malignDeleter(napi_env env, void* data, void* finalize_hint) {
NODE_API_ASSERT_RETURN_VOID(
env, data != NULL && strcmp(data, theText) == 0, "invalid data");
napi_ref finalizer_ref = (napi_ref)finalize_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 malignFinalizerBuffer(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 theBuffer;
NODE_API_CALL(env,
napi_create_external_buffer(env,
sizeof(theText),
(void*)theText,
malignDeleter,
finalizer_ref, // finalize_hint
&theBuffer));
return theBuffer;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor methods[] = {
DECLARE_NODE_API_PROPERTY("malignFinalizerBuffer", malignFinalizerBuffer),
};
NODE_API_CALL(
env,
napi_define_properties(
env, exports, sizeof(methods) / sizeof(methods[0]), methods));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,25 @@
'use strict';
// Flags: --expose-gc --force-node-api-uncaught-exceptions-policy
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_finalizer`);
const assert = require('assert');
const tick = require('util').promisify(require('../../common/tick'));
process.on('uncaughtException', common.mustCall((err) => {
assert.throws(() => { throw err; }, /finalizer error/);
}));
(async function() {
{
binding.malignFinalizerBuffer(common.mustCall(() => {
throw new Error('finalizer error');
}));
}
global.gc({ type: 'minor' });
await tick(common.platformTimeout(100));
global.gc();
await tick(common.platformTimeout(100));
global.gc();
await tick(common.platformTimeout(100));
})().then(common.mustCall());

View File

@@ -0,0 +1,119 @@
#include <stdlib.h>
#include "node_api.h"
#include "uv.h"
#include "../../js-native-api/common.h"
static napi_value RunInCallbackScope(napi_env env, napi_callback_info info) {
size_t argc;
napi_value args[3];
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, NULL, NULL, NULL));
NODE_API_ASSERT(env, argc == 3 , "Wrong number of arguments");
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
napi_valuetype valuetype;
NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype));
NODE_API_ASSERT(env, valuetype == napi_object,
"Wrong type of arguments. Expects an object as first argument.");
NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype));
NODE_API_ASSERT(env, valuetype == napi_string,
"Wrong type of arguments. Expects a string as second argument.");
NODE_API_CALL(env, napi_typeof(env, args[2], &valuetype));
NODE_API_ASSERT(env, valuetype == napi_function,
"Wrong type of arguments. Expects a function as third argument.");
napi_async_context context;
NODE_API_CALL(env, napi_async_init(env, args[0], args[1], &context));
napi_callback_scope scope = NULL;
NODE_API_CALL(env,
napi_open_callback_scope(env, args[0], context, &scope));
// If the function has an exception pending after the call that is ok
// so we don't use NODE_API_CALL as we must close the callback scope
// regardless.
napi_value result = NULL;
napi_status function_call_result =
napi_call_function(env, args[0], args[2], 0, NULL, &result);
if (function_call_result != napi_ok) {
GET_AND_THROW_LAST_ERROR((env));
}
NODE_API_CALL(env, napi_close_callback_scope(env, scope));
NODE_API_CALL(env, napi_async_destroy(env, context));
return result;
}
static napi_env shared_env = NULL;
static napi_deferred deferred = NULL;
static void Callback(uv_work_t* req, int ignored) {
napi_env env = shared_env;
napi_handle_scope handle_scope = NULL;
NODE_API_CALL_RETURN_VOID(env, napi_open_handle_scope(env, &handle_scope));
napi_value resource_name;
NODE_API_CALL_RETURN_VOID(env, napi_create_string_utf8(
env, "test", NAPI_AUTO_LENGTH, &resource_name));
napi_async_context context;
NODE_API_CALL_RETURN_VOID(env,
napi_async_init(env, NULL, resource_name, &context));
napi_value resource_object;
NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &resource_object));
napi_value undefined_value;
NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined_value));
napi_callback_scope scope = NULL;
NODE_API_CALL_RETURN_VOID(env,
napi_open_callback_scope(env, resource_object, context, &scope));
NODE_API_CALL_RETURN_VOID(env,
napi_resolve_deferred(env, deferred, undefined_value));
NODE_API_CALL_RETURN_VOID(env, napi_close_callback_scope(env, scope));
NODE_API_CALL_RETURN_VOID(env, napi_close_handle_scope(env, handle_scope));
NODE_API_CALL_RETURN_VOID(env, napi_async_destroy(env, context));
free(req);
}
static void NoopWork(uv_work_t* work) { (void) work; }
static napi_value TestResolveAsync(napi_env env, napi_callback_info info) {
napi_value promise = NULL;
if (deferred == NULL) {
shared_env = env;
NODE_API_CALL(env, napi_create_promise(env, &deferred, &promise));
uv_loop_t* loop = NULL;
NODE_API_CALL(env, napi_get_uv_event_loop(env, &loop));
uv_work_t* req = malloc(sizeof(*req));
uv_queue_work(loop,
req,
NoopWork,
Callback);
}
return promise;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
DECLARE_NODE_API_PROPERTY("runInCallbackScope", RunInCallbackScope),
DECLARE_NODE_API_PROPERTY("testResolveAsync", TestResolveAsync)
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,39 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
// The async_hook that we enable would register the process.emitWarning()
// call from loading the N-API addon as asynchronous activity because
// it contains a process.nextTick() call. Monkey patch it to be a no-op
// before we load the addon in order to avoid this.
process.emitWarning = () => {};
const { runInCallbackScope } = require(`./build/${common.buildType}/binding`);
const expectedResource = {};
const expectedResourceType = 'test-resource';
let insideHook = false;
let expectedId;
async_hooks.createHook({
init: common.mustCall((id, type, triggerAsyncId, resource) => {
if (type !== expectedResourceType) {
return;
}
assert.strictEqual(resource, expectedResource);
expectedId = id;
}),
before: common.mustCall((id) => {
assert.strictEqual(id, expectedId);
insideHook = true;
}),
after: common.mustCall((id) => {
assert.strictEqual(id, expectedId);
insideHook = false;
}),
}).enable();
runInCallbackScope(expectedResource, expectedResourceType, () => {
assert(insideHook);
});

View File

@@ -0,0 +1,6 @@
'use strict';
const common = require('../../common');
const { testResolveAsync } = require(`./build/${common.buildType}/binding`);
testResolveAsync().then(common.mustCall());

View File

@@ -0,0 +1,17 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const { runInCallbackScope } = require(`./build/${common.buildType}/binding`);
assert.strictEqual(runInCallbackScope({}, 'test-resource', () => 42), 42);
{
process.once('uncaughtException', common.mustCall((err) => {
assert.strictEqual(err.message, 'foo');
}));
runInCallbackScope({}, 'test-resource', () => {
throw new Error('foo');
});
}

View File

@@ -0,0 +1,50 @@
#include <assert.h>
#include <stdlib.h>
#include "../../js-native-api/common.h"
#include "node_api.h"
#include "uv.h"
static int cleanup_hook_count = 0;
static void cleanup(void* arg) {
cleanup_hook_count++;
printf("cleanup(%d)\n", *(int*)(arg));
fflush(stdout);
}
static int secret = 42;
static int wrong_secret = 17;
static void ObjectFinalizer(napi_env env, void* data, void* hint) {
// cleanup is called once.
assert(cleanup_hook_count == 1);
napi_ref* ref = data;
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *ref));
free(ref);
}
static void CreateObjectWrap(napi_env env) {
napi_value js_obj;
napi_ref* ref = malloc(sizeof(*ref));
NODE_API_CALL_RETURN_VOID(env, napi_create_object(env, &js_obj));
NODE_API_CALL_RETURN_VOID(
env, napi_wrap(env, js_obj, ref, ObjectFinalizer, NULL, ref));
// create a strong reference so that the finalizer is called at shutdown.
NODE_API_CALL_RETURN_VOID(env, napi_reference_ref(env, *ref, NULL));
}
static napi_value Init(napi_env env, napi_value exports) {
// Create object wrap before cleanup hooks.
CreateObjectWrap(env);
napi_add_env_cleanup_hook(env, cleanup, &wrong_secret);
napi_add_env_cleanup_hook(env, cleanup, &secret);
napi_remove_env_cleanup_hook(env, cleanup, &wrong_secret);
// Create object wrap after cleanup hooks.
CreateObjectWrap(env);
return NULL;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,13 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');
if (process.argv[2] === 'child') {
require(`./build/${common.buildType}/binding`);
} else {
const { stdout, status, signal } =
child_process.spawnSync(process.execPath, [__filename, 'child']);
assert.strictEqual(status, 0, `process exited with status(${status}) and signal(${signal})`);
assert.strictEqual(stdout.toString().trim(), 'cleanup(42)');
}

View File

@@ -0,0 +1,37 @@
#include <stdlib.h>
#include <node_api.h>
#include "../../js-native-api/common.h"
static void MyObject_fini(napi_env env, void* data, void* hint) {
napi_ref* ref = data;
napi_value global;
napi_value cleanup;
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global));
NODE_API_CALL_RETURN_VOID(
env, napi_get_named_property(env, global, "cleanup", &cleanup));
napi_status status = napi_call_function(env, global, cleanup, 0, NULL, NULL);
// We may not be allowed to call into JS, in which case a pending exception
// will be returned.
NODE_API_ASSERT_RETURN_VOID(env,
status == napi_ok || status == napi_pending_exception,
"Unexpected status for napi_call_function");
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *ref));
free(ref);
}
static napi_value MyObject(napi_env env, napi_callback_info info) {
napi_value js_this;
napi_ref* ref = malloc(sizeof(*ref));
NODE_API_CALL(env, napi_get_cb_info(env, info, NULL, NULL, &js_this, NULL));
NODE_API_CALL(env, napi_wrap(env, js_this, ref, MyObject_fini, NULL, ref));
return NULL;
}
NAPI_MODULE_INIT() {
napi_value ctor;
NODE_API_CALL(
env, napi_define_class(
env, "MyObject", NAPI_AUTO_LENGTH, MyObject, NULL, 0, NULL, &ctor));
NODE_API_CALL(env, napi_set_named_property(env, exports, "MyObject", ctor));
return exports;
}

View File

@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,14 @@
'use strict';
// Flags: --expose-gc
process.env.NODE_TEST_KNOWN_GLOBALS = 0;
const common = require('../../common');
const binding = require(`./build/${common.buildType}/binding`);
global.it = new binding.MyObject();
global.cleanup = () => {
delete global.it;
global.gc();
};

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_exception",
"sources": [ "test_exception.c" ]
}
]
}

View File

@@ -0,0 +1,20 @@
'use strict';
// Flags: --expose-gc
const common = require('../../common');
const assert = require('assert');
const test_exception = require(`./build/${common.buildType}/test_exception`);
// Make sure that exceptions that occur during finalization are propagated.
function testFinalize(binding) {
let x = test_exception[binding]();
x = null;
global.gc();
process.on('uncaughtException', (err) => {
assert.strictEqual(err.message, 'Error during Finalize');
});
// To assuage the linter's concerns.
(function() {})(x);
}
testFinalize('createExternalBuffer');

View File

@@ -0,0 +1,27 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
static void finalizer(napi_env env, void *data, void *hint) {
NODE_API_CALL_RETURN_VOID(env,
napi_throw_error(env, NULL, "Error during Finalize"));
}
static char buffer_data[12];
static napi_value createExternalBuffer(napi_env env, napi_callback_info info) {
napi_value buffer;
NODE_API_CALL(env, napi_create_external_buffer(env, sizeof(buffer_data),
buffer_data, finalizer, NULL, &buffer));
return buffer;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
DECLARE_NODE_API_PROPERTY("createExternalBuffer", createExternalBuffer),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_fatal",
"sources": [ "test_fatal.c" ]
}
]
}

View File

@@ -0,0 +1,19 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');
const test_fatal = require(`./build/${common.buildType}/test_fatal`);
// Test in a child process because the test code will trigger a fatal error
// that crashes the process.
if (process.argv[2] === 'child') {
test_fatal.Test();
return;
}
const p = child_process.spawnSync(
process.execPath, [ __filename, 'child' ]);
assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(
'FATAL ERROR: test_fatal::Test fatal message'));
assert.ok(p.status === 134 || p.signal === 'SIGABRT');

View File

@@ -0,0 +1,19 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');
const test_fatal = require(`./build/${common.buildType}/test_fatal`);
// Test in a child process because the test code will trigger a fatal error
// that crashes the process.
if (process.argv[2] === 'child') {
test_fatal.TestStringLength();
return;
}
const p = child_process.spawnSync(
process.execPath, [ '--napi-modules', __filename, 'child' ]);
assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(
'FATAL ERROR: test_fatal::Test fatal message'));
assert.ok(p.status === 134 || p.signal === 'SIGABRT');

View File

@@ -0,0 +1,48 @@
// For the purpose of this test we use libuv's threading library. When deciding
// on a threading library for a new project it bears remembering that in the
// future libuv may introduce API changes which may render it non-ABI-stable,
// which, in turn, may affect the ABI stability of the project despite its use
// of N-API.
#include <uv.h>
#include <node_api.h>
#include "../../js-native-api/common.h"
static uv_thread_t uv_thread;
static void work_thread(void* data) {
napi_fatal_error("work_thread", NAPI_AUTO_LENGTH,
"foobar", NAPI_AUTO_LENGTH);
}
static napi_value Test(napi_env env, napi_callback_info info) {
napi_fatal_error("test_fatal::Test", NAPI_AUTO_LENGTH,
"fatal message", NAPI_AUTO_LENGTH);
return NULL;
}
static napi_value TestThread(napi_env env, napi_callback_info info) {
NODE_API_ASSERT(env,
(uv_thread_create(&uv_thread, work_thread, NULL) == 0),
"Thread creation");
return NULL;
}
static napi_value TestStringLength(napi_env env, napi_callback_info info) {
napi_fatal_error("test_fatal::TestStringLength", 16, "fatal message", 13);
return NULL;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("Test", Test),
DECLARE_NODE_API_PROPERTY("TestStringLength", TestStringLength),
DECLARE_NODE_API_PROPERTY("TestThread", TestThread),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,21 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const child_process = require('child_process');
const test_fatal = require(`./build/${common.buildType}/test_fatal`);
// Test in a child process because the test code will trigger a fatal error
// that crashes the process.
if (process.argv[2] === 'child') {
test_fatal.TestThread();
while (true) {
// Busy loop to allow the work thread to abort.
}
}
const p = child_process.spawnSync(
process.execPath, [ __filename, 'child' ]);
assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(
'FATAL ERROR: work_thread foobar'));
assert(common.nodeProcessAborted(p.status, p.signal));

View File

@@ -0,0 +1,36 @@
'use strict';
const common = require('../../common');
const helper = require('../../common/report.js');
const tmpdir = require('../../common/tmpdir');
const assert = require('assert');
const child_process = require('child_process');
const test_fatal = require(`./build/${common.buildType}/test_fatal`);
if (common.buildType === 'Debug')
common.skip('as this will currently fail with a Debug check ' +
'in v8::Isolate::GetCurrent()');
// Test in a child process because the test code will trigger a fatal error
// that crashes the process.
if (process.argv[2] === 'child') {
test_fatal.TestThread();
// Busy loop to allow the work thread to abort.
while (true);
}
tmpdir.refresh();
const p = child_process.spawnSync(
process.execPath,
[ '--report-on-fatalerror', __filename, 'child' ],
{ cwd: tmpdir.path });
assert.ifError(p.error);
assert.ok(p.stderr.toString().includes(
'FATAL ERROR: work_thread foobar'));
assert.ok(p.status === 134 || p.signal === 'SIGABRT');
const reports = helper.findReports(p.pid, tmpdir.path);
assert.strictEqual(reports.length, 1);
const report = reports[0];
helper.validate(report);

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_fatal_exception",
"sources": [ "test_fatal_exception.c" ]
}
]
}

View File

@@ -0,0 +1,11 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const test_fatal = require(`./build/${common.buildType}/test_fatal_exception`);
process.on('uncaughtException', common.mustCall(function(err) {
assert.strictEqual(err.message, 'fatal error');
}));
const err = new Error('fatal error');
test_fatal.Test(err);

View File

@@ -0,0 +1,26 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
static napi_value Test(napi_env env, napi_callback_info info) {
napi_value err;
size_t argc = 1;
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &err, NULL, NULL));
NODE_API_CALL(env, napi_fatal_exception(env, err));
return NULL;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("Test", Test),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_general",
"sources": [ "test_general.c" ]
}
]
}

View File

@@ -0,0 +1,46 @@
'use strict';
const common = require('../../common');
const tmpdir = require('../../common/tmpdir');
const child_process = require('child_process');
const fs = require('fs');
const path = require('path');
const url = require('url');
const filename = require.resolve(`./build/${common.buildType}/test_general`);
const test_general = require(filename);
const assert = require('assert');
tmpdir.refresh();
{
// TODO(gabrielschulhof): This test may need updating if/when the filename
// becomes a full-fledged URL.
assert.strictEqual(test_general.filename, url.pathToFileURL(filename).href);
}
{
const urlTestDir = tmpdir.resolve('foo%#bar');
const urlTestFile = path.join(urlTestDir, path.basename(filename));
fs.mkdirSync(urlTestDir, { recursive: true });
fs.copyFileSync(filename, urlTestFile);
// Use a child process as indirection so that the built-in modules is not loaded
// into this process and can be removed here.
const reportedFilename = child_process.spawnSync(
process.execPath,
['-p', `require(${JSON.stringify(urlTestFile)}).filename`],
{ encoding: 'utf8' }).stdout.trim();
assert.doesNotMatch(reportedFilename, /foo%#bar/);
assert.strictEqual(reportedFilename, url.pathToFileURL(urlTestFile).href);
fs.rmSync(urlTestDir, {
force: true,
recursive: true,
maxRetries: 256,
});
}
{
const [ major, minor, patch, release ] = test_general.testGetNodeVersion();
assert.strictEqual(process.version.split('-')[0],
`v${major}.${minor}.${patch}`);
assert.strictEqual(release, process.release.name);
}

View File

@@ -0,0 +1,52 @@
#define NAPI_VERSION 9
// we define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED here to validate that it can
// be used as a form of test itself. It is
// not related to any of the other tests
// defined in the file
#define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED
#include <node_api.h>
#include <stdlib.h>
#include "../../js-native-api/common.h"
static napi_value testGetNodeVersion(napi_env env, napi_callback_info info) {
const napi_node_version* node_version;
napi_value result, major, minor, patch, release;
NODE_API_CALL(env, napi_get_node_version(env, &node_version));
NODE_API_CALL(env, napi_create_uint32(env, node_version->major, &major));
NODE_API_CALL(env, napi_create_uint32(env, node_version->minor, &minor));
NODE_API_CALL(env, napi_create_uint32(env, node_version->patch, &patch));
NODE_API_CALL(env,
napi_create_string_utf8(
env, node_version->release, NAPI_AUTO_LENGTH, &release));
NODE_API_CALL(env, napi_create_array_with_length(env, 4, &result));
NODE_API_CALL(env, napi_set_element(env, result, 0, major));
NODE_API_CALL(env, napi_set_element(env, result, 1, minor));
NODE_API_CALL(env, napi_set_element(env, result, 2, patch));
NODE_API_CALL(env, napi_set_element(env, result, 3, release));
return result;
}
static napi_value GetFilename(napi_env env, napi_callback_info info) {
const char* filename;
napi_value result;
NODE_API_CALL(env, node_api_get_module_file_name(env, &filename));
NODE_API_CALL(env,
napi_create_string_utf8(env, filename, NAPI_AUTO_LENGTH, &result));
return result;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = {
DECLARE_NODE_API_PROPERTY("testGetNodeVersion", testGetNodeVersion),
DECLARE_NODE_API_GETTER("filename", GetFilename),
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_init_order",
"sources": [ "test_init_order.cc" ]
}
]
}

View File

@@ -0,0 +1,10 @@
'use strict';
// This test verifies that C++ static variable dynamic initialization is called
// correctly and does not interfere with the module initialization.
const common = require('../../common');
const test_init_order = require(`./build/${common.buildType}/test_init_order`);
const assert = require('assert');
assert.strictEqual(test_init_order.cppIntValue, 42);
assert.strictEqual(test_init_order.cppStringValue, '123');

View File

@@ -0,0 +1,52 @@
#include <node_api.h>
#include <memory>
#include <string>
#include "../../js-native-api/common.h"
// This test verifies that use of the NAPI_MODULE in C++ code does not
// interfere with the C++ dynamic static initializers.
namespace {
// This class uses dynamic static initializers for the test.
// In production code developers must avoid dynamic static initializers because
// they affect the start up time. They must prefer static initialization such as
// use of constexpr functions or classes with constexpr constructors. E.g.
// instead of using std::string, it is preferable to use const char[], or
// constexpr std::string_view starting with C++17, or even constexpr
// std::string starting with C++20.
struct MyClass {
static const std::unique_ptr<int> valueHolder;
static const std::string testString;
};
const std::unique_ptr<int> MyClass::valueHolder =
std::unique_ptr<int>(new int(42));
// NOLINTNEXTLINE(runtime/string)
const std::string MyClass::testString = std::string("123");
} // namespace
EXTERN_C_START
napi_value Init(napi_env env, napi_value exports) {
napi_value cppIntValue, cppStringValue;
NODE_API_CALL(env,
napi_create_int32(env, *MyClass::valueHolder, &cppIntValue));
NODE_API_CALL(
env,
napi_create_string_utf8(
env, MyClass::testString.c_str(), NAPI_AUTO_LENGTH, &cppStringValue));
napi_property_descriptor descriptors[] = {
DECLARE_NODE_API_PROPERTY_VALUE("cppIntValue", cppIntValue),
DECLARE_NODE_API_PROPERTY_VALUE("cppStringValue", cppStringValue)};
NODE_API_CALL(env,
napi_define_properties(
env, exports, std::size(descriptors), descriptors));
return exports;
}
EXTERN_C_END
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,22 @@
#include <stdio.h>
#include <stdlib.h>
#include <node_api.h>
static void addon_free(napi_env env, void* data, void* hint) {
napi_ref* ref = data;
napi_delete_reference(env, *ref);
free(ref);
fprintf(stderr, "addon_free");
}
napi_value addon_new(napi_env env, napi_value exports, bool ref_first) {
napi_ref* ref = malloc(sizeof(*ref));
if (ref_first) {
napi_create_reference(env, exports, 1, ref);
napi_set_instance_data(env, ref, addon_free, NULL);
} else {
napi_set_instance_data(env, ref, addon_free, NULL);
napi_create_reference(env, exports, 1, ref);
}
return exports;
}

View File

@@ -0,0 +1,24 @@
{
"targets": [
{
"target_name": "test_instance_data",
"sources": [
"test_instance_data.c"
]
},
{
"target_name": "test_set_then_ref",
"sources": [
"addon.c",
"test_set_then_ref.c",
]
},
{
"target_name": "test_ref_then_set",
"sources": [
"addon.c",
"test_ref_then_set.c",
]
},
]
}

View File

@@ -0,0 +1,50 @@
'use strict';
// Test API calls for instance data.
const common = require('../../common');
if (module !== require.main) {
// When required as a module, run the tests.
const test_instance_data =
require(`./build/${common.buildType}/test_instance_data`);
// Test that instance data can be used in an async work callback.
new Promise((resolve) => test_instance_data.asyncWorkCallback(resolve))
// Test that the buffer finalizer can access the instance data.
.then(() => new Promise((resolve) => {
test_instance_data.testBufferFinalizer(resolve);
global.gc();
}))
// Test that the thread-safe function can access the instance data.
.then(() => new Promise((resolve) =>
test_instance_data.testThreadsafeFunction(common.mustCall(),
common.mustCall(resolve))));
} else {
// When launched as a script, run tests in either a child process or in a
// worker thread.
const assert = require('assert');
const requireAs = require('../../common/require-as');
const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] };
const { spawnSync } = require('child_process');
// Run tests in a child process.
requireAs(__filename, ['--expose-gc'], runOptions, 'child');
// Run tests in a worker thread in a child process.
requireAs(__filename, ['--expose-gc'], runOptions, 'worker');
function testProcessExit(addonName) {
// Make sure that process exit is clean when the instance data has
// references to JS objects.
const path = require.resolve(`./build/${common.buildType}/${addonName}`);
const child = spawnSync(process.execPath, ['-e', `require(${JSON.stringify(path)});`]);
assert.strictEqual(child.signal, null);
assert.strictEqual(child.status, 0);
assert.strictEqual(child.stderr.toString(), 'addon_free');
}
testProcessExit('test_ref_then_set');
testProcessExit('test_set_then_ref');
}

View File

@@ -0,0 +1,206 @@
#include <stdlib.h>
#include <uv.h>
#include <node_api.h>
#include "../../js-native-api/common.h"
typedef struct {
napi_ref js_cb_ref;
napi_ref js_tsfn_finalizer_ref;
napi_threadsafe_function tsfn;
uv_thread_t thread;
} AddonData;
static void AsyncWorkCbExecute(napi_env env, void* data) {
(void) env;
(void) data;
}
static void call_cb_and_delete_ref(napi_env env, napi_ref* optional_ref) {
napi_value js_cb, undefined;
if (optional_ref == NULL) {
AddonData* data;
NODE_API_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data));
optional_ref = &data->js_cb_ref;
}
NODE_API_CALL_RETURN_VOID(env,
napi_get_reference_value(env, *optional_ref, &js_cb));
NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
NODE_API_CALL_RETURN_VOID(env,
napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, *optional_ref));
*optional_ref = NULL;
}
static void AsyncWorkCbComplete(napi_env env,
napi_status status,
void* data) {
(void) status;
(void) data;
call_cb_and_delete_ref(env, NULL);
}
static bool establish_callback_ref(napi_env env, napi_callback_info info) {
AddonData* data;
size_t argc = 1;
napi_value js_cb;
NODE_API_CALL_BASE(env, napi_get_instance_data(env, (void**)&data), false);
NODE_API_ASSERT_BASE(
env, data->js_cb_ref == NULL, "reference must be NULL", false);
NODE_API_CALL_BASE(
env, napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL), false);
NODE_API_CALL_BASE(
env, napi_create_reference(env, js_cb, 1, &data->js_cb_ref), false);
return true;
}
static napi_value AsyncWorkCallback(napi_env env, napi_callback_info info) {
if (establish_callback_ref(env, info)) {
napi_value resource_name;
napi_async_work work;
NODE_API_CALL(env,
napi_create_string_utf8(
env, "AsyncIncrement", NAPI_AUTO_LENGTH, &resource_name));
NODE_API_CALL(env,
napi_create_async_work(
env, NULL, resource_name, AsyncWorkCbExecute, AsyncWorkCbComplete,
NULL, &work));
NODE_API_CALL(env, napi_queue_async_work(env, work));
}
return NULL;
}
static void TestBufferFinalizerCallback(napi_env env, void* data, void* hint) {
(void) data;
(void) hint;
call_cb_and_delete_ref(env, NULL);
}
static napi_value TestBufferFinalizer(napi_env env, napi_callback_info info) {
napi_value buffer = NULL;
if (establish_callback_ref(env, info)) {
NODE_API_CALL(env,
napi_create_external_buffer(
env, sizeof(napi_callback), TestBufferFinalizer,
TestBufferFinalizerCallback, NULL, &buffer));
}
return buffer;
}
static void ThreadsafeFunctionCallJS(napi_env env,
napi_value tsfn_cb,
void* context,
void* data) {
(void) tsfn_cb;
(void) context;
(void) data;
call_cb_and_delete_ref(env, NULL);
}
static void ThreadsafeFunctionTestThread(void* raw_data) {
AddonData* data = raw_data;
napi_status status;
// No need to call `napi_acquire_threadsafe_function()` because the main
// thread has set the refcount to 1 and there is only this one secondary
// thread.
status = napi_call_threadsafe_function(data->tsfn,
ThreadsafeFunctionCallJS,
napi_tsfn_nonblocking);
if (status != napi_ok) {
napi_fatal_error("ThreadSafeFunctionTestThread",
NAPI_AUTO_LENGTH,
"Failed to call TSFN",
NAPI_AUTO_LENGTH);
}
status = napi_release_threadsafe_function(data->tsfn, napi_tsfn_release);
if (status != napi_ok) {
napi_fatal_error("ThreadSafeFunctionTestThread",
NAPI_AUTO_LENGTH,
"Failed to release TSFN",
NAPI_AUTO_LENGTH);
}
}
static void FinalizeThreadsafeFunction(napi_env env, void* raw, void* hint) {
AddonData* data;
NODE_API_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data));
NODE_API_ASSERT_RETURN_VOID(env,
uv_thread_join(&data->thread) == 0, "Failed to join the thread");
call_cb_and_delete_ref(env, &data->js_tsfn_finalizer_ref);
data->tsfn = NULL;
}
// This function accepts two arguments: the JS callback, and the finalize
// callback. The latter moves the test forward.
static napi_value
TestThreadsafeFunction(napi_env env, napi_callback_info info) {
AddonData* data;
size_t argc = 2;
napi_value argv[2], resource_name;
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_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL");
NODE_API_ASSERT(
env, data->js_tsfn_finalizer_ref == NULL,
"tsfn finalizer reference must be NULL");
NODE_API_CALL(env, napi_create_reference(env, argv[0], 1, &data->js_cb_ref));
NODE_API_CALL(env,
napi_create_reference(env, argv[1], 1, &data->js_tsfn_finalizer_ref));
NODE_API_CALL(env,
napi_create_string_utf8(
env, "TSFN instance data test", NAPI_AUTO_LENGTH, &resource_name));
NODE_API_CALL(env,
napi_create_threadsafe_function(
env, NULL, NULL, resource_name, 0, 1, NULL,
FinalizeThreadsafeFunction, NULL, ThreadsafeFunctionCallJS,
&data->tsfn));
NODE_API_ASSERT(env,
uv_thread_create(&data->thread, ThreadsafeFunctionTestThread, data) == 0,
"uv_thread_create failed");
return NULL;
}
static void DeleteAddonData(napi_env env, void* raw_data, void* hint) {
AddonData* data = raw_data;
if (data->js_cb_ref) {
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref));
}
if (data->js_tsfn_finalizer_ref) {
NODE_API_CALL_RETURN_VOID(env,
napi_delete_reference(env, data->js_tsfn_finalizer_ref));
}
free(data);
}
static napi_value Init(napi_env env, napi_value exports) {
AddonData* data = malloc(sizeof(*data));
data->js_cb_ref = NULL;
data->js_tsfn_finalizer_ref = NULL;
NODE_API_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL));
napi_property_descriptor props[] = {
DECLARE_NODE_API_PROPERTY("asyncWorkCallback", AsyncWorkCallback),
DECLARE_NODE_API_PROPERTY("testBufferFinalizer", TestBufferFinalizer),
DECLARE_NODE_API_PROPERTY("testThreadsafeFunction", TestThreadsafeFunction),
};
NODE_API_CALL(env,
napi_define_properties(
env, exports, sizeof(props) / sizeof(*props), props));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <node_api.h>
napi_value addon_new(napi_env env, napi_value exports, bool ref_first);
// static napi_value
NAPI_MODULE_INIT(/*napi_env env, napi_value exports */) {
return addon_new(env, exports, true);
}

View File

@@ -0,0 +1,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <node_api.h>
napi_value addon_new(napi_env env, napi_value exports, bool ref_first);
// static napi_value
NAPI_MODULE_INIT(/*napi_env env, napi_value exports */) {
return addon_new(env, exports, false);
}

View File

@@ -0,0 +1,60 @@
#include <node_api.h>
#include <assert.h>
#include "../../js-native-api/common.h"
#define MAX_ARGUMENTS 10
#define RESERVED_ARGS 3
static napi_value MakeCallback(napi_env env, napi_callback_info info) {
size_t argc = MAX_ARGUMENTS;
size_t n;
napi_value args[MAX_ARGUMENTS];
// NOLINTNEXTLINE (readability/null_usage)
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
NODE_API_ASSERT(env, argc > 0, "Wrong number of arguments");
napi_value resource = args[0];
napi_value recv = args[1];
napi_value func = args[2];
napi_value argv[MAX_ARGUMENTS - RESERVED_ARGS];
for (n = RESERVED_ARGS; n < argc; n += 1) {
argv[n - RESERVED_ARGS] = args[n];
}
napi_valuetype func_type;
NODE_API_CALL(env, napi_typeof(env, func, &func_type));
napi_value resource_name;
NODE_API_CALL(env, napi_create_string_utf8(
env, "test", NAPI_AUTO_LENGTH, &resource_name));
napi_async_context context;
NODE_API_CALL(env, napi_async_init(env, resource, resource_name, &context));
napi_value result;
if (func_type == napi_function) {
NODE_API_CALL(env, napi_make_callback(
env, context, recv, func, argc - RESERVED_ARGS, argv, &result));
} else {
NODE_API_ASSERT(env, false, "Unexpected argument type");
}
NODE_API_CALL(env, napi_async_destroy(env, context));
return result;
}
static
napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
NODE_API_CALL(env, napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env, NULL, NAPI_AUTO_LENGTH, MakeCallback, NULL, &fn));
NODE_API_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,53 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const async_hooks = require('async_hooks');
const binding = require(`./build/${common.buildType}/binding`);
const makeCallback = binding.makeCallback;
// Check async hooks integration using async context.
const hook_result = {
id: null,
init_called: false,
before_called: false,
after_called: false,
destroy_called: false,
};
const test_hook = async_hooks.createHook({
init: (id, type) => {
if (type === 'test') {
hook_result.id = id;
hook_result.init_called = true;
}
},
before: (id) => {
if (id === hook_result.id) hook_result.before_called = true;
},
after: (id) => {
if (id === hook_result.id) hook_result.after_called = true;
},
destroy: (id) => {
if (id === hook_result.id) hook_result.destroy_called = true;
},
});
test_hook.enable();
/**
* Resource should be able to be arbitrary objects without special internal
* slots. Testing with plain object here.
*/
const resource = {};
makeCallback(resource, process, function cb() {
assert.strictEqual(this, process);
assert.strictEqual(async_hooks.executionAsyncResource(), resource);
});
assert.strictEqual(hook_result.init_called, true);
assert.strictEqual(hook_result.before_called, true);
assert.strictEqual(hook_result.after_called, true);
setImmediate(() => {
assert.strictEqual(hook_result.destroy_called, true);
test_hook.disable();
});

View File

@@ -0,0 +1,86 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const vm = require('vm');
const binding = require(`./build/${common.buildType}/binding`);
const makeCallback = binding.makeCallback;
function myMultiArgFunc(arg1, arg2, arg3) {
assert.strictEqual(arg1, 1);
assert.strictEqual(arg2, 2);
assert.strictEqual(arg3, 3);
return 42;
}
/**
* Resource should be able to be arbitrary objects without special internal
* slots. Testing with plain object here.
*/
const resource = {};
assert.strictEqual(makeCallback(resource, process, common.mustCall(function() {
assert.strictEqual(arguments.length, 0);
assert.strictEqual(this, process);
return 42;
})), 42);
assert.strictEqual(makeCallback(resource, process, common.mustCall(function(x) {
assert.strictEqual(arguments.length, 1);
assert.strictEqual(this, process);
assert.strictEqual(x, 1337);
return 42;
}), 1337), 42);
assert.strictEqual(makeCallback(resource, this,
common.mustCall(myMultiArgFunc), 1, 2, 3), 42);
// TODO(node-api): napi_make_callback needs to support
// strings passed for the func argument
//
// const recv = {
// one: common.mustCall(function() {
// assert.strictEqual(0, arguments.length);
// assert.strictEqual(this, recv);
// return 42;
// }),
// two: common.mustCall(function(x) {
// assert.strictEqual(1, arguments.length);
// assert.strictEqual(this, recv);
// assert.strictEqual(x, 1337);
// return 42;
// }),
// };
//
// assert.strictEqual(makeCallback(recv, 'one'), 42);
// assert.strictEqual(makeCallback(recv, 'two', 1337), 42);
//
// // Check that callbacks on a receiver from a different context works.
// const foreignObject = vm.runInNewContext('({ fortytwo() { return 42; } })');
// assert.strictEqual(makeCallback(foreignObject, 'fortytwo'), 42);
// Check that the callback is made in the context of the receiver.
const target = vm.runInNewContext(`
(function($Object) {
if (Object === $Object)
throw new Error('bad');
return Object;
})
`);
assert.notStrictEqual(makeCallback(resource, process, target, Object), Object);
// Runs in inner context.
const forward = vm.runInNewContext(`
(function(forward) {
return forward(Object);
})
`);
// Runs in outer context.
function endpoint($Object) {
if (Object === $Object)
throw new Error('bad');
return Object;
}
assert.strictEqual(makeCallback(resource, process, forward, endpoint), Object);

View File

@@ -0,0 +1,41 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
static napi_value MakeCallback(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
// NOLINTNEXTLINE (readability/null_usage)
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL));
napi_value recv = args[0];
napi_value func = args[1];
napi_status status = napi_make_callback(env, NULL /* async_context */,
recv, func, 0 /* argc */, NULL /* argv */, NULL /* result */);
bool isExceptionPending;
NODE_API_CALL(env, napi_is_exception_pending(env, &isExceptionPending));
if (isExceptionPending && !(status == napi_pending_exception)) {
// if there is an exception pending we don't expect any
// other error
napi_value pending_error;
status = napi_get_and_clear_last_exception(env, &pending_error);
NODE_API_CALL(env,
napi_throw_error((env),
NULL,
"error when only pending exception expected"));
}
return recv;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
NODE_API_CALL(env, napi_create_function(
// NOLINTNEXTLINE (readability/null_usage)
env, NULL, NAPI_AUTO_LENGTH, MakeCallback, NULL, &fn));
NODE_API_CALL(env, napi_set_named_property(env, exports, "makeCallback", fn));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.c' ]
}
]
}

View File

@@ -0,0 +1,150 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const domain = require('domain');
const binding = require(`./build/${common.buildType}/binding`);
const makeCallback = binding.makeCallback;
// Make sure this is run in the future.
const mustCallCheckDomains = common.mustCall(checkDomains);
// Make sure that using MakeCallback allows the error to propagate.
assert.throws(function() {
makeCallback({}, function() {
throw new Error('hi from domain error');
});
}, /^Error: hi from domain error$/);
// Check the execution order of the nextTickQueue and MicrotaskQueue in
// relation to running multiple MakeCallback's from bootstrap,
// node::MakeCallback() and node::AsyncWrap::MakeCallback().
// TODO(trevnorris): Is there a way to verify this is being run during
// bootstrap?
(function verifyExecutionOrder(arg) {
const results = [];
// Processing of the MicrotaskQueue is manually handled by node. They are not
// processed until after the nextTickQueue has been processed.
Promise.resolve(1).then(common.mustCall(function() {
results.push(7);
}));
// The nextTick should run after all immediately invoked calls.
process.nextTick(common.mustCall(function() {
results.push(3);
// Run same test again but while processing the nextTickQueue to make sure
// the following MakeCallback call breaks in the middle of processing the
// queue and allows the script to run normally.
process.nextTick(common.mustCall(function() {
results.push(6);
}));
makeCallback({}, common.mustCall(function() {
results.push(4);
}));
results.push(5);
}));
results.push(0);
// MakeCallback is calling the function immediately, but should then detect
// that a script is already in the middle of execution and return before
// either the nextTickQueue or MicrotaskQueue are processed.
makeCallback({}, common.mustCall(function() {
results.push(1);
}));
// This should run before either the nextTickQueue or MicrotaskQueue are
// processed. Previously MakeCallback would not detect this circumstance
// and process them immediately.
results.push(2);
setImmediate(common.mustCall(function() {
for (let i = 0; i < results.length; i++) {
assert.strictEqual(results[i], i,
`verifyExecutionOrder(${arg}) results: ${results}`);
}
if (arg === 1) {
// The tests are first run on bootstrap during LoadEnvironment() in
// src/node.cc. Now run the tests through node::MakeCallback().
setImmediate(function() {
makeCallback({}, common.mustCall(function() {
verifyExecutionOrder(2);
}));
});
} else if (arg === 2) {
// Make sure there are no conflicts using node::MakeCallback()
// within timers.
setTimeout(common.mustCall(function() {
verifyExecutionOrder(3);
}), 10);
} else if (arg === 3) {
mustCallCheckDomains();
} else {
throw new Error('UNREACHABLE');
}
}));
}(1));
function checkDomains() {
// Check that domains are properly entered/exited when called in multiple
// levels from both node::MakeCallback() and AsyncWrap::MakeCallback
setImmediate(common.mustCall(function() {
const d1 = domain.create();
const d2 = domain.create();
const d3 = domain.create();
makeCallback({ domain: d1 }, common.mustCall(function() {
assert.strictEqual(d1, process.domain);
makeCallback({ domain: d2 }, common.mustCall(function() {
assert.strictEqual(d2, process.domain);
makeCallback({ domain: d3 }, common.mustCall(function() {
assert.strictEqual(d3, process.domain);
}));
assert.strictEqual(d2, process.domain);
}));
assert.strictEqual(d1, process.domain);
}));
}));
setTimeout(common.mustCall(function() {
const d1 = domain.create();
const d2 = domain.create();
const d3 = domain.create();
makeCallback({ domain: d1 }, common.mustCall(function() {
assert.strictEqual(d1, process.domain);
makeCallback({ domain: d2 }, common.mustCall(function() {
assert.strictEqual(d2, process.domain);
makeCallback({ domain: d3 }, common.mustCall(function() {
assert.strictEqual(d3, process.domain);
}));
assert.strictEqual(d2, process.domain);
}));
assert.strictEqual(d1, process.domain);
}));
}), 1);
function testTimer(id) {
// Make sure nextTick, setImmediate and setTimeout can all recover properly
// after a thrown makeCallback call.
const d = domain.create();
d.on('error', common.mustCall(function(e) {
assert.strictEqual(e.message, `throw from domain ${id}`);
}));
makeCallback({ domain: d }, function() {
throw new Error(`throw from domain ${id}`);
});
throw new Error('UNREACHABLE');
}
process.nextTick(common.mustCall(testTimer), 3);
setImmediate(common.mustCall(testTimer), 2);
setTimeout(common.mustCall(testTimer), 1, 1);
}

View File

@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'test_null_init',
'sources': [ 'test_null_init.c' ]
}
]
}

View File

@@ -0,0 +1,7 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
assert.throws(
() => require(`./build/${common.buildType}/test_null_init`),
/Module has no declared entry point[.]/);

View File

@@ -0,0 +1,53 @@
#include <node_api.h>
// This test uses old module initialization style deprecated in current code.
// The goal is to see that all previously compiled code continues to work the
// same way as before.
// The test has a copy of previous macro definitions which are removed from
// the node_api.h file.
#if defined(_MSC_VER)
#if defined(__cplusplus)
#define NAPI_C_CTOR(fn) \
static void NAPI_CDECL fn(void); \
namespace { \
struct fn##_ { \
fn##_() { fn(); } \
} fn##_v_; \
} \
static void NAPI_CDECL fn(void)
#else // !defined(__cplusplus)
#pragma section(".CRT$XCU", read)
// The NAPI_C_CTOR macro defines a function fn that is called during CRT
// initialization.
// C does not support dynamic initialization of static variables and this code
// simulates C++ behavior. Exporting the function pointer prevents it from being
// optimized. See for details:
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-initialization?view=msvc-170
#define NAPI_C_CTOR(fn) \
static void NAPI_CDECL fn(void); \
__declspec(dllexport, allocate(".CRT$XCU")) void(NAPI_CDECL * fn##_)(void) = \
fn; \
static void NAPI_CDECL fn(void)
#endif // defined(__cplusplus)
#else
#define NAPI_C_CTOR(fn) \
static void fn(void) __attribute__((constructor)); \
static void fn(void)
#endif
#define NAPI_MODULE_TEST(modname, regfunc) \
EXTERN_C_START \
static napi_module _module = { \
NAPI_MODULE_VERSION, \
0, \
__FILE__, \
regfunc, \
#modname, \
NULL, \
{0}, \
}; \
NAPI_C_CTOR(_register_##modname) { napi_module_register(&_module); } \
EXTERN_C_END
NAPI_MODULE_TEST(NODE_GYP_MODULE_NAME, NULL)

View File

@@ -0,0 +1,14 @@
{
"targets": [
{
"target_name": "test_reference_all_types",
"sources": [ "test_reference_by_node_api_version.c" ],
"defines": [ "NAPI_EXPERIMENTAL" ],
},
{
"target_name": "test_reference_obj_only",
"sources": [ "test_reference_by_node_api_version.c" ],
"defines": [ "NAPI_VERSION=8" ],
}
]
}

View File

@@ -0,0 +1,125 @@
'use strict';
// Flags: --expose-gc
//
// Testing API calls for Node-API references.
// We compare their behavior between Node-API version 8 and later.
// In version 8 references can be created only for object, function,
// and symbol types, while in newer versions they can be created for
// any value type.
//
const { buildType } = require('../../common');
const { gcUntil } = require('../../common/gc');
const assert = require('assert');
const addon_v8 = require(`./build/${buildType}/test_reference_obj_only`);
const addon_new = require(`./build/${buildType}/test_reference_all_types`);
async function runTests(addon, isVersion8, isLocalSymbol) {
let allEntries = [];
(() => {
// Create values of all napi_valuetype types.
const undefinedValue = undefined;
const nullValue = null;
const booleanValue = false;
const numberValue = 42;
const stringValue = 'test_string';
const globalSymbolValue = Symbol.for('test_symbol_global');
const localSymbolValue = Symbol('test_symbol_local');
const symbolValue = isLocalSymbol ? localSymbolValue : globalSymbolValue;
const objectValue = { x: 1, y: 2 };
const functionValue = (x, y) => x + y;
const externalValue = addon.createExternal();
const bigintValue = 9007199254740991n;
// The position of entries in the allEntries array corresponds to the
// napi_valuetype enum value. See the CreateRef function for the
// implementation details.
allEntries = [
{ value: undefinedValue, canBeWeak: false, canBeRefV8: false },
{ value: nullValue, canBeWeak: false, canBeRefV8: false },
{ value: booleanValue, canBeWeak: false, canBeRefV8: false },
{ value: numberValue, canBeWeak: false, canBeRefV8: false },
{ value: stringValue, canBeWeak: false, canBeRefV8: false },
{ value: symbolValue, canBeWeak: isLocalSymbol, canBeRefV8: true,
isAlwaysStrong: !isLocalSymbol },
{ value: objectValue, canBeWeak: true, canBeRefV8: true },
{ value: functionValue, canBeWeak: true, canBeRefV8: true },
{ value: externalValue, canBeWeak: true, canBeRefV8: true },
{ value: bigintValue, canBeWeak: false, canBeRefV8: false },
];
// Go over all values of different types, create strong ref values for
// them, read the stored values, and check how the ref count works.
for (const entry of allEntries) {
if (!isVersion8 || entry.canBeRefV8) {
const index = addon.createRef(entry.value);
const refValue = addon.getRefValue(index);
assert.strictEqual(entry.value, refValue);
assert.strictEqual(addon.ref(index), 2);
assert.strictEqual(addon.unref(index), 1);
assert.strictEqual(addon.unref(index), 0);
} else {
assert.throws(() => { addon.createRef(entry.value); },
{
name: 'Error',
message: 'Invalid argument',
});
}
}
// When the reference count is zero, then object types become weak pointers
// and other types are released.
// Here we know that the GC is not run yet because the values are
// still in the allEntries array.
allEntries.forEach((entry, index) => {
if (!isVersion8 || entry.canBeRefV8) {
if (entry.canBeWeak || entry.isAlwaysStrong) {
assert.strictEqual(addon.getRefValue(index), entry.value);
} else {
assert.strictEqual(addon.getRefValue(index), undefined);
}
}
// Set to undefined to allow GC collect the value.
entry.value = undefined;
});
// To check that GC pass is done.
const objWithFinalizer = {};
addon.addFinalizer(objWithFinalizer);
})();
addon.initFinalizeCount();
assert.strictEqual(addon.getFinalizeCount(), 0);
await gcUntil('Wait until a finalizer is called',
() => (addon.getFinalizeCount() === 1));
// Create and call finalizer again to make sure that we had another GC pass.
(() => {
const objWithFinalizer = {};
addon.addFinalizer(objWithFinalizer);
})();
await gcUntil('Wait until a finalizer is called again',
() => (addon.getFinalizeCount() === 2));
// After GC and finalizers run, all values that support weak reference
// semantic must return undefined value.
allEntries.forEach((entry, index) => {
if (!isVersion8 || entry.canBeRefV8) {
if (!entry.isAlwaysStrong) {
assert.strictEqual(addon.getRefValue(index), undefined);
} else {
assert.notStrictEqual(addon.getRefValue(index), undefined);
}
addon.deleteRef(index);
}
});
}
async function runAllTests() {
await runTests(addon_v8, /* isVersion8 */ true, /* isLocalSymbol */ true);
await runTests(addon_v8, /* isVersion8 */ true, /* isLocalSymbol */ false);
await runTests(addon_new, /* isVersion8 */ false, /* isLocalSymbol */ true);
await runTests(addon_new, /* isVersion8 */ false, /* isLocalSymbol */ false);
}
runAllTests();

View File

@@ -0,0 +1,169 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
#include "stdlib.h"
static uint32_t finalizeCount = 0;
static void FreeData(node_api_basic_env env, void* data, void* hint) {
NODE_API_BASIC_ASSERT_RETURN_VOID(data != NULL, "Expects non-NULL data.");
free(data);
}
static void Finalize(node_api_basic_env env, void* data, void* hint) {
++finalizeCount;
}
static napi_status GetArgValue(napi_env env,
napi_callback_info info,
napi_value* argValue) {
size_t argc = 1;
NODE_API_CHECK_STATUS(
napi_get_cb_info(env, info, &argc, argValue, NULL, NULL));
NODE_API_ASSERT_STATUS(env, argc == 1, "Expects one arg.");
return napi_ok;
}
static napi_status GetArgValueAsIndex(napi_env env,
napi_callback_info info,
uint32_t* index) {
napi_value argValue;
NODE_API_CHECK_STATUS(GetArgValue(env, info, &argValue));
napi_valuetype valueType;
NODE_API_CHECK_STATUS(napi_typeof(env, argValue, &valueType));
NODE_API_ASSERT_STATUS(
env, valueType == napi_number, "Argument must be a number.");
return napi_get_value_uint32(env, argValue, index);
}
static napi_status GetRef(napi_env env,
napi_callback_info info,
napi_ref* ref) {
uint32_t index;
NODE_API_CHECK_STATUS(GetArgValueAsIndex(env, info, &index));
napi_ref* refValues;
NODE_API_CHECK_STATUS(napi_get_instance_data(env, (void**)&refValues));
NODE_API_ASSERT_STATUS(env, refValues != NULL, "Cannot get instance data.");
*ref = refValues[index];
return napi_ok;
}
static napi_value ToUInt32Value(napi_env env, uint32_t value) {
napi_value result;
NODE_API_CALL(env, napi_create_uint32(env, value, &result));
return result;
}
static napi_status InitRefArray(napi_env env) {
// valueRefs array has one entry per napi_valuetype
napi_ref* valueRefs = malloc(sizeof(napi_ref) * ((int)napi_bigint + 1));
return napi_set_instance_data(env, valueRefs, (napi_finalize)&FreeData, NULL);
}
static napi_value CreateExternal(napi_env env, napi_callback_info info) {
napi_value result;
int* data = (int*)malloc(sizeof(int));
*data = 42;
NODE_API_CALL(env, napi_create_external(env, data, &FreeData, NULL, &result));
return result;
}
static napi_value CreateRef(napi_env env, napi_callback_info info) {
napi_value argValue;
NODE_API_CALL(env, GetArgValue(env, info, &argValue));
napi_valuetype valueType;
NODE_API_CALL(env, napi_typeof(env, argValue, &valueType));
uint32_t index = (uint32_t)valueType;
napi_ref* valueRefs;
NODE_API_CALL(env, napi_get_instance_data(env, (void**)&valueRefs));
NODE_API_CALL(env,
napi_create_reference(env, argValue, 1, valueRefs + index));
return ToUInt32Value(env, index);
}
static napi_value GetRefValue(napi_env env, napi_callback_info info) {
napi_ref refValue;
NODE_API_CALL(env, GetRef(env, info, &refValue));
napi_value value;
NODE_API_CALL(env, napi_get_reference_value(env, refValue, &value));
return value;
}
static napi_value Ref(napi_env env, napi_callback_info info) {
napi_ref refValue;
NODE_API_CALL(env, GetRef(env, info, &refValue));
uint32_t refCount;
NODE_API_CALL(env, napi_reference_ref(env, refValue, &refCount));
return ToUInt32Value(env, refCount);
}
static napi_value Unref(napi_env env, napi_callback_info info) {
napi_ref refValue;
NODE_API_CALL(env, GetRef(env, info, &refValue));
uint32_t refCount;
NODE_API_CALL(env, napi_reference_unref(env, refValue, &refCount));
return ToUInt32Value(env, refCount);
}
static napi_value DeleteRef(napi_env env, napi_callback_info info) {
napi_ref refValue;
NODE_API_CALL(env, GetRef(env, info, &refValue));
NODE_API_CALL(env, napi_delete_reference(env, refValue));
return NULL;
}
static napi_value AddFinalizer(napi_env env, napi_callback_info info) {
napi_value obj;
NODE_API_CALL(env, GetArgValue(env, info, &obj));
napi_valuetype valueType;
NODE_API_CALL(env, napi_typeof(env, obj, &valueType));
NODE_API_ASSERT(env, valueType == napi_object, "Argument must be an object.");
NODE_API_CALL(env, napi_add_finalizer(env, obj, NULL, &Finalize, NULL, NULL));
return NULL;
}
static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) {
return ToUInt32Value(env, finalizeCount);
}
static napi_value InitFinalizeCount(napi_env env, napi_callback_info info) {
finalizeCount = 0;
return NULL;
}
EXTERN_C_START
NAPI_MODULE_INIT() {
finalizeCount = 0;
NODE_API_CALL(env, InitRefArray(env));
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal),
DECLARE_NODE_API_PROPERTY("createRef", CreateRef),
DECLARE_NODE_API_PROPERTY("getRefValue", GetRefValue),
DECLARE_NODE_API_PROPERTY("ref", Ref),
DECLARE_NODE_API_PROPERTY("unref", Unref),
DECLARE_NODE_API_PROPERTY("deleteRef", DeleteRef),
DECLARE_NODE_API_PROPERTY("addFinalizer", AddFinalizer),
DECLARE_NODE_API_PROPERTY("getFinalizeCount", GetFinalizeCount),
DECLARE_NODE_API_PROPERTY("initFinalizeCount", InitFinalizeCount),
};
NODE_API_CALL(
env,
napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
EXTERN_C_END

View File

@@ -0,0 +1,347 @@
// For the purpose of this test we use libuv's threading library. When deciding
// on a threading library for a new project it bears remembering that in the
// future libuv may introduce API changes which may render it non-ABI-stable,
// which, in turn, may affect the ABI stability of the project despite its use
// of N-API.
#include <uv.h>
#include <node_api.h>
#include "../../js-native-api/common.h"
#define ARRAY_LENGTH 10000
#define MAX_QUEUE_SIZE 2
static uv_thread_t uv_threads[2];
static napi_threadsafe_function ts_fn;
typedef struct {
napi_threadsafe_function_call_mode block_on_full;
napi_threadsafe_function_release_mode abort;
bool start_secondary;
napi_ref js_finalize_cb;
uint32_t max_queue_size;
} ts_fn_hint;
static ts_fn_hint ts_info;
// Thread data to transmit to JS
static int ints[ARRAY_LENGTH];
static void secondary_thread(void* data) {
napi_threadsafe_function ts_fn = data;
if (napi_release_threadsafe_function(ts_fn, napi_tsfn_release) != napi_ok) {
napi_fatal_error("secondary_thread", NAPI_AUTO_LENGTH,
"napi_release_threadsafe_function failed", NAPI_AUTO_LENGTH);
}
}
// Source thread producing the data
static void data_source_thread(void* data) {
napi_threadsafe_function ts_fn = data;
int index;
void* hint;
ts_fn_hint *ts_fn_info;
napi_status status;
bool queue_was_full = false;
bool queue_was_closing = false;
if (napi_get_threadsafe_function_context(ts_fn, &hint) != napi_ok) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"napi_get_threadsafe_function_context failed", NAPI_AUTO_LENGTH);
}
ts_fn_info = (ts_fn_hint *)hint;
if (ts_fn_info != &ts_info) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"thread-safe function hint is not as expected", NAPI_AUTO_LENGTH);
}
if (ts_fn_info->start_secondary) {
if (napi_acquire_threadsafe_function(ts_fn) != napi_ok) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"napi_acquire_threadsafe_function failed", NAPI_AUTO_LENGTH);
}
if (uv_thread_create(&uv_threads[1], secondary_thread, ts_fn) != 0) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"failed to start secondary thread", NAPI_AUTO_LENGTH);
}
}
for (index = ARRAY_LENGTH - 1; index > -1 && !queue_was_closing; index--) {
status = napi_call_threadsafe_function(ts_fn, &ints[index],
ts_fn_info->block_on_full);
if (ts_fn_info->max_queue_size == 0 && (index % 1000 == 0)) {
// Let's make this thread really busy for 200 ms to give the main thread a
// chance to abort.
uint64_t start = uv_hrtime();
for (; uv_hrtime() - start < 200000000;);
}
switch (status) {
case napi_queue_full:
queue_was_full = true;
index++;
// fall through
case napi_ok:
continue;
case napi_closing:
queue_was_closing = true;
break;
default:
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"napi_call_threadsafe_function failed", NAPI_AUTO_LENGTH);
}
}
// Assert that the enqueuing of a value was refused at least once, if this is
// a non-blocking test run.
if (!ts_fn_info->block_on_full && !queue_was_full) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"queue was never full", NAPI_AUTO_LENGTH);
}
// Assert that the queue was marked as closing at least once, if this is an
// aborting test run.
if (ts_fn_info->abort == napi_tsfn_abort && !queue_was_closing) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"queue was never closing", NAPI_AUTO_LENGTH);
}
if (!queue_was_closing &&
napi_release_threadsafe_function(ts_fn, napi_tsfn_release) != napi_ok) {
napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
"napi_release_threadsafe_function failed", NAPI_AUTO_LENGTH);
}
}
// Getting the data into JS
static void call_js(napi_env env, napi_value cb, void* hint, void* data) {
if (!(env == NULL || cb == NULL)) {
napi_value argv, undefined;
NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, *(int*)data, &argv));
NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, undefined, cb, 1, &argv,
NULL));
}
}
static napi_ref alt_ref;
// Getting the data into JS with the alternative reference
static void call_ref(napi_env env, napi_value _, void* hint, void* data) {
if (!(env == NULL || alt_ref == NULL)) {
napi_value fn, argv, undefined;
NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, alt_ref, &fn));
NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, *(int*)data, &argv));
NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, undefined, fn, 1, &argv,
NULL));
}
}
// Cleanup
static napi_value StopThread(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value argv[2];
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
napi_valuetype value_type;
NODE_API_CALL(env, napi_typeof(env, argv[0], &value_type));
NODE_API_ASSERT(env, value_type == napi_function,
"StopThread argument is a function");
NODE_API_ASSERT(env, (ts_fn != NULL), "Existing threadsafe function");
NODE_API_CALL(env,
napi_create_reference(env, argv[0], 1, &(ts_info.js_finalize_cb)));
bool abort;
NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &abort));
NODE_API_CALL(env,
napi_release_threadsafe_function(ts_fn,
abort ? napi_tsfn_abort : napi_tsfn_release));
ts_fn = NULL;
return NULL;
}
// Join the thread and inform JS that we're done.
static void join_the_threads(napi_env env, void *data, void *hint) {
uv_thread_t *the_threads = data;
ts_fn_hint *the_hint = hint;
napi_value js_cb, undefined;
uv_thread_join(&the_threads[0]);
if (the_hint->start_secondary) {
uv_thread_join(&the_threads[1]);
}
NODE_API_CALL_RETURN_VOID(env,
napi_get_reference_value(env, the_hint->js_finalize_cb, &js_cb));
NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
NODE_API_CALL_RETURN_VOID(env,
napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env,
the_hint->js_finalize_cb));
if (alt_ref != NULL) {
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, alt_ref));
alt_ref = NULL;
}
}
static napi_value StartThreadInternal(napi_env env,
napi_callback_info info,
napi_threadsafe_function_call_js cb,
bool block_on_full,
bool alt_ref_js_cb) {
size_t argc = 4;
napi_value argv[4];
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
if (alt_ref_js_cb) {
NODE_API_CALL(env, napi_create_reference(env, argv[0], 1, &alt_ref));
argv[0] = NULL;
}
ts_info.block_on_full =
(block_on_full ? napi_tsfn_blocking : napi_tsfn_nonblocking);
NODE_API_ASSERT(env, (ts_fn == NULL), "Existing thread-safe function");
napi_value async_name;
NODE_API_CALL(env, napi_create_string_utf8(env,
"N-API Thread-safe Function Test", NAPI_AUTO_LENGTH, &async_name));
NODE_API_CALL(env,
napi_get_value_uint32(env, argv[3], &ts_info.max_queue_size));
NODE_API_CALL(env, napi_create_threadsafe_function(env,
argv[0],
NULL,
async_name,
ts_info.max_queue_size,
2,
uv_threads,
join_the_threads,
&ts_info,
cb,
&ts_fn));
bool abort;
NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &abort));
ts_info.abort = abort ? napi_tsfn_abort : napi_tsfn_release;
NODE_API_CALL(env,
napi_get_value_bool(env, argv[2], &(ts_info.start_secondary)));
NODE_API_ASSERT(env,
(uv_thread_create(&uv_threads[0], data_source_thread, ts_fn) == 0),
"Thread creation");
return NULL;
}
static napi_value Unref(napi_env env, napi_callback_info info) {
NODE_API_ASSERT(env, ts_fn != NULL, "No existing thread-safe function");
NODE_API_CALL(env, napi_unref_threadsafe_function(env, ts_fn));
return NULL;
}
static napi_value Release(napi_env env, napi_callback_info info) {
NODE_API_ASSERT(env, ts_fn != NULL, "No existing thread-safe function");
NODE_API_CALL(env, napi_release_threadsafe_function(ts_fn, napi_tsfn_release));
return NULL;
}
// Startup
static napi_value StartThread(napi_env env, napi_callback_info info) {
return StartThreadInternal(env, info, call_js,
/** block_on_full */true, /** alt_ref_js_cb */false);
}
static napi_value StartThreadNonblocking(napi_env env,
napi_callback_info info) {
return StartThreadInternal(env, info, call_js,
/** block_on_full */false, /** alt_ref_js_cb */false);
}
static napi_value StartThreadNoNative(napi_env env, napi_callback_info info) {
return StartThreadInternal(env, info, NULL,
/** block_on_full */true, /** alt_ref_js_cb */false);
}
static napi_value StartThreadNoJsFunc(napi_env env, napi_callback_info info) {
return StartThreadInternal(env, info, call_ref,
/** block_on_full */true, /** alt_ref_js_cb */true);
}
// Testing calling into JavaScript
static void ThreadSafeFunctionFinalize(napi_env env,
void* finalize_data,
void* finalize_hint) {
napi_ref js_func_ref = (napi_ref) finalize_data;
napi_value js_func;
napi_value recv;
NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_func_ref, &js_func));
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, recv, js_func, 0, NULL, NULL));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_func_ref));
}
// Testing calling into JavaScript
static napi_value CallIntoModule(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value argv[4];
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
napi_ref finalize_func;
NODE_API_CALL(env, napi_create_reference(env, argv[3], 1, &finalize_func));
napi_threadsafe_function tsfn;
NODE_API_CALL(env, napi_create_threadsafe_function(env, argv[0], argv[1], argv[2], 0, 1, finalize_func, ThreadSafeFunctionFinalize, NULL, NULL, &tsfn));
NODE_API_CALL(env, napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking));
NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release));
return NULL;
}
// Module init
static napi_value Init(napi_env env, napi_value exports) {
size_t index;
for (index = 0; index < ARRAY_LENGTH; index++) {
ints[index] = index;
}
napi_value js_array_length, js_max_queue_size;
napi_create_uint32(env, ARRAY_LENGTH, &js_array_length);
napi_create_uint32(env, MAX_QUEUE_SIZE, &js_max_queue_size);
napi_property_descriptor properties[] = {
{
"ARRAY_LENGTH",
NULL,
NULL,
NULL,
NULL,
js_array_length,
napi_enumerable,
NULL
},
{
"MAX_QUEUE_SIZE",
NULL,
NULL,
NULL,
NULL,
js_max_queue_size,
napi_enumerable,
NULL
},
DECLARE_NODE_API_PROPERTY("StartThread", StartThread),
DECLARE_NODE_API_PROPERTY("StartThreadNoNative", StartThreadNoNative),
DECLARE_NODE_API_PROPERTY("StartThreadNonblocking", StartThreadNonblocking),
DECLARE_NODE_API_PROPERTY("StartThreadNoJsFunc", StartThreadNoJsFunc),
DECLARE_NODE_API_PROPERTY("StopThread", StopThread),
DECLARE_NODE_API_PROPERTY("Unref", Unref),
DECLARE_NODE_API_PROPERTY("Release", Release),
DECLARE_NODE_API_PROPERTY("CallIntoModule", CallIntoModule),
};
NODE_API_CALL(env, napi_define_properties(env, exports,
sizeof(properties)/sizeof(properties[0]), properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,22 @@
{
'targets': [
{
'target_name': 'binding',
'sources': ['binding.c']
},
{
'target_name': 'test_uncaught_exception_v9',
'defines': [
'NAPI_VERSION=9'
],
'sources': ['test_uncaught_exception.c']
},
{
'target_name': 'test_uncaught_exception',
'defines': [
'NAPI_EXPERIMENTAL'
],
'sources': ['test_uncaught_exception.c']
}
]
}

View File

@@ -0,0 +1,227 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const binding = require(`./build/${common.buildType}/binding`);
const { fork } = require('child_process');
const expectedArray = (function(arrayLength) {
const result = [];
for (let index = 0; index < arrayLength; index++) {
result.push(arrayLength - 1 - index);
}
return result;
})(binding.ARRAY_LENGTH);
// Handle the rapid teardown test case as the child process. We unref the
// thread-safe function after we have received two values. This causes the
// process to exit and the environment cleanup handler to be invoked.
if (process.argv[2] === 'child') {
let callCount = 0;
binding.StartThread((value) => {
callCount++;
console.log(value);
if (callCount === 2) {
binding.Unref();
}
}, false /* abort */, true /* launchSecondary */, +process.argv[3]);
// Release the thread-safe function from the main thread so that it may be
// torn down via the environment cleanup handler.
binding.Release();
return;
}
function testWithJSMarshaller({
threadStarter,
quitAfter,
abort,
maxQueueSize,
launchSecondary,
}) {
return new Promise((resolve) => {
const array = [];
binding[threadStarter](function testCallback(value) {
array.push(value);
if (array.length === quitAfter) {
setImmediate(() => {
binding.StopThread(common.mustCall(() => {
resolve(array);
}), !!abort);
});
}
}, !!abort, !!launchSecondary, maxQueueSize);
if (threadStarter === 'StartThreadNonblocking') {
// Let's make this thread really busy for a short while to ensure that
// the queue fills and the thread receives a napi_queue_full.
const start = Date.now();
while (Date.now() - start < 200);
}
});
}
function testUnref(queueSize) {
return new Promise((resolve, reject) => {
let output = '';
const child = fork(__filename, ['child', queueSize], {
stdio: [process.stdin, 'pipe', process.stderr, 'ipc'],
});
child.on('close', (code) => {
if (code === 0) {
resolve(output.match(/\S+/g));
} else {
reject(new Error('Child process died with code ' + code));
}
});
child.stdout.on('data', (data) => (output += data.toString()));
})
.then((result) => assert.strictEqual(result.indexOf(0), -1));
}
new Promise(function testWithoutJSMarshaller(resolve) {
let callCount = 0;
binding.StartThreadNoNative(function testCallback() {
callCount++;
// The default call-into-JS implementation passes no arguments.
assert.strictEqual(arguments.length, 0);
if (callCount === binding.ARRAY_LENGTH) {
setImmediate(() => {
binding.StopThread(common.mustCall(() => {
resolve();
}), false);
});
}
}, false /* abort */, false /* launchSecondary */, binding.MAX_QUEUE_SIZE);
})
// Start the thread in blocking mode, and assert that all values are passed.
// Quit after it's done.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
maxQueueSize: binding.MAX_QUEUE_SIZE,
quitAfter: binding.ARRAY_LENGTH,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode, and assert that all values are passed.
// Quit after it's done.
// Doesn't pass the callback js function to napi_create_threadsafe_function.
// Instead, use an alternative reference to get js function called.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNoJsFunc',
maxQueueSize: binding.MAX_QUEUE_SIZE,
quitAfter: binding.ARRAY_LENGTH,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode with an infinite queue, and assert that all
// values are passed. Quit after it's done.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
maxQueueSize: 0,
quitAfter: binding.ARRAY_LENGTH,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in non-blocking mode, and assert that all values are passed.
// Quit after it's done.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNonblocking',
maxQueueSize: binding.MAX_QUEUE_SIZE,
quitAfter: binding.ARRAY_LENGTH,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode, and assert that all values are passed.
// Quit early, but let the thread finish.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
maxQueueSize: binding.MAX_QUEUE_SIZE,
quitAfter: 1,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode with an infinite queue, and assert that all
// values are passed. Quit early, but let the thread finish.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
maxQueueSize: 0,
quitAfter: 1,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in non-blocking mode, and assert that all values are passed.
// Quit early, but let the thread finish.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNonblocking',
maxQueueSize: binding.MAX_QUEUE_SIZE,
quitAfter: 1,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode, and assert that all values are passed.
// Quit early, but let the thread finish. Launch a secondary thread to test the
// reference counter incrementing functionality.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
quitAfter: 1,
maxQueueSize: binding.MAX_QUEUE_SIZE,
launchSecondary: true,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in non-blocking mode, and assert that all values are passed.
// Quit early, but let the thread finish. Launch a secondary thread to test the
// reference counter incrementing functionality.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNonblocking',
quitAfter: 1,
maxQueueSize: binding.MAX_QUEUE_SIZE,
launchSecondary: true,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start the thread in blocking mode, and assert that it could not finish.
// Quit early by aborting.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
quitAfter: 1,
maxQueueSize: binding.MAX_QUEUE_SIZE,
abort: true,
}))
.then((result) => assert.strictEqual(result.indexOf(0), -1))
// Start the thread in blocking mode with an infinite queue, and assert that it
// could not finish. Quit early by aborting.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThread',
quitAfter: 1,
maxQueueSize: 0,
abort: true,
}))
.then((result) => assert.strictEqual(result.indexOf(0), -1))
// Start the thread in non-blocking mode, and assert that it could not finish.
// Quit early and aborting.
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNonblocking',
quitAfter: 1,
maxQueueSize: binding.MAX_QUEUE_SIZE,
abort: true,
}))
.then((result) => assert.strictEqual(result.indexOf(0), -1))
// Make sure that threadsafe function isn't stalled when we hit
// `kMaxIterationCount` in `src/node_api.cc`
.then(() => testWithJSMarshaller({
threadStarter: 'StartThreadNonblocking',
maxQueueSize: binding.ARRAY_LENGTH >>> 1,
quitAfter: binding.ARRAY_LENGTH,
}))
.then((result) => assert.deepStrictEqual(result, expectedArray))
// Start a child process to test rapid teardown
.then(() => testUnref(binding.MAX_QUEUE_SIZE))
// Start a child process with an infinite queue to test rapid teardown
.then(() => testUnref(0));

View File

@@ -0,0 +1,22 @@
'use strict';
// Flags: --no-force-node-api-uncaught-exceptions-policy
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_uncaught_exception_v9`);
process.on(
'uncaughtException',
common.mustNotCall('uncaught callback errors should be suppressed ' +
'with the option --no-force-node-api-uncaught-exceptions-policy'),
);
binding.CallIntoModule(
common.mustCall(() => {
throw new Error('callback error');
}),
{},
'resource_name',
common.mustCall(function finalizer() {
throw new Error('finalizer error');
}),
);

View File

@@ -0,0 +1,62 @@
#include <node_api.h>
#include "../../js-native-api/common.h"
// Testing calling into JavaScript
static void ThreadSafeFunctionFinalize(napi_env env,
void* finalize_data,
void* finalize_hint) {
napi_ref js_func_ref = (napi_ref)finalize_data;
napi_value js_func;
napi_value recv;
NODE_API_CALL_RETURN_VOID(
env, napi_get_reference_value(env, js_func_ref, &js_func));
NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
NODE_API_CALL_RETURN_VOID(
env, napi_call_function(env, recv, js_func, 0, NULL, NULL));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_func_ref));
}
// Testing calling into JavaScript
static napi_value CallIntoModule(napi_env env, napi_callback_info info) {
size_t argc = 4;
napi_value argv[4];
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
napi_ref finalize_func;
NODE_API_CALL(env, napi_create_reference(env, argv[3], 1, &finalize_func));
napi_threadsafe_function tsfn;
NODE_API_CALL(env,
napi_create_threadsafe_function(env,
argv[0],
argv[1],
argv[2],
0,
1,
finalize_func,
ThreadSafeFunctionFinalize,
NULL,
NULL,
&tsfn));
NODE_API_CALL(env,
napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking));
NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release));
return NULL;
}
// Module init
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("CallIntoModule", CallIntoModule),
};
NODE_API_CALL(
env,
napi_define_properties(env,
exports,
sizeof(properties) / sizeof(properties[0]),
properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,7 @@
'use strict';
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_uncaught_exception`);
const { testUncaughtException } = require('./uncaught_exception');
testUncaughtException(binding);

View File

@@ -0,0 +1,8 @@
'use strict';
// Flags: --force-node-api-uncaught-exceptions-policy
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_uncaught_exception_v9`);
const { testUncaughtException } = require('./uncaught_exception');
testUncaughtException(binding);

View File

@@ -0,0 +1,31 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
function testUncaughtException(binding) {
const callbackCheck = common.mustCall((err) => {
assert.throws(() => { throw err; }, /callback error/);
process.removeListener('uncaughtException', callbackCheck);
process.on('uncaughtException', finalizerCheck);
});
const finalizerCheck = common.mustCall((err) => {
assert.throws(() => { throw err; }, /finalizer error/);
});
process.on('uncaughtException', callbackCheck);
binding.CallIntoModule(
common.mustCall(() => {
throw new Error('callback error');
}),
{},
'resource_name',
common.mustCall(function finalizer() {
throw new Error('finalizer error');
}),
);
}
module.exports = {
testUncaughtException,
};

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_uv_loop",
"sources": [ "test_uv_loop.cc" ]
}
]
}

View File

@@ -0,0 +1,5 @@
'use strict';
const common = require('../../common');
const { SetImmediate } = require(`./build/${common.buildType}/test_uv_loop`);
SetImmediate(common.mustCall());

View File

@@ -0,0 +1,89 @@
#include "../../js-native-api/common.h"
#include <node_api.h>
#include <uv.h>
#include <cassert>
#include <memory>
#include <utility>
template <typename T>
void* SetImmediate(napi_env env, T&& cb) {
T* ptr = new T(std::move(cb));
uv_loop_t* loop = nullptr;
uv_check_t* check = new uv_check_t;
check->data = ptr;
NODE_API_ASSERT(env,
napi_get_uv_event_loop(env, &loop) == napi_ok,
"can get event loop");
uv_check_init(loop, check);
uv_check_start(check, [](uv_check_t* check) {
std::unique_ptr<T> ptr {static_cast<T*>(check->data)};
T cb = std::move(*ptr);
uv_close(reinterpret_cast<uv_handle_t*>(check), [](uv_handle_t* handle) {
delete reinterpret_cast<uv_check_t*>(handle);
});
assert(cb() != nullptr);
});
// Idle handle is needed only to stop the event loop from blocking in poll.
uv_idle_t* idle = new uv_idle_t;
uv_idle_init(loop, idle);
uv_idle_start(idle, [](uv_idle_t* idle) {
uv_close(reinterpret_cast<uv_handle_t*>(idle), [](uv_handle_t* handle) {
delete reinterpret_cast<uv_check_t*>(handle);
});
});
return nullptr;
}
static char dummy;
napi_value SetImmediateBinding(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1];
napi_value _this;
void* data;
NODE_API_CALL(env,
napi_get_cb_info(env, info, &argc, argv, &_this, &data));
NODE_API_ASSERT(env, argc >= 1, "Not enough arguments, expected 1.");
napi_valuetype t;
NODE_API_CALL(env, napi_typeof(env, argv[0], &t));
NODE_API_ASSERT(env, t == napi_function,
"Wrong first argument, function expected.");
napi_ref cbref;
NODE_API_CALL(env,
napi_create_reference(env, argv[0], 1, &cbref));
SetImmediate(env, [=]() -> char* {
napi_value undefined;
napi_value callback;
napi_handle_scope scope;
NODE_API_CALL(env, napi_open_handle_scope(env, &scope));
NODE_API_CALL(env, napi_get_undefined(env, &undefined));
NODE_API_CALL(env, napi_get_reference_value(env, cbref, &callback));
NODE_API_CALL(env, napi_delete_reference(env, cbref));
NODE_API_CALL(env,
napi_call_function(env, undefined, callback, 0, nullptr, nullptr));
NODE_API_CALL(env, napi_close_handle_scope(env, scope));
return &dummy;
});
return nullptr;
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("SetImmediate", SetImmediateBinding)
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_uv_threadpool_size",
"sources": [ "test_uv_threadpool_size.c" ]
}
]
}

View File

@@ -0,0 +1,28 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const path = require('path');
const { spawnSync } = require('child_process');
if (process.config.variables.node_without_node_options) {
common.skip('missing NODE_OPTIONS support');
}
const uvThreadPoolPath = '../../fixtures/dotenv/uv-threadpool.env';
// Should update UV_THREADPOOL_SIZE
const filePath = path.join(__dirname, `./build/${common.buildType}/test_uv_threadpool_size`);
const code = `
const { test } = require(${JSON.stringify(filePath)});
const size = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
require('assert').strictEqual(size, 4);
test(size);
`.trim();
const child = spawnSync(
process.execPath,
[ `--env-file=${uvThreadPoolPath}`, '--eval', code ],
{ cwd: __dirname, encoding: 'utf-8' },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.status, 0);

View File

@@ -0,0 +1,7 @@
'use strict';
const common = require('../../common');
const { test } = require(`./build/${common.buildType}/test_uv_threadpool_size`);
const uvThreadpoolSize = parseInt(process.env.EXPECTED_UV_THREADPOOL_SIZE ||
process.env.UV_THREADPOOL_SIZE, 10) || 4;
test(uvThreadpoolSize);

View File

@@ -0,0 +1,189 @@
#undef NDEBUG
#include <assert.h>
#include <node_api.h>
#include <stdlib.h>
#include <uv.h>
#include "../../js-native-api/common.h"
typedef struct {
uv_mutex_t mutex;
uint32_t threadpool_size;
uint32_t n_tasks_started;
uint32_t n_tasks_exited;
uint32_t n_tasks_finalized;
bool observed_saturation;
} async_shared_data;
typedef struct {
uint32_t task_id;
async_shared_data* shared_data;
napi_async_work request;
} async_carrier;
static inline bool all_tasks_started(async_shared_data* d) {
assert(d->n_tasks_started <= d->threadpool_size + 1);
return d->n_tasks_started == d->threadpool_size + 1;
}
static inline bool all_tasks_exited(async_shared_data* d) {
assert(d->n_tasks_exited <= d->n_tasks_started);
return all_tasks_started(d) && d->n_tasks_exited == d->n_tasks_started;
}
static inline bool all_tasks_finalized(async_shared_data* d) {
assert(d->n_tasks_finalized <= d->n_tasks_exited);
return all_tasks_exited(d) && d->n_tasks_finalized == d->n_tasks_exited;
}
static inline bool still_saturating(async_shared_data* d) {
return d->n_tasks_started < d->threadpool_size;
}
static inline bool threadpool_saturated(async_shared_data* d) {
return d->n_tasks_started == d->threadpool_size && d->n_tasks_exited == 0;
}
static inline bool threadpool_desaturating(async_shared_data* d) {
return d->n_tasks_started >= d->threadpool_size && d->n_tasks_exited != 0;
}
static inline void print_info(const char* label, async_carrier* c) {
async_shared_data* d = c->shared_data;
printf("%s task_id=%u n_tasks_started=%u n_tasks_exited=%u "
"n_tasks_finalized=%u observed_saturation=%d\n",
label,
c->task_id,
d->n_tasks_started,
d->n_tasks_exited,
d->n_tasks_finalized,
d->observed_saturation);
}
static void Execute(napi_env env, void* data) {
async_carrier* c = (async_carrier*)data;
async_shared_data* d = c->shared_data;
// As long as fewer than threadpool_size async tasks have been started, more
// should be started (eventually). Only once that happens should scheduled
// async tasks remain queued.
uv_mutex_lock(&d->mutex);
bool should_be_concurrent = still_saturating(d);
d->n_tasks_started++;
assert(d->n_tasks_started <= d->threadpool_size + 1);
print_info("start", c);
if (should_be_concurrent) {
// Wait for the thread pool to be saturated. This is not an elegant way of
// doing so, but it really does not matter much here.
while (still_saturating(d)) {
print_info("waiting", c);
uv_mutex_unlock(&d->mutex);
uv_sleep(100);
uv_mutex_lock(&d->mutex);
}
// One async task will observe that the threadpool is saturated, that is,
// that threadpool_size tasks have been started and none have exited yet.
// That task will be the first to exit.
if (!d->observed_saturation) {
assert(threadpool_saturated(d));
d->observed_saturation = true;
} else {
assert(threadpool_saturated(d) || threadpool_desaturating(d));
}
} else {
// If this task is not among the first threadpool_size tasks, it should not
// have been started unless other tasks have already finished.
assert(threadpool_desaturating(d));
}
print_info("exit", c);
// Allow other tasks to access the shared data. If the thread pool is actually
// larger than threadpool_size, this allows an extraneous task to start, which
// will lead to an assertion error.
uv_mutex_unlock(&d->mutex);
uv_sleep(1000);
uv_mutex_lock(&d->mutex);
d->n_tasks_exited++;
uv_mutex_unlock(&d->mutex);
}
static void Complete(napi_env env, napi_status status, void* data) {
async_carrier* c = (async_carrier*)data;
async_shared_data* d = c->shared_data;
if (status != napi_ok) {
napi_throw_type_error(env, NULL, "Execute callback failed.");
return;
}
uv_mutex_lock(&d->mutex);
assert(threadpool_desaturating(d));
d->n_tasks_finalized++;
print_info("finalize", c);
if (all_tasks_finalized(d)) {
uv_mutex_unlock(&d->mutex);
uv_mutex_destroy(&d->mutex);
free(d);
} else {
uv_mutex_unlock(&d->mutex);
}
NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->request));
free(c);
}
static napi_value Test(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1];
napi_value this;
void* data;
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &this, &data));
NODE_API_ASSERT(env, argc >= 1, "Not enough arguments, expected 1.");
async_shared_data* shared_data = calloc(1, sizeof(async_shared_data));
assert(shared_data != NULL);
int ret = uv_mutex_init(&shared_data->mutex);
assert(ret == 0);
napi_valuetype t;
NODE_API_CALL(env, napi_typeof(env, argv[0], &t));
NODE_API_ASSERT(
env, t == napi_number, "Wrong first argument, integer expected.");
NODE_API_CALL(
env, napi_get_value_uint32(env, argv[0], &shared_data->threadpool_size));
napi_value resource_name;
NODE_API_CALL(env,
napi_create_string_utf8(
env, "TestResource", NAPI_AUTO_LENGTH, &resource_name));
for (uint32_t i = 0; i <= shared_data->threadpool_size; i++) {
async_carrier* carrier = malloc(sizeof(async_carrier));
assert(carrier != NULL);
carrier->task_id = i;
carrier->shared_data = shared_data;
NODE_API_CALL(env,
napi_create_async_work(env,
NULL,
resource_name,
Execute,
Complete,
carrier,
&carrier->request));
NODE_API_CALL(env, napi_queue_async_work(env, carrier->request));
}
return NULL;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc = DECLARE_NODE_API_PROPERTY("test", Test);
NODE_API_CALL(env, napi_define_properties(env, exports, 1, &desc));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'test_worker_buffer_callback.c' ]
}
]
}

View File

@@ -0,0 +1,17 @@
'use strict';
const common = require('../../common');
const path = require('path');
const assert = require('assert');
const { Worker } = require('worker_threads');
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
const { getFreeCallCount } = require(binding);
// Test that buffers allocated with a free callback through our APIs are
// released when a Worker owning it exits.
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
assert.strictEqual(getFreeCallCount(), 0);
w.on('exit', common.mustCall(() => {
assert.strictEqual(getFreeCallCount(), 1);
}));

View File

@@ -0,0 +1,18 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const { MessageChannel } = require('worker_threads');
const { buffer } = require(`./build/${common.buildType}/binding`);
// Test that buffers allocated with a free callback through our APIs are not
// transferred.
const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength;
assert.throws(() => port1.postMessage(buffer, [buffer]), {
code: 25,
name: 'DataCloneError',
});
assert.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0);

View File

@@ -0,0 +1,46 @@
#include <stdio.h>
#include <stdlib.h>
#include <node_api.h>
#include <assert.h>
#include "../../js-native-api/common.h"
uint32_t free_call_count = 0;
napi_value GetFreeCallCount(napi_env env, napi_callback_info info) {
napi_value value;
NODE_API_CALL(env, napi_create_uint32(env, free_call_count, &value));
return value;
}
static void finalize_cb(napi_env env, void* finalize_data, void* hint) {
free(finalize_data);
free_call_count++;
}
NAPI_MODULE_INIT() {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("getFreeCallCount", GetFreeCallCount)
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
// This is a slight variation on the non-N-API test: We create an ArrayBuffer
// rather than a Node.js Buffer, since testing the latter would only test
// the same code paths and not the ones specific to N-API.
napi_value buffer;
char* data = malloc(sizeof(char));
NODE_API_CALL(env, napi_create_external_arraybuffer(
env,
data,
sizeof(char),
finalize_cb,
NULL,
&buffer));
NODE_API_CALL(env, napi_set_named_property(env, exports, "buffer", buffer));
return exports;
}

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_worker_terminate",
"sources": [ "test_worker_terminate.c" ]
}
]
}

View File

@@ -0,0 +1,28 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const { Worker, isMainThread, workerData } = require('worker_threads');
if (isMainThread) {
// Load the addon in the main thread first.
// This checks that N-API addons can be loaded from multiple contexts
// when they are not loaded through NAPI_MODULE().
require(`./build/${common.buildType}/test_worker_terminate`);
const counter = new Int32Array(new SharedArrayBuffer(4));
const worker = new Worker(__filename, { workerData: { counter } });
worker.on('exit', common.mustCall(() => {
assert.strictEqual(counter[0], 1);
}));
worker.on('error', common.mustNotCall());
} else {
const { Test } = require(`./build/${common.buildType}/test_worker_terminate`);
const { counter } = workerData;
// Test() tries to call a function and asserts it fails because of a
// pending termination exception.
Test(() => {
Atomics.add(counter, 0, 1);
process.exit();
});
}

View File

@@ -0,0 +1,39 @@
#include <stdio.h>
#include <node_api.h>
#include <assert.h>
#include "../../js-native-api/common.h"
napi_value Test(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value recv;
napi_value argv[1];
napi_status status;
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &recv, NULL));
NODE_API_ASSERT(env, argc >= 1, "Not enough arguments, expected 1.");
napi_valuetype t;
NODE_API_CALL(env, napi_typeof(env, argv[0], &t));
NODE_API_ASSERT(env, t == napi_function,
"Wrong first argument, function expected.");
status = napi_call_function(env, recv, argv[0], 0, NULL, NULL);
assert(status == napi_pending_exception);
return NULL;
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("Test", Test)
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
// Do not start using NAPI_MODULE_INIT() here, so that we can test
// compatibility of Workers with NAPI_MODULE().
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "test_worker_terminate_finalization",
"sources": [ "test_worker_terminate_finalization.c" ]
}
]
}

View File

@@ -0,0 +1,23 @@
'use strict';
const common = require('../../common');
// Refs: https://github.com/nodejs/node/issues/34731
// Refs: https://github.com/nodejs/node/pull/35777
// Refs: https://github.com/nodejs/node/issues/35778
const { Worker, isMainThread } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('error', common.mustNotCall());
} else {
const { Test } =
require(`./build/${common.buildType}/test_worker_terminate_finalization`);
// Spin up thread and call add-on create the right sequence
// of rerences to hit the case reported in
// https://github.com/nodejs/node-addon-api/issues/722
// will crash if run under debug and its not possible to
// create object in the specific finalizer
Test(new Object());
}

View File

@@ -0,0 +1,51 @@
#include <stdio.h>
#include <node_api.h>
#include <assert.h>
#include <stdlib.h>
#include "../../js-native-api/common.h"
#define BUFFER_SIZE 4
int wrappedNativeData;
napi_ref ref;
void WrapFinalizer(napi_env env, void* data, void* hint) {
uint32_t count;
NODE_API_CALL_RETURN_VOID(env, napi_reference_unref(env, ref, &count));
NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, ref));
}
void BufferFinalizer(napi_env env, void* data, void* hint) {
free(hint);
}
napi_value Test(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value argv[1];
napi_value result;
void* bufferData = malloc(BUFFER_SIZE);
NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
NODE_API_CALL(env,
napi_create_external_buffer(
env, BUFFER_SIZE, bufferData, BufferFinalizer, bufferData, &result));
NODE_API_CALL(env, napi_create_reference(env, result, 1, &ref));
NODE_API_CALL(env,
napi_wrap(
env, argv[0], (void*) &wrappedNativeData, WrapFinalizer, NULL, NULL));
return NULL;
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
DECLARE_NODE_API_PROPERTY("Test", Test)
};
NODE_API_CALL(env, napi_define_properties(
env, exports, sizeof(properties) / sizeof(*properties), properties));
return exports;
}
// Do not start using NAPI_MODULE_INIT() here, so that we can test
// compatibility of Workers with NAPI_MODULE().
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

View File

@@ -0,0 +1,6 @@
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import testpy
def GetConfiguration(context, root):
return testpy.AddonTestConfiguration(context, root, 'node-api')