n-api: add APIs for per-instance state management

Adds `napi_set_instance_data()` and `napi_get_instance_data()`, which
allow native addons to store their data on and retrieve their data from
`napi_env`. `napi_set_instance_data()` accepts a finalizer which is
called when the `node::Environment()` is destroyed.

This entails rendering the `napi_env` local to each add-on.

Fixes: https://github.com/nodejs/abi-stable-node/issues/378
PR-URL: https://github.com/nodejs/node/pull/28682
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
This commit is contained in:
Gabriel Schulhof 2019-07-14 17:21:13 -07:00
parent e9ea8eaf7a
commit 5030e81ce3
17 changed files with 630 additions and 133 deletions

View File

@ -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
<!-- YAML
added: REPLACEME
-->
```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
<!-- YAML
added: REPLACEME
-->
```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

View File

@ -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") \

View File

@ -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

View File

@ -305,12 +305,10 @@ class Reference : private Finalizer {
static void SecondPassCallback(const v8::WeakCallbackInfo<Reference>& 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);
}

View File

@ -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<v8::Context> 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<v8::Context> context_persistent;
@ -25,11 +33,37 @@ struct napi_env__ {
virtual bool can_call_into_js() const { return true; }
template <typename T, typename U>
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 <typename T>
void CallIntoModuleThrow(T&& call) {
CallIntoModule(call, [&](napi_env env, v8::Local<v8::Value> value) {
env->isolate->ThrowException(value);
});
}
v8impl::Persistent<v8::Value> 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 <typename T, typename U>
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 <typename T>
void NapiCallIntoModuleThrow(napi_env env, T&& call) {
NapiCallIntoModule(env, call, [&](v8::Local<v8::Value> value) {
env->isolate->ThrowException(value);
});
}
namespace v8impl {
//=== Conversion between V8 Handles and napi_value ========================

View File

@ -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<v8::Context> context) {
static inline napi_env NewEnv(v8::Local<v8::Context> 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<node_napi_env>(value.As<v8::External>()->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<napi_env>(arg)->Unref();
},
static_cast<void*>(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<napi_env>(arg)->Unref();
},
static_cast<void*>(result));
return result;
}
@ -325,7 +303,7 @@ class ThreadSafeFunction : public node::AsyncResource {
v8::Local<v8::Function>::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<v8::Object> 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<v8::Value> local_err) {
_env->CallIntoModule([&](napi_env env) {
_complete(env, ConvertUVErrorCode(status), _data);
}, [](napi_env env, v8::Local<v8::Value> 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.)

28
test/common/require-as.js Normal file
View File

@ -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);
}

View File

@ -0,0 +1,11 @@
{
"targets": [
{
"target_name": "test_instance_data",
"sources": [
"../entry_point.c",
"test_instance_data.c"
]
}
]
}

View File

@ -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'));
}

View File

@ -0,0 +1,97 @@
#include <stdio.h>
#include <stdlib.h>
#define NAPI_EXPERIMENTAL
#include <js_native_api.h>
#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

View File

@ -1,12 +0,0 @@
{
"targets": [
{
"target_name": "store_env",
"sources": [ "store_env.c" ]
},
{
"target_name": "compare_env",
"sources": [ "compare_env.c" ]
}
]
}

View File

@ -1,23 +0,0 @@
#include <node_api.h>
#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)

View File

@ -1,10 +0,0 @@
#include <node_api.h>
#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)

View File

@ -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);

View File

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

View File

@ -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');
}

View File

@ -0,0 +1,236 @@
#include <stdlib.h>
#include <uv.h>
#define NAPI_EXPERIMENTAL
#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;
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)