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:
parent
e9ea8eaf7a
commit
5030e81ce3
@ -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
|
||||
|
@ -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") \
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 ========================
|
||||
|
@ -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
28
test/common/require-as.js
Normal 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);
|
||||
}
|
11
test/js-native-api/test_instance_data/binding.gyp
Normal file
11
test/js-native-api/test_instance_data/binding.gyp
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_instance_data",
|
||||
"sources": [
|
||||
"../entry_point.c",
|
||||
"test_instance_data.c"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
40
test/js-native-api/test_instance_data/test.js
Normal file
40
test/js-native-api/test_instance_data/test.js
Normal 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'));
|
||||
}
|
97
test/js-native-api/test_instance_data/test_instance_data.c
Normal file
97
test/js-native-api/test_instance_data/test_instance_data.c
Normal 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
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "store_env",
|
||||
"sources": [ "store_env.c" ]
|
||||
},
|
||||
{
|
||||
"target_name": "compare_env",
|
||||
"sources": [ "compare_env.c" ]
|
||||
}
|
||||
]
|
||||
}
|
@ -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)
|
@ -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)
|
@ -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);
|
10
test/node-api/test_instance_data/binding.gyp
Normal file
10
test/node-api/test_instance_data/binding.gyp
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "test_instance_data",
|
||||
"sources": [
|
||||
"test_instance_data.c"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
35
test/node-api/test_instance_data/test.js
Normal file
35
test/node-api/test_instance_data/test.js
Normal 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');
|
||||
}
|
236
test/node-api/test_instance_data/test_instance_data.c
Normal file
236
test/node-api/test_instance_data/test_instance_data.c
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user