Skip to content

Commit f29fb14

Browse files
Gabriel SchulhofBethGriggs
Gabriel Schulhof
authored andcommittedFeb 25, 2020
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: nodejs/abi-stable-node#378 PR-URL: #28682 Backport-PR-URL: #30537 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
1 parent 20177b9 commit f29fb14

File tree

13 files changed

+628
-100
lines changed

13 files changed

+628
-100
lines changed
 

‎doc/api/n-api.md

+78
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,82 @@ available to the module code.
157157

158158
\* Indicates that the N-API version was released as experimental
159159

160+
## Environment Life Cycle APIs
161+
162+
> Stability: 1 - Experimental
163+
164+
[Section 8.7][] of the [ECMAScript Language Specification][] defines the concept
165+
of an "Agent" as a self-contained environment in which JavaScript code runs.
166+
Multiple such Agents may be started and terminated either concurrently or in
167+
sequence by the process.
168+
169+
A Node.js environment corresponds to an ECMAScript Agent. In the main process,
170+
an environment is created at startup, and additional environments can be created
171+
on separate threads to serve as [worker threads][]. When Node.js is embedded in
172+
another application, the main thread of the application may also construct and
173+
destroy a Node.js environment multiple times during the life cycle of the
174+
application process such that each Node.js environment created by the
175+
application may, in turn, during its life cycle create and destroy additional
176+
environments as worker threads.
177+
178+
From the perspective of a native addon this means that the bindings it provides
179+
may be called multiple times, from multiple contexts, and even concurrently from
180+
multiple threads.
181+
182+
Native addons may need to allocate global state of which they make use during
183+
their entire life cycle such that the state must be unique to each instance of
184+
the addon.
185+
186+
To this env, N-API provides a way to allocate data such that its life cycle is
187+
tied to the life cycle of the Agent.
188+
189+
### napi_set_instance_data
190+
<!-- YAML
191+
added: REPLACEME
192+
-->
193+
194+
```C
195+
napi_status napi_set_instance_data(napi_env env,
196+
void* data,
197+
napi_finalize finalize_cb,
198+
void* finalize_hint);
199+
```
200+
201+
- `[in] env`: The environment that the N-API call is invoked under.
202+
- `[in] data`: The data item to make available to bindings of this instance.
203+
- `[in] finalize_cb`: The function to call when the environment is being torn
204+
down. The function receives `data` so that it might free it.
205+
- `[in] finalize_hint`: Optional hint to pass to the finalize callback
206+
during collection.
207+
208+
Returns `napi_ok` if the API succeeded.
209+
210+
This API associates `data` with the currently running Agent. `data` can later
211+
be retrieved using `napi_get_instance_data()`. Any existing data associated with
212+
the currently running Agent which was set by means of a previous call to
213+
`napi_set_instance_data()` will be overwritten. If a `finalize_cb` was provided
214+
by the previous call, it will not be called.
215+
216+
### napi_get_instance_data
217+
<!-- YAML
218+
added: REPLACEME
219+
-->
220+
221+
```C
222+
napi_status napi_get_instance_data(napi_env env,
223+
void** data);
224+
```
225+
226+
- `[in] env`: The environment that the N-API call is invoked under.
227+
- `[out] data`: The data item that was previously associated with the currently
228+
running Agent by a call to `napi_set_instance_data()`.
229+
230+
Returns `napi_ok` if the API succeeded.
231+
232+
This API retrieves data that was previously associated with the currently
233+
running Agent via `napi_set_instance_data()`. If no data is set, the call will
234+
succeed and `data` will be set to `NULL`.
235+
160236
## Basic N-API Data Types
161237

162238
N-API exposes the following fundamental datatypes as abstractions that are
@@ -4735,6 +4811,7 @@ This API may only be called from the main thread.
47354811
[Section 6.1.4]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-string-type
47364812
[Section 6.1.6]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type
47374813
[Section 6.1.7.1]: https://tc39.github.io/ecma262/#table-2
4814+
[Section 8.7]: https://tc39.es/ecma262/#sec-agents
47384815
[Section 9.1.6]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc
47394816
[Working with JavaScript Functions]: #n_api_working_with_javascript_functions
47404817
[Working with JavaScript Properties]: #n_api_working_with_javascript_properties
@@ -4789,3 +4866,4 @@ This API may only be called from the main thread.
47894866
[`uv_unref`]: http://docs.libuv.org/en/v1.x/handle.html#c.uv_unref
47904867
[async_hooks `type`]: async_hooks.html#async_hooks_type
47914868
[context-aware addons]: addons.html#addons_context_aware_addons
4869+
[worker threads]: https://nodejs.org/api/worker_threads.html

‎src/env.h

-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ struct PackageConfig {
108108
V(contextify_context_private_symbol, "node:contextify:context") \
109109
V(contextify_global_private_symbol, "node:contextify:global") \
110110
V(decorated_private_symbol, "node:decorated") \
111-
V(napi_env, "node:napi:env") \
112111
V(napi_wrapper, "node:napi:wrapper") \
113112
V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \
114113

‎src/node_api.cc

+83-78
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ struct napi_env__ {
2525
CHECK_NOT_NULL(node_env());
2626
}
2727

28+
virtual ~napi_env__() {
29+
if (instance_data.finalize_cb != nullptr) {
30+
CallIntoModuleThrow([&](napi_env env) {
31+
instance_data.finalize_cb(env, instance_data.data, instance_data.hint);
32+
});
33+
}
34+
}
35+
2836
v8::Isolate* const isolate; // Shortcut for context()->GetIsolate()
2937
node::Persistent<v8::Context> context_persistent;
3038

@@ -39,11 +47,37 @@ struct napi_env__ {
3947
inline void Ref() { refs++; }
4048
inline void Unref() { if (--refs == 0) delete this; }
4149

50+
template <typename T, typename U>
51+
void CallIntoModule(T&& call, U&& handle_exception) {
52+
int open_handle_scopes_before = open_handle_scopes;
53+
int open_callback_scopes_before = open_callback_scopes;
54+
napi_clear_last_error(this);
55+
call(this);
56+
CHECK_EQ(open_handle_scopes, open_handle_scopes_before);
57+
CHECK_EQ(open_callback_scopes, open_callback_scopes_before);
58+
if (!last_exception.IsEmpty()) {
59+
handle_exception(this, last_exception.Get(this->isolate));
60+
last_exception.Reset();
61+
}
62+
}
63+
64+
template <typename T>
65+
void CallIntoModuleThrow(T&& call) {
66+
CallIntoModule(call, [&](napi_env env, v8::Local<v8::Value> value) {
67+
env->isolate->ThrowException(value);
68+
});
69+
}
70+
4271
node::Persistent<v8::Value> last_exception;
4372
napi_extended_error_info last_error;
4473
int open_handle_scopes = 0;
4574
int open_callback_scopes = 0;
4675
int refs = 1;
76+
struct {
77+
void* data = nullptr;
78+
void* hint = nullptr;
79+
napi_finalize finalize_cb = nullptr;
80+
} instance_data;
4781
};
4882

4983
#define NAPI_PRIVATE_KEY(context, suffix) \
@@ -158,27 +192,6 @@ struct napi_env__ {
158192
(out) = v8::type::New((buffer), (byte_offset), (length)); \
159193
} while (0)
160194

161-
template <typename T, typename U>
162-
void NapiCallIntoModule(napi_env env, T&& call, U&& handle_exception) {
163-
int open_handle_scopes = env->open_handle_scopes;
164-
int open_callback_scopes = env->open_callback_scopes;
165-
napi_clear_last_error(env);
166-
call();
167-
CHECK_EQ(env->open_handle_scopes, open_handle_scopes);
168-
CHECK_EQ(env->open_callback_scopes, open_callback_scopes);
169-
if (!env->last_exception.IsEmpty()) {
170-
handle_exception(env->last_exception.Get(env->isolate));
171-
env->last_exception.Reset();
172-
}
173-
}
174-
175-
template <typename T>
176-
void NapiCallIntoModuleThrow(napi_env env, T&& call) {
177-
NapiCallIntoModule(env, call, [&](v8::Local<v8::Value> value) {
178-
env->isolate->ThrowException(value);
179-
});
180-
}
181-
182195
namespace {
183196
namespace v8impl {
184197

@@ -357,11 +370,8 @@ class Finalizer {
357370
static void FinalizeBufferCallback(char* data, void* hint) {
358371
Finalizer* finalizer = static_cast<Finalizer*>(hint);
359372
if (finalizer->_finalize_callback != nullptr) {
360-
NapiCallIntoModuleThrow(finalizer->_env, [&]() {
361-
finalizer->_finalize_callback(
362-
finalizer->_env,
363-
data,
364-
finalizer->_finalize_hint);
373+
finalizer->_env->CallIntoModuleThrow([&](napi_env env) {
374+
finalizer->_finalize_callback(env, data, finalizer->_finalize_hint);
365375
});
366376
}
367377

@@ -494,12 +504,10 @@ class Reference : private Finalizer {
494504
static void SecondPassCallback(const v8::WeakCallbackInfo<Reference>& data) {
495505
Reference* reference = data.GetParameter();
496506

497-
napi_env env = reference->_env;
498-
499507
if (reference->_finalize_callback != nullptr) {
500-
NapiCallIntoModuleThrow(env, [&]() {
508+
reference->_env->CallIntoModuleThrow([&](napi_env env) {
501509
reference->_finalize_callback(
502-
reference->_env,
510+
env,
503511
reference->_finalize_data,
504512
reference->_finalize_hint);
505513
});
@@ -617,7 +625,9 @@ class CallbackWrapperBase : public CallbackWrapper {
617625
napi_callback cb = _bundle->*FunctionField;
618626

619627
napi_value result;
620-
NapiCallIntoModuleThrow(env, [&]() { result = cb(env, cbinfo_wrapper); });
628+
env->CallIntoModuleThrow([&](napi_env env) {
629+
result = cb(env, cbinfo_wrapper);
630+
});
621631

622632
if (result != nullptr) {
623633
this->SetReturnValue(result);
@@ -781,44 +791,22 @@ v8::Local<v8::Value> CreateAccessorCallbackData(napi_env env,
781791
}
782792

783793
static
784-
napi_env GetEnv(v8::Local<v8::Context> context) {
794+
napi_env NewEnv(v8::Local<v8::Context> context) {
785795
napi_env result;
786796

787-
auto isolate = context->GetIsolate();
788-
auto global = context->Global();
789-
790-
// In the case of the string for which we grab the private and the value of
791-
// the private on the global object we can call .ToLocalChecked() directly
792-
// because we need to stop hard if either of them is empty.
793-
//
794-
// Re https://github.com/nodejs/node/pull/14217#discussion_r128775149
795-
auto value = global->GetPrivate(context, NAPI_PRIVATE_KEY(context, env))
796-
.ToLocalChecked();
797-
798-
if (value->IsExternal()) {
799-
result = static_cast<napi_env>(value.As<v8::External>()->Value());
800-
} else {
801-
result = new napi_env__(context);
802-
auto external = v8::External::New(isolate, result);
803-
804-
// We must also stop hard if the result of assigning the env to the global
805-
// is either nothing or false.
806-
CHECK(global->SetPrivate(context, NAPI_PRIVATE_KEY(context, env), external)
807-
.FromJust());
808-
809-
// TODO(addaleax): There was previously code that tried to delete the
810-
// napi_env when its v8::Context was garbage collected;
811-
// However, as long as N-API addons using this napi_env are in place,
812-
// the Context needs to be accessible and alive.
813-
// Ideally, we’d want an on-addon-unload hook that takes care of this
814-
// once all N-API addons using this napi_env are unloaded.
815-
// For now, a per-Environment cleanup hook is the best we can do.
816-
result->node_env()->AddCleanupHook(
817-
[](void* arg) {
818-
static_cast<napi_env>(arg)->Unref();
819-
},
820-
static_cast<void*>(result));
821-
}
797+
result = new napi_env__(context);
798+
// TODO(addaleax): There was previously code that tried to delete the
799+
// napi_env when its v8::Context was garbage collected;
800+
// However, as long as N-API addons using this napi_env are in place,
801+
// the Context needs to be accessible and alive.
802+
// Ideally, we'd want an on-addon-unload hook that takes care of this
803+
// once all N-API addons using this napi_env are unloaded.
804+
// For now, a per-Environment cleanup hook is the best we can do.
805+
result->node_env()->AddCleanupHook(
806+
[](void* arg) {
807+
static_cast<napi_env>(arg)->Unref();
808+
},
809+
static_cast<void*>(result));
822810

823811
return result;
824812
}
@@ -1311,10 +1299,10 @@ void napi_module_register_by_symbol(v8::Local<v8::Object> exports,
13111299

13121300
// Create a new napi_env for this module or reference one if a pre-existing
13131301
// one is found.
1314-
napi_env env = v8impl::GetEnv(context);
1302+
napi_env env = v8impl::NewEnv(context);
13151303

13161304
napi_value _exports;
1317-
NapiCallIntoModuleThrow(env, [&]() {
1305+
env->CallIntoModuleThrow([&](napi_env env) {
13181306
_exports = init(env, v8impl::JsValueFromV8LocalValue(exports));
13191307
});
13201308

@@ -3941,15 +3929,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork {
39413929

39423930
CallbackScope callback_scope(this);
39433931

3944-
// We have to back up the env here because the `NAPI_CALL_INTO_MODULE` macro
3945-
// makes use of it after the call into the module completes, but the module
3946-
// may have deallocated **this**, and along with it the place where _env is
3947-
// stored.
3948-
napi_env env = _env;
3949-
3950-
NapiCallIntoModule(env, [&]() {
3951-
_complete(_env, ConvertUVErrorCode(status), _data);
3952-
}, [env](v8::Local<v8::Value> local_err) {
3932+
_env->CallIntoModule([&](napi_env env) {
3933+
_complete(env, ConvertUVErrorCode(status), _data);
3934+
}, [](napi_env env, v8::Local<v8::Value> local_err) {
39533935
// If there was an unhandled exception in the complete callback,
39543936
// report it as a fatal exception. (There is no JavaScript on the
39553937
// callstack that can possibly handle it.)
@@ -4287,3 +4269,26 @@ napi_status napi_add_finalizer(napi_env env,
42874269
finalize_hint,
42884270
result);
42894271
}
4272+
4273+
napi_status napi_set_instance_data(napi_env env,
4274+
void* data,
4275+
napi_finalize finalize_cb,
4276+
void* finalize_hint) {
4277+
CHECK_ENV(env);
4278+
4279+
env->instance_data.data = data;
4280+
env->instance_data.finalize_cb = finalize_cb;
4281+
env->instance_data.hint = finalize_hint;
4282+
4283+
return napi_clear_last_error(env);
4284+
}
4285+
4286+
napi_status napi_get_instance_data(napi_env env,
4287+
void** data) {
4288+
CHECK_ENV(env);
4289+
CHECK_ARG(env, data);
4290+
4291+
*data = env->instance_data.data;
4292+
4293+
return napi_clear_last_error(env);
4294+
}

‎src/node_api.h

+9
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,15 @@ NAPI_EXTERN napi_status napi_get_value_bigint_words(napi_env env,
726726
int* sign_bit,
727727
size_t* word_count,
728728
uint64_t* words);
729+
730+
// Instance data
731+
NAPI_EXTERN napi_status napi_set_instance_data(napi_env env,
732+
void* data,
733+
napi_finalize finalize_cb,
734+
void* finalize_hint);
735+
736+
NAPI_EXTERN napi_status napi_get_instance_data(napi_env env,
737+
void** data);
729738
#endif // NAPI_EXPERIMENTAL
730739

731740
EXTERN_C_END

‎test/addons-napi/test_env_sharing/binding.gyp

-12
This file was deleted.

‎test/addons-napi/test_env_sharing/test.js

-9
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "test_instance_data",
5+
"sources": [
6+
"test_instance_data.c"
7+
]
8+
}
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
// Test API calls for instance data.
3+
4+
const common = require('../../common');
5+
const assert = require('assert');
6+
7+
if (module.parent) {
8+
// When required as a module, run the tests.
9+
const test_instance_data =
10+
require(`./build/${common.buildType}/test_instance_data`);
11+
12+
// Print to stdout when the environment deletes the instance data. This output
13+
// is checked by the parent process.
14+
test_instance_data.setPrintOnDelete();
15+
16+
// Test that instance data can be accessed from a binding.
17+
assert.strictEqual(test_instance_data.increment(), 42);
18+
19+
// Test that the instance data can be accessed from a finalizer.
20+
test_instance_data.objectWithFinalizer(common.mustCall());
21+
global.gc();
22+
} else {
23+
// When launched as a script, run tests in either a child process or in a
24+
// worker thread.
25+
const requireAs = require('../../common/require-as');
26+
const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] };
27+
28+
function checkOutput(child) {
29+
assert.strictEqual(child.status, 0);
30+
assert.strictEqual(
31+
(child.stdout.toString().split(/\r\n?|\n/) || [])[0],
32+
'deleting addon data');
33+
}
34+
35+
// Run tests in a child process.
36+
checkOutput(requireAs(__filename, ['--expose-gc', '--experimental-worker'],
37+
runOptions, 'child'));
38+
39+
// Run tests in a worker thread in a child process.
40+
checkOutput(requireAs(__filename, ['--expose-gc', '--experimental-worker'],
41+
runOptions, 'worker'));
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#define NAPI_EXPERIMENTAL
4+
#include <node_api.h>
5+
#include "../common.h"
6+
7+
typedef struct {
8+
size_t value;
9+
bool print;
10+
napi_ref js_cb_ref;
11+
} AddonData;
12+
13+
static napi_value Increment(napi_env env, napi_callback_info info) {
14+
AddonData* data;
15+
napi_value result;
16+
17+
NAPI_CALL(env, napi_get_instance_data(env, (void**)&data));
18+
NAPI_CALL(env, napi_create_uint32(env, ++data->value, &result));
19+
20+
return result;
21+
}
22+
23+
static void DeleteAddonData(napi_env env, void* raw_data, void* hint) {
24+
AddonData* data = raw_data;
25+
if (data->print) {
26+
printf("deleting addon data\n");
27+
}
28+
if (data->js_cb_ref != NULL) {
29+
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref));
30+
}
31+
free(data);
32+
}
33+
34+
static napi_value SetPrintOnDelete(napi_env env, napi_callback_info info) {
35+
AddonData* data;
36+
37+
NAPI_CALL(env, napi_get_instance_data(env, (void**)&data));
38+
data->print = true;
39+
40+
return NULL;
41+
}
42+
43+
static void TestFinalizer(napi_env env, void* raw_data, void* hint) {
44+
(void) raw_data;
45+
(void) hint;
46+
47+
AddonData* data;
48+
NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data));
49+
napi_value js_cb, undefined;
50+
NAPI_CALL_RETURN_VOID(env,
51+
napi_get_reference_value(env, data->js_cb_ref, &js_cb));
52+
NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
53+
NAPI_CALL_RETURN_VOID(env,
54+
napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
55+
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref));
56+
data->js_cb_ref = NULL;
57+
}
58+
59+
static napi_value ObjectWithFinalizer(napi_env env, napi_callback_info info) {
60+
AddonData* data;
61+
napi_value result, js_cb;
62+
size_t argc = 1;
63+
64+
NAPI_CALL(env, napi_get_instance_data(env, (void**)&data));
65+
NAPI_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL");
66+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL));
67+
NAPI_CALL(env, napi_create_object(env, &result));
68+
NAPI_CALL(env,
69+
napi_add_finalizer(env, result, NULL, TestFinalizer, NULL, NULL));
70+
NAPI_CALL(env, napi_create_reference(env, js_cb, 1, &data->js_cb_ref));
71+
72+
return result;
73+
}
74+
75+
napi_value Init(napi_env env, napi_value exports) {
76+
AddonData* data = malloc(sizeof(*data));
77+
data->value = 41;
78+
data->print = false;
79+
data->js_cb_ref = NULL;
80+
81+
NAPI_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL));
82+
83+
napi_property_descriptor props[] = {
84+
DECLARE_NAPI_PROPERTY("increment", Increment),
85+
DECLARE_NAPI_PROPERTY("setPrintOnDelete", SetPrintOnDelete),
86+
DECLARE_NAPI_PROPERTY("objectWithFinalizer", ObjectWithFinalizer),
87+
};
88+
89+
NAPI_CALL(env, napi_define_properties(env,
90+
exports,
91+
sizeof(props) / sizeof(*props),
92+
props));
93+
94+
return exports;
95+
}
96+
97+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

‎test/common/require-as.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable node-core/require-common-first, node-core/required-modules */
2+
'use strict';
3+
4+
if (module.parent) {
5+
const { spawnSync } = require('child_process');
6+
7+
function runModuleAs(filename, flags, spawnOptions, role) {
8+
return spawnSync(process.execPath,
9+
[...flags, __filename, role, filename], spawnOptions);
10+
}
11+
12+
module.exports = runModuleAs;
13+
return;
14+
}
15+
16+
const { Worker, isMainThread, workerData } = require('worker_threads');
17+
18+
if (isMainThread) {
19+
if (process.argv[2] === 'worker') {
20+
new Worker(__filename, {
21+
workerData: process.argv[3]
22+
});
23+
return;
24+
}
25+
require(process.argv[3]);
26+
} else {
27+
require(workerData);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "test_instance_data",
5+
"sources": [
6+
"test_instance_data.c"
7+
]
8+
}
9+
]
10+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
// Test API calls for instance data.
3+
4+
const common = require('../../common');
5+
6+
if (module.parent) {
7+
// When required as a module, run the tests.
8+
const test_instance_data =
9+
require(`./build/${common.buildType}/test_instance_data`);
10+
11+
// Test that instance data can be used in an async work callback.
12+
new Promise((resolve) => test_instance_data.asyncWorkCallback(resolve))
13+
14+
// Test that the buffer finalizer can access the instance data.
15+
.then(() => new Promise((resolve) => {
16+
test_instance_data.testBufferFinalizer(resolve);
17+
global.gc();
18+
}))
19+
20+
// Test that the thread-safe function can access the instance data.
21+
.then(() => new Promise((resolve) =>
22+
test_instance_data.testThreadsafeFunction(common.mustCall(),
23+
common.mustCall(resolve))));
24+
} else {
25+
// When launched as a script, run tests in either a child process or in a
26+
// worker thread.
27+
const requireAs = require('../../common/require-as');
28+
const runOptions = { stdio: ['inherit', 'pipe', 'inherit'] };
29+
30+
// Run tests in a child process.
31+
requireAs(__filename, ['--expose-gc'], runOptions, 'child');
32+
33+
// Run tests in a worker thread in a child process.
34+
requireAs(__filename, ['--expose-gc'], runOptions, 'worker');
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#include <stdlib.h>
2+
#include <uv.h>
3+
#define NAPI_EXPERIMENTAL
4+
#include <node_api.h>
5+
#include "../../js-native-api/common.h"
6+
7+
typedef struct {
8+
napi_ref js_cb_ref;
9+
napi_ref js_tsfn_finalizer_ref;
10+
napi_threadsafe_function tsfn;
11+
uv_thread_t thread;
12+
} AddonData;
13+
14+
static void AsyncWorkCbExecute(napi_env env, void* data) {
15+
(void) env;
16+
(void) data;
17+
}
18+
19+
static void call_cb_and_delete_ref(napi_env env, napi_ref* optional_ref) {
20+
napi_value js_cb, undefined;
21+
22+
if (optional_ref == NULL) {
23+
AddonData* data;
24+
NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data));
25+
optional_ref = &data->js_cb_ref;
26+
}
27+
28+
NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env,
29+
*optional_ref,
30+
&js_cb));
31+
NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
32+
NAPI_CALL_RETURN_VOID(env, napi_call_function(env,
33+
undefined,
34+
js_cb,
35+
0,
36+
NULL,
37+
NULL));
38+
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, *optional_ref));
39+
40+
*optional_ref = NULL;
41+
}
42+
43+
static void AsyncWorkCbComplete(napi_env env,
44+
napi_status status,
45+
void* data) {
46+
(void) status;
47+
(void) data;
48+
call_cb_and_delete_ref(env, NULL);
49+
}
50+
51+
static bool establish_callback_ref(napi_env env, napi_callback_info info) {
52+
AddonData* data;
53+
size_t argc = 1;
54+
napi_value js_cb;
55+
56+
NAPI_CALL_BASE(env, napi_get_instance_data(env, (void**)&data), false);
57+
NAPI_ASSERT_BASE(env,
58+
data->js_cb_ref == NULL,
59+
"reference must be NULL",
60+
false);
61+
NAPI_CALL_BASE(env,
62+
napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL),
63+
false);
64+
NAPI_CALL_BASE(env,
65+
napi_create_reference(env, js_cb, 1, &data->js_cb_ref),
66+
false);
67+
68+
return true;
69+
}
70+
71+
static napi_value AsyncWorkCallback(napi_env env, napi_callback_info info) {
72+
if (establish_callback_ref(env, info)) {
73+
napi_value resource_name;
74+
napi_async_work work;
75+
76+
NAPI_CALL(env, napi_create_string_utf8(env,
77+
"AsyncIncrement",
78+
NAPI_AUTO_LENGTH,
79+
&resource_name));
80+
NAPI_CALL(env, napi_create_async_work(env,
81+
NULL,
82+
resource_name,
83+
AsyncWorkCbExecute,
84+
AsyncWorkCbComplete,
85+
NULL,
86+
&work));
87+
NAPI_CALL(env, napi_queue_async_work(env, work));
88+
}
89+
90+
return NULL;
91+
}
92+
93+
static void TestBufferFinalizerCallback(napi_env env, void* data, void* hint) {
94+
(void) data;
95+
(void) hint;
96+
call_cb_and_delete_ref(env, NULL);
97+
}
98+
99+
static napi_value TestBufferFinalizer(napi_env env, napi_callback_info info) {
100+
napi_value buffer = NULL;
101+
if (establish_callback_ref(env, info)) {
102+
NAPI_CALL(env, napi_create_external_buffer(env,
103+
sizeof(napi_callback),
104+
TestBufferFinalizer,
105+
TestBufferFinalizerCallback,
106+
NULL,
107+
&buffer));
108+
}
109+
return buffer;
110+
}
111+
112+
static void ThreadsafeFunctionCallJS(napi_env env,
113+
napi_value tsfn_cb,
114+
void* context,
115+
void* data) {
116+
(void) tsfn_cb;
117+
(void) context;
118+
(void) data;
119+
call_cb_and_delete_ref(env, NULL);
120+
}
121+
122+
static void ThreadsafeFunctionTestThread(void* raw_data) {
123+
AddonData* data = raw_data;
124+
napi_status status;
125+
126+
// No need to call `napi_acquire_threadsafe_function()` because the main
127+
// thread has set the refcount to 1 and there is only this one secondary
128+
// thread.
129+
status = napi_call_threadsafe_function(data->tsfn,
130+
ThreadsafeFunctionCallJS,
131+
napi_tsfn_nonblocking);
132+
if (status != napi_ok) {
133+
napi_fatal_error("ThreadSafeFunctionTestThread",
134+
NAPI_AUTO_LENGTH,
135+
"Failed to call TSFN",
136+
NAPI_AUTO_LENGTH);
137+
}
138+
139+
status = napi_release_threadsafe_function(data->tsfn, napi_tsfn_release);
140+
if (status != napi_ok) {
141+
napi_fatal_error("ThreadSafeFunctionTestThread",
142+
NAPI_AUTO_LENGTH,
143+
"Failed to release TSFN",
144+
NAPI_AUTO_LENGTH);
145+
}
146+
147+
}
148+
149+
static void FinalizeThreadsafeFunction(napi_env env, void* raw, void* hint) {
150+
AddonData* data;
151+
NAPI_CALL_RETURN_VOID(env, napi_get_instance_data(env, (void**)&data));
152+
NAPI_ASSERT_RETURN_VOID(env,
153+
uv_thread_join(&data->thread) == 0,
154+
"Failed to join the thread");
155+
call_cb_and_delete_ref(env, &data->js_tsfn_finalizer_ref);
156+
data->tsfn = NULL;
157+
}
158+
159+
// Ths function accepts two arguments: the JS callback, and the finalize
160+
// callback. The latter moves the test forward.
161+
static napi_value
162+
TestThreadsafeFunction(napi_env env, napi_callback_info info) {
163+
AddonData* data;
164+
size_t argc = 2;
165+
napi_value argv[2], resource_name;
166+
167+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
168+
NAPI_CALL(env, napi_get_instance_data(env, (void**)&data));
169+
NAPI_ASSERT(env, data->js_cb_ref == NULL, "reference must be NULL");
170+
NAPI_ASSERT(env,
171+
data->js_tsfn_finalizer_ref == NULL,
172+
"tsfn finalizer reference must be NULL");
173+
NAPI_CALL(env, napi_create_reference(env, argv[0], 1, &data->js_cb_ref));
174+
NAPI_CALL(env, napi_create_reference(env,
175+
argv[1],
176+
1,
177+
&data->js_tsfn_finalizer_ref));
178+
NAPI_CALL(env, napi_create_string_utf8(env,
179+
"TSFN instance data test",
180+
NAPI_AUTO_LENGTH,
181+
&resource_name));
182+
NAPI_CALL(env, napi_create_threadsafe_function(env,
183+
NULL,
184+
NULL,
185+
resource_name,
186+
0,
187+
1,
188+
NULL,
189+
FinalizeThreadsafeFunction,
190+
NULL,
191+
ThreadsafeFunctionCallJS,
192+
&data->tsfn));
193+
NAPI_ASSERT(env,
194+
uv_thread_create(&data->thread,
195+
ThreadsafeFunctionTestThread,
196+
data) == 0,
197+
"uv_thread_create failed");
198+
199+
return NULL;
200+
}
201+
202+
static void DeleteAddonData(napi_env env, void* raw_data, void* hint) {
203+
AddonData* data = raw_data;
204+
if (data->js_cb_ref) {
205+
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, data->js_cb_ref));
206+
}
207+
if (data->js_tsfn_finalizer_ref) {
208+
NAPI_CALL_RETURN_VOID(env,
209+
napi_delete_reference(env,
210+
data->js_tsfn_finalizer_ref));
211+
}
212+
free(data);
213+
}
214+
215+
static napi_value Init(napi_env env, napi_value exports) {
216+
AddonData* data = malloc(sizeof(*data));
217+
data->js_cb_ref = NULL;
218+
data->js_tsfn_finalizer_ref = NULL;
219+
220+
NAPI_CALL(env, napi_set_instance_data(env, data, DeleteAddonData, NULL));
221+
222+
napi_property_descriptor props[] = {
223+
DECLARE_NAPI_PROPERTY("asyncWorkCallback", AsyncWorkCallback),
224+
DECLARE_NAPI_PROPERTY("testBufferFinalizer", TestBufferFinalizer),
225+
DECLARE_NAPI_PROPERTY("testThreadsafeFunction", TestThreadsafeFunction),
226+
};
227+
228+
NAPI_CALL(env, napi_define_properties(env,
229+
exports,
230+
sizeof(props) / sizeof(*props),
231+
props));
232+
233+
return exports;
234+
}
235+
236+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

0 commit comments

Comments
 (0)
Please sign in to comment.