diff --git a/doc/api/n-api.md b/doc/api/n-api.md index e4b9e4c896d194..841e756e82726a 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -251,6 +251,82 @@ NAPI_MODULE_INIT() { } ``` +## Environment Life Cycle APIs + +> Stability: 1 - Experimental + +[Section 8.7][] of the [ECMAScript Language Specification][] defines the concept +of an "Agent" as a self-contained environment in which JavaScript code runs. +Multiple such Agents may be started and terminated either concurrently or in +sequence by the process. + +A Node.js environment corresponds to an ECMAScript Agent. In the main process, +an environment is created at startup, and additional environments can be created +on separate threads to serve as [worker threads][]. When Node.js is embedded in +another application, the main thread of the application may also construct and +destroy a Node.js environment multiple times during the life cycle of the +application process such that each Node.js environment created by the +application may, in turn, during its life cycle create and destroy additional +environments as worker threads. + +From the perspective of a native addon this means that the bindings it provides +may be called multiple times, from multiple contexts, and even concurrently from +multiple threads. + +Native addons may need to allocate global state of which they make use during +their entire life cycle such that the state must be unique to each instance of +the addon. + +To this env, N-API provides a way to allocate data such that its life cycle is +tied to the life cycle of the Agent. + +### napi_set_instance_data + + +```C +napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint); +``` + +- `[in] env`: The environment that the N-API call is invoked under. +- `[in] data`: The data item to make available to bindings of this instance. +- `[in] finalize_cb`: The function to call when the environment is being torn +down. The function receives `data` so that it might free it. +- `[in] finalize_hint`: Optional hint to pass to the finalize callback +during collection. + +Returns `napi_ok` if the API succeeded. + +This API associates `data` with the currently running Agent. `data` can later +be retrieved using `napi_get_instance_data()`. Any existing data associated with +the currently running Agent which was set by means of a previous call to +`napi_set_instance_data()` will be overwritten. If a `finalize_cb` was provided +by the previous call, it will not be called. + +### napi_get_instance_data + + +```C +napi_status napi_get_instance_data(napi_env env, + void** data); +``` + +- `[in] env`: The environment that the N-API call is invoked under. +- `[out] data`: The data item that was previously associated with the currently +running Agent by a call to `napi_set_instance_data()`. + +Returns `napi_ok` if the API succeeded. + +This API retrieves data that was previously associated with the currently +running Agent via `napi_set_instance_data()`. If no data is set, the call will +succeed and `data` will be set to `NULL`. + ## Basic N-API Data Types N-API exposes the following fundamental datatypes as abstractions that are @@ -4876,6 +4952,7 @@ This API may only be called from the main thread. [Section 6.1.4]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-string-type [Section 6.1.6]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type [Section 6.1.7.1]: https://tc39.github.io/ecma262/#table-2 +[Section 8.7]: https://tc39.es/ecma262/#sec-agents [Section 9.1.6]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc [Working with JavaScript Functions]: #n_api_working_with_javascript_functions [Working with JavaScript Properties]: #n_api_working_with_javascript_properties @@ -4930,3 +5007,4 @@ This API may only be called from the main thread. [`uv_unref`]: http://docs.libuv.org/en/v1.x/handle.html#c.uv_unref [async_hooks `type`]: async_hooks.html#async_hooks_type [context-aware addons]: addons.html#addons_context_aware_addons +[worker threads]: https://nodejs.org/api/worker_threads.html diff --git a/src/env.h b/src/env.h index 4e598a42e5eae4..4eb6575b8ff60b 100644 --- a/src/env.h +++ b/src/env.h @@ -152,7 +152,6 @@ constexpr size_t kFsStatsBufferLength = V(contextify_context_private_symbol, "node:contextify:context") \ V(contextify_global_private_symbol, "node:contextify:global") \ V(decorated_private_symbol, "node:decorated") \ - V(napi_env, "node:napi:env") \ V(napi_wrapper, "node:napi:wrapper") \ V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \ diff --git a/src/js_native_api.h b/src/js_native_api.h index 4f9345a4abf134..22cbf4ee30612c 100644 --- a/src/js_native_api.h +++ b/src/js_native_api.h @@ -499,6 +499,15 @@ NAPI_EXTERN napi_status napi_add_finalizer(napi_env env, napi_finalize finalize_cb, void* finalize_hint, napi_ref* result); + +// Instance data +NAPI_EXTERN napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint); + +NAPI_EXTERN napi_status napi_get_instance_data(napi_env env, + void** data); #endif // NAPI_EXPERIMENTAL EXTERN_C_END diff --git a/src/js_native_api_v8.cc b/src/js_native_api_v8.cc index 5dac55cc82a4fc..fd8728d30f6cd9 100644 --- a/src/js_native_api_v8.cc +++ b/src/js_native_api_v8.cc @@ -305,12 +305,10 @@ class Reference : private Finalizer { static void SecondPassCallback(const v8::WeakCallbackInfo& data) { Reference* reference = data.GetParameter(); - napi_env env = reference->_env; - if (reference->_finalize_callback != nullptr) { - NapiCallIntoModuleThrow(env, [&]() { + reference->_env->CallIntoModuleThrow([&](napi_env env) { reference->_finalize_callback( - reference->_env, + env, reference->_finalize_data, reference->_finalize_hint); }); @@ -452,7 +450,9 @@ class CallbackWrapperBase : public CallbackWrapper { napi_callback cb = _bundle->*FunctionField; napi_value result; - NapiCallIntoModuleThrow(env, [&]() { result = cb(env, cbinfo_wrapper); }); + env->CallIntoModuleThrow([&](napi_env env) { + result = cb(env, cbinfo_wrapper); + }); if (result != nullptr) { this->SetReturnValue(result); @@ -2986,3 +2986,26 @@ napi_status napi_adjust_external_memory(napi_env env, return napi_clear_last_error(env); } + +napi_status napi_set_instance_data(napi_env env, + void* data, + napi_finalize finalize_cb, + void* finalize_hint) { + CHECK_ENV(env); + + env->instance_data.data = data; + env->instance_data.finalize_cb = finalize_cb; + env->instance_data.hint = finalize_hint; + + return napi_clear_last_error(env); +} + +napi_status napi_get_instance_data(napi_env env, + void** data) { + CHECK_ENV(env); + CHECK_ARG(env, data); + + *data = env->instance_data.data; + + return napi_clear_last_error(env); +} diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index 88c59869a07a07..506e693f821227 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -6,13 +6,21 @@ #include "js_native_api_types.h" #include "js_native_api_v8_internals.h" +static napi_status napi_clear_last_error(napi_env env); + struct napi_env__ { explicit napi_env__(v8::Local context) : isolate(context->GetIsolate()), context_persistent(isolate, context) { CHECK_EQ(isolate, context->GetIsolate()); } - virtual ~napi_env__() = default; + virtual ~napi_env__() { + if (instance_data.finalize_cb != nullptr) { + CallIntoModuleThrow([&](napi_env env) { + instance_data.finalize_cb(env, instance_data.data, instance_data.hint); + }); + } + } v8::Isolate* const isolate; // Shortcut for context()->GetIsolate() v8impl::Persistent context_persistent; @@ -25,11 +33,37 @@ struct napi_env__ { virtual bool can_call_into_js() const { return true; } + template + void CallIntoModule(T&& call, U&& handle_exception) { + int open_handle_scopes_before = open_handle_scopes; + int open_callback_scopes_before = open_callback_scopes; + napi_clear_last_error(this); + call(this); + CHECK_EQ(open_handle_scopes, open_handle_scopes_before); + CHECK_EQ(open_callback_scopes, open_callback_scopes_before); + if (!last_exception.IsEmpty()) { + handle_exception(this, last_exception.Get(this->isolate)); + last_exception.Reset(); + } + } + + template + void CallIntoModuleThrow(T&& call) { + CallIntoModule(call, [&](napi_env env, v8::Local value) { + env->isolate->ThrowException(value); + }); + } + v8impl::Persistent last_exception; napi_extended_error_info last_error; int open_handle_scopes = 0; int open_callback_scopes = 0; int refs = 1; + struct { + void* data = nullptr; + void* hint = nullptr; + napi_finalize finalize_cb = nullptr; + } instance_data; }; static inline napi_status napi_clear_last_error(napi_env env) { @@ -114,27 +148,6 @@ napi_status napi_set_last_error(napi_env env, napi_status error_code, } \ } while (0) -template -void NapiCallIntoModule(napi_env env, T&& call, U&& handle_exception) { - int open_handle_scopes = env->open_handle_scopes; - int open_callback_scopes = env->open_callback_scopes; - napi_clear_last_error(env); - call(); - CHECK_EQ(env->open_handle_scopes, open_handle_scopes); - CHECK_EQ(env->open_callback_scopes, open_callback_scopes); - if (!env->last_exception.IsEmpty()) { - handle_exception(env->last_exception.Get(env->isolate)); - env->last_exception.Reset(); - } -} - -template -void NapiCallIntoModuleThrow(napi_env env, T&& call) { - NapiCallIntoModule(env, call, [&](v8::Local value) { - env->isolate->ThrowException(value); - }); -} - namespace v8impl { //=== Conversion between V8 Handles and napi_value ======================== diff --git a/src/node_api.cc b/src/node_api.cc index a9f26e551db03d..49472930634938 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -46,9 +46,9 @@ class BufferFinalizer : private Finalizer { v8::HandleScope handle_scope(finalizer->_env->isolate); v8::Context::Scope context_scope(finalizer->_env->context()); - NapiCallIntoModuleThrow(finalizer->_env, [&]() { + finalizer->_env->CallIntoModuleThrow([&](napi_env env) { finalizer->_finalize_callback( - finalizer->_env, + env, finalizer->_finalize_data, finalizer->_finalize_hint); }); @@ -59,44 +59,22 @@ class BufferFinalizer : private Finalizer { } }; -static inline napi_env GetEnv(v8::Local context) { +static inline napi_env NewEnv(v8::Local context) { node_napi_env result; - auto isolate = context->GetIsolate(); - auto global = context->Global(); - - // In the case of the string for which we grab the private and the value of - // the private on the global object we can call .ToLocalChecked() directly - // because we need to stop hard if either of them is empty. - // - // Re https://github.com/nodejs/node/pull/14217#discussion_r128775149 - auto value = global->GetPrivate(context, NAPI_PRIVATE_KEY(context, env)) - .ToLocalChecked(); - - if (value->IsExternal()) { - result = static_cast(value.As()->Value()); - } else { - result = new node_napi_env__(context); - auto external = v8::External::New(isolate, result); - - // We must also stop hard if the result of assigning the env to the global - // is either nothing or false. - CHECK(global->SetPrivate(context, NAPI_PRIVATE_KEY(context, env), external) - .FromJust()); - - // TODO(addaleax): There was previously code that tried to delete the - // napi_env when its v8::Context was garbage collected; - // However, as long as N-API addons using this napi_env are in place, - // the Context needs to be accessible and alive. - // Ideally, we'd want an on-addon-unload hook that takes care of this - // once all N-API addons using this napi_env are unloaded. - // For now, a per-Environment cleanup hook is the best we can do. - result->node_env()->AddCleanupHook( - [](void* arg) { - static_cast(arg)->Unref(); - }, - static_cast(result)); - } + result = new node_napi_env__(context); + // TODO(addaleax): There was previously code that tried to delete the + // napi_env when its v8::Context was garbage collected; + // However, as long as N-API addons using this napi_env are in place, + // the Context needs to be accessible and alive. + // Ideally, we'd want an on-addon-unload hook that takes care of this + // once all N-API addons using this napi_env are unloaded. + // For now, a per-Environment cleanup hook is the best we can do. + result->node_env()->AddCleanupHook( + [](void* arg) { + static_cast(arg)->Unref(); + }, + static_cast(result)); return result; } @@ -325,7 +303,7 @@ class ThreadSafeFunction : public node::AsyncResource { v8::Local::New(env->isolate, ref); js_callback = v8impl::JsValueFromV8LocalValue(js_cb); } - NapiCallIntoModuleThrow(env, [&]() { + env->CallIntoModuleThrow([&](napi_env env) { call_js_cb(env, js_callback, context, data); }); } @@ -346,7 +324,7 @@ class ThreadSafeFunction : public node::AsyncResource { v8::HandleScope scope(env->isolate); if (finalize_cb) { CallbackScope cb_scope(this); - NapiCallIntoModuleThrow(env, [&]() { + env->CallIntoModuleThrow([&](napi_env env) { finalize_cb(env, finalize_data, context); }); } @@ -481,10 +459,10 @@ void napi_module_register_by_symbol(v8::Local exports, // Create a new napi_env for this module or reference one if a pre-existing // one is found. - napi_env env = v8impl::GetEnv(context); + napi_env env = v8impl::NewEnv(context); napi_value _exports; - NapiCallIntoModuleThrow(env, [&]() { + env->CallIntoModuleThrow([&](napi_env env) { _exports = init(env, v8impl::JsValueFromV8LocalValue(exports)); }); @@ -889,15 +867,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork { CallbackScope callback_scope(this); - // We have to back up the env here because the `NAPI_CALL_INTO_MODULE` macro - // makes use of it after the call into the module completes, but the module - // may have deallocated **this**, and along with it the place where _env is - // stored. - napi_env env = _env; - - NapiCallIntoModule(env, [&]() { - _complete(_env, ConvertUVErrorCode(status), _data); - }, [env](v8::Local local_err) { + _env->CallIntoModule([&](napi_env env) { + _complete(env, ConvertUVErrorCode(status), _data); + }, [](napi_env env, v8::Local local_err) { // If there was an unhandled exception in the complete callback, // report it as a fatal exception. (There is no JavaScript on the // callstack that can possibly handle it.) diff --git a/test/common/require-as.js b/test/common/require-as.js new file mode 100644 index 00000000000000..f55c1a67c49ec2 --- /dev/null +++ b/test/common/require-as.js @@ -0,0 +1,28 @@ +/* eslint-disable node-core/require-common-first, node-core/required-modules */ +'use strict'; + +if (module.parent) { + const { spawnSync } = require('child_process'); + + function runModuleAs(filename, flags, spawnOptions, role) { + return spawnSync(process.execPath, + [...flags, __filename, role, filename], spawnOptions); + } + + module.exports = runModuleAs; + return; +} + +const { Worker, isMainThread, workerData } = require('worker_threads'); + +if (isMainThread) { + if (process.argv[2] === 'worker') { + new Worker(__filename, { + workerData: process.argv[3] + }); + return; + } + require(process.argv[3]); +} else { + require(workerData); +} diff --git a/test/js-native-api/test_instance_data/binding.gyp b/test/js-native-api/test_instance_data/binding.gyp new file mode 100644 index 00000000000000..5b2d4ff328b4fa --- /dev/null +++ b/test/js-native-api/test_instance_data/binding.gyp @@ -0,0 +1,11 @@ +{ + "targets": [ + { + "target_name": "test_instance_data", + "sources": [ + "../entry_point.c", + "test_instance_data.c" + ] + } + ] +} diff --git a/test/js-native-api/test_instance_data/test.js b/test/js-native-api/test_instance_data/test.js new file mode 100644 index 00000000000000..986f644fd226b9 --- /dev/null +++ b/test/js-native-api/test_instance_data/test.js @@ -0,0 +1,40 @@ +'use strict'; +// Test API calls for instance data. + +const common = require('../../common'); +const assert = require('assert'); + +if (module.parent) { + // When required as a module, run the tests. + const test_instance_data = + require(`./build/${common.buildType}/test_instance_data`); + + // Print to stdout when the environment deletes the instance data. This output + // is checked by the parent process. + test_instance_data.setPrintOnDelete(); + + // Test that instance data can be accessed from a binding. + assert.strictEqual(test_instance_data.increment(), 42); + + // Test that the instance data can be accessed from a finalizer. + test_instance_data.objectWithFinalizer(common.mustCall()); + global.gc(); +} else { + // When launched as a script, run tests in either a child process or in a + // worker thread. + const requireAs = require('../../common/require-as'); + const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] }; + + function checkOutput(child) { + assert.strictEqual(child.status, 0); + assert.strictEqual( + (child.stdout.toString().split(/\r\n?|\n/) || [])[0], + 'deleting addon data'); + } + + // Run tests in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'child')); + + // Run tests in a worker thread in a child process. + checkOutput(requireAs(__filename, ['--expose-gc'], runOptions, 'worker')); +} diff --git a/test/js-native-api/test_instance_data/test_instance_data.c b/test/js-native-api/test_instance_data/test_instance_data.c new file mode 100644 index 00000000000000..a64ebec0c1aba9 --- /dev/null +++ b/test/js-native-api/test_instance_data/test_instance_data.c @@ -0,0 +1,97 @@ +#include +#include +#define NAPI_EXPERIMENTAL +#include +#include "../common.h" + +typedef struct { + size_t value; + bool print; + napi_ref js_cb_ref; +} AddonData; + +static napi_value Increment(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result; + + NAPI_CALL(env, napi_get_instance_data(env, (void**)&data)); + NAPI_CALL(env, napi_create_uint32(env, ++data->value, &result)); + + return result; +} + +static void DeleteAddonData(napi_env env, void* raw_data, void* hint) { + AddonData* data = raw_data; + if (data->print) { + printf("deleting addon data\n"); + } + if (data->js_cb_ref != NULL) { + NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + } + free(data); +} + +static napi_value SetPrintOnDelete(napi_env env, napi_callback_info info) { + AddonData* data; + + NAPI_CALL(env, napi_get_instance_data(env, (void**)&data)); + data->print = true; + + return NULL; +} + +static void TestFinalizer(napi_env env, void* raw_data, void* hint) { + (void) raw_data; + (void) hint; + + AddonData* data; + NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); + napi_value js_cb, undefined; + NAPI_CALL_RETURN_VOID(env, + napi_get_reference_value(env, data->js_cb_ref, &js_cb)); + NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NAPI_CALL_RETURN_VOID(env, + napi_call_function(env, undefined, js_cb, 0, NULL, NULL)); + NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + data->js_cb_ref = NULL; +} + +static napi_value ObjectWithFinalizer(napi_env env, napi_callback_info info) { + AddonData* data; + napi_value result, js_cb; + size_t argc = 1; + + NAPI_CALL(env, napi_get_instance_data(env, (void**)&data)); + NAPI_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL"); + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL)); + NAPI_CALL(env, napi_create_object(env, &result)); + NAPI_CALL(env, + napi_add_finalizer(env, result, NULL, TestFinalizer, NULL, NULL)); + NAPI_CALL(env, napi_create_reference(env, js_cb, 1, &data->js_cb_ref)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + AddonData* data = malloc(sizeof(*data)); + data->value = 41; + data->print = false; + data->js_cb_ref = NULL; + + NAPI_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL)); + + napi_property_descriptor props[] = { + DECLARE_NAPI_PROPERTY("increment", Increment), + DECLARE_NAPI_PROPERTY("setPrintOnDelete", SetPrintOnDelete), + DECLARE_NAPI_PROPERTY("objectWithFinalizer", ObjectWithFinalizer), + }; + + NAPI_CALL(env, napi_define_properties(env, + exports, + sizeof(props) / sizeof(*props), + props)); + + return exports; +} +EXTERN_C_END diff --git a/test/node-api/test_env_sharing/binding.gyp b/test/node-api/test_env_sharing/binding.gyp deleted file mode 100644 index 5699a8391dd347..00000000000000 --- a/test/node-api/test_env_sharing/binding.gyp +++ /dev/null @@ -1,12 +0,0 @@ -{ - "targets": [ - { - "target_name": "store_env", - "sources": [ "store_env.c" ] - }, - { - "target_name": "compare_env", - "sources": [ "compare_env.c" ] - } - ] -} diff --git a/test/node-api/test_env_sharing/compare_env.c b/test/node-api/test_env_sharing/compare_env.c deleted file mode 100644 index 0c365f7e343dca..00000000000000 --- a/test/node-api/test_env_sharing/compare_env.c +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include "../../js-native-api/common.h" - -static napi_value compare(napi_env env, napi_callback_info info) { - napi_value external; - size_t argc = 1; - void* data; - napi_value return_value; - - NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &external, NULL, NULL)); - NAPI_CALL(env, napi_get_value_external(env, external, &data)); - NAPI_CALL(env, napi_get_boolean(env, ((napi_env)data) == env, &return_value)); - - return return_value; -} - -static napi_value Init(napi_env env, napi_value exports) { - NAPI_CALL(env, napi_create_function( - env, "exports", NAPI_AUTO_LENGTH, compare, NULL, &exports)); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/node-api/test_env_sharing/store_env.c b/test/node-api/test_env_sharing/store_env.c deleted file mode 100644 index 6f59cf1fc0f2d9..00000000000000 --- a/test/node-api/test_env_sharing/store_env.c +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include "../../js-native-api/common.h" - -static napi_value Init(napi_env env, napi_value exports) { - napi_value external; - NAPI_CALL(env, napi_create_external(env, env, NULL, NULL, &external)); - return external; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/node-api/test_env_sharing/test.js b/test/node-api/test_env_sharing/test.js deleted file mode 100644 index 0a3507177d56fa..00000000000000 --- a/test/node-api/test_env_sharing/test.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const common = require('../../common'); -const storeEnv = require(`./build/${common.buildType}/store_env`); -const compareEnv = require(`./build/${common.buildType}/compare_env`); -const assert = require('assert'); - -// N-API environment pointers in two different modules have the same value -assert.strictEqual(compareEnv(storeEnv), true); diff --git a/test/node-api/test_instance_data/binding.gyp b/test/node-api/test_instance_data/binding.gyp new file mode 100644 index 00000000000000..0d55905e9e7236 --- /dev/null +++ b/test/node-api/test_instance_data/binding.gyp @@ -0,0 +1,10 @@ +{ + "targets": [ + { + "target_name": "test_instance_data", + "sources": [ + "test_instance_data.c" + ] + } + ] +} diff --git a/test/node-api/test_instance_data/test.js b/test/node-api/test_instance_data/test.js new file mode 100644 index 00000000000000..969c164afdad58 --- /dev/null +++ b/test/node-api/test_instance_data/test.js @@ -0,0 +1,35 @@ +'use strict'; +// Test API calls for instance data. + +const common = require('../../common'); + +if (module.parent) { + // 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 requireAs = require('../../common/require-as'); + const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] }; + + // 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'); +} diff --git a/test/node-api/test_instance_data/test_instance_data.c b/test/node-api/test_instance_data/test_instance_data.c new file mode 100644 index 00000000000000..1a814e91c0665b --- /dev/null +++ b/test/node-api/test_instance_data/test_instance_data.c @@ -0,0 +1,236 @@ +#include +#include +#define NAPI_EXPERIMENTAL +#include +#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; + NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); + optional_ref = &data->js_cb_ref; + } + + NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env, + *optional_ref, + &js_cb)); + NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined)); + NAPI_CALL_RETURN_VOID(env, napi_call_function(env, + undefined, + js_cb, + 0, + NULL, + NULL)); + NAPI_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; + + NAPI_CALL_BASE(env, napi_get_instance_data(env, (void**)&data), false); + NAPI_ASSERT_BASE(env, + data->js_cb_ref == NULL, + "reference must be NULL", + false); + NAPI_CALL_BASE(env, + napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL), + false); + NAPI_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; + + NAPI_CALL(env, napi_create_string_utf8(env, + "AsyncIncrement", + NAPI_AUTO_LENGTH, + &resource_name)); + NAPI_CALL(env, napi_create_async_work(env, + NULL, + resource_name, + AsyncWorkCbExecute, + AsyncWorkCbComplete, + NULL, + &work)); + NAPI_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)) { + NAPI_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; + NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data)); + NAPI_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; +} + +// Ths 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; + + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NAPI_CALL(env, napi_get_instance_data(env, (void**)&data)); + NAPI_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL"); + NAPI_ASSERT(env, + data->js_tsfn_finalizer_ref == NULL, + "tsfn finalizer reference must be NULL"); + NAPI_CALL(env, napi_create_reference(env, argv[0], 1, &data->js_cb_ref)); + NAPI_CALL(env, napi_create_reference(env, + argv[1], + 1, + &data->js_tsfn_finalizer_ref)); + NAPI_CALL(env, napi_create_string_utf8(env, + "TSFN instance data test", + NAPI_AUTO_LENGTH, + &resource_name)); + NAPI_CALL(env, napi_create_threadsafe_function(env, + NULL, + NULL, + resource_name, + 0, + 1, + NULL, + FinalizeThreadsafeFunction, + NULL, + ThreadsafeFunctionCallJS, + &data->tsfn)); + NAPI_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) { + NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref)); + } + if (data->js_tsfn_finalizer_ref) { + NAPI_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; + + NAPI_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL)); + + napi_property_descriptor props[] = { + DECLARE_NAPI_PROPERTY("asyncWorkCallback", AsyncWorkCallback), + DECLARE_NAPI_PROPERTY("testBufferFinalizer", TestBufferFinalizer), + DECLARE_NAPI_PROPERTY("testThreadsafeFunction", TestThreadsafeFunction), + }; + + NAPI_CALL(env, napi_define_properties(env, + exports, + sizeof(props) / sizeof(*props), + props)); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)