Skip to content

Commit 860c0d8

Browse files
Gabriel Schulhoftargos
Gabriel Schulhof
authored andcommitted
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 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent 881e345 commit 860c0d8

File tree

17 files changed

+630
-133
lines changed

17 files changed

+630
-133
lines changed

doc/api/n-api.md

+78
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,82 @@ NAPI_MODULE_INIT() {
251251
}
252252
```
253253

254+
## Environment Life Cycle APIs
255+
256+
> Stability: 1 - Experimental
257+
258+
[Section 8.7][] of the [ECMAScript Language Specification][] defines the concept
259+
of an "Agent" as a self-contained environment in which JavaScript code runs.
260+
Multiple such Agents may be started and terminated either concurrently or in
261+
sequence by the process.
262+
263+
A Node.js environment corresponds to an ECMAScript Agent. In the main process,
264+
an environment is created at startup, and additional environments can be created
265+
on separate threads to serve as [worker threads][]. When Node.js is embedded in
266+
another application, the main thread of the application may also construct and
267+
destroy a Node.js environment multiple times during the life cycle of the
268+
application process such that each Node.js environment created by the
269+
application may, in turn, during its life cycle create and destroy additional
270+
environments as worker threads.
271+
272+
From the perspective of a native addon this means that the bindings it provides
273+
may be called multiple times, from multiple contexts, and even concurrently from
274+
multiple threads.
275+
276+
Native addons may need to allocate global state of which they make use during
277+
their entire life cycle such that the state must be unique to each instance of
278+
the addon.
279+
280+
To this env, N-API provides a way to allocate data such that its life cycle is
281+
tied to the life cycle of the Agent.
282+
283+
### napi_set_instance_data
284+
<!-- YAML
285+
added: REPLACEME
286+
-->
287+
288+
```C
289+
napi_status napi_set_instance_data(napi_env env,
290+
void* data,
291+
napi_finalize finalize_cb,
292+
void* finalize_hint);
293+
```
294+
295+
- `[in] env`: The environment that the N-API call is invoked under.
296+
- `[in] data`: The data item to make available to bindings of this instance.
297+
- `[in] finalize_cb`: The function to call when the environment is being torn
298+
down. The function receives `data` so that it might free it.
299+
- `[in] finalize_hint`: Optional hint to pass to the finalize callback
300+
during collection.
301+
302+
Returns `napi_ok` if the API succeeded.
303+
304+
This API associates `data` with the currently running Agent. `data` can later
305+
be retrieved using `napi_get_instance_data()`. Any existing data associated with
306+
the currently running Agent which was set by means of a previous call to
307+
`napi_set_instance_data()` will be overwritten. If a `finalize_cb` was provided
308+
by the previous call, it will not be called.
309+
310+
### napi_get_instance_data
311+
<!-- YAML
312+
added: REPLACEME
313+
-->
314+
315+
```C
316+
napi_status napi_get_instance_data(napi_env env,
317+
void** data);
318+
```
319+
320+
- `[in] env`: The environment that the N-API call is invoked under.
321+
- `[out] data`: The data item that was previously associated with the currently
322+
running Agent by a call to `napi_set_instance_data()`.
323+
324+
Returns `napi_ok` if the API succeeded.
325+
326+
This API retrieves data that was previously associated with the currently
327+
running Agent via `napi_set_instance_data()`. If no data is set, the call will
328+
succeed and `data` will be set to `NULL`.
329+
254330
## Basic N-API Data Types
255331

256332
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.
48764952
[Section 6.1.4]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-string-type
48774953
[Section 6.1.6]: https://tc39.github.io/ecma262/#sec-ecmascript-language-types-number-type
48784954
[Section 6.1.7.1]: https://tc39.github.io/ecma262/#table-2
4955+
[Section 8.7]: https://tc39.es/ecma262/#sec-agents
48794956
[Section 9.1.6]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc
48804957
[Working with JavaScript Functions]: #n_api_working_with_javascript_functions
48814958
[Working with JavaScript Properties]: #n_api_working_with_javascript_properties
@@ -4930,3 +5007,4 @@ This API may only be called from the main thread.
49305007
[`uv_unref`]: http://docs.libuv.org/en/v1.x/handle.html#c.uv_unref
49315008
[async_hooks `type`]: async_hooks.html#async_hooks_type
49325009
[context-aware addons]: addons.html#addons_context_aware_addons
5010+
[worker threads]: https://nodejs.org/api/worker_threads.html

src/env.h

-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
130130
V(contextify_context_private_symbol, "node:contextify:context") \
131131
V(contextify_global_private_symbol, "node:contextify:global") \
132132
V(decorated_private_symbol, "node:decorated") \
133-
V(napi_env, "node:napi:env") \
134133
V(napi_wrapper, "node:napi:wrapper") \
135134
V(sab_lifetimepartner_symbol, "node:sharedArrayBufferLifetimePartner") \
136135

src/js_native_api.h

+9
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,15 @@ NAPI_EXTERN napi_status napi_add_finalizer(napi_env env,
499499
napi_finalize finalize_cb,
500500
void* finalize_hint,
501501
napi_ref* result);
502+
503+
// Instance data
504+
NAPI_EXTERN napi_status napi_set_instance_data(napi_env env,
505+
void* data,
506+
napi_finalize finalize_cb,
507+
void* finalize_hint);
508+
509+
NAPI_EXTERN napi_status napi_get_instance_data(napi_env env,
510+
void** data);
502511
#endif // NAPI_EXPERIMENTAL
503512

504513
EXTERN_C_END

src/js_native_api_v8.cc

+28-5
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,10 @@ class Reference : private Finalizer {
305305
static void SecondPassCallback(const v8::WeakCallbackInfo<Reference>& data) {
306306
Reference* reference = data.GetParameter();
307307

308-
napi_env env = reference->_env;
309-
310308
if (reference->_finalize_callback != nullptr) {
311-
NapiCallIntoModuleThrow(env, [&]() {
309+
reference->_env->CallIntoModuleThrow([&](napi_env env) {
312310
reference->_finalize_callback(
313-
reference->_env,
311+
env,
314312
reference->_finalize_data,
315313
reference->_finalize_hint);
316314
});
@@ -452,7 +450,9 @@ class CallbackWrapperBase : public CallbackWrapper {
452450
napi_callback cb = _bundle->*FunctionField;
453451

454452
napi_value result;
455-
NapiCallIntoModuleThrow(env, [&]() { result = cb(env, cbinfo_wrapper); });
453+
env->CallIntoModuleThrow([&](napi_env env) {
454+
result = cb(env, cbinfo_wrapper);
455+
});
456456

457457
if (result != nullptr) {
458458
this->SetReturnValue(result);
@@ -2986,3 +2986,26 @@ napi_status napi_adjust_external_memory(napi_env env,
29862986

29872987
return napi_clear_last_error(env);
29882988
}
2989+
2990+
napi_status napi_set_instance_data(napi_env env,
2991+
void* data,
2992+
napi_finalize finalize_cb,
2993+
void* finalize_hint) {
2994+
CHECK_ENV(env);
2995+
2996+
env->instance_data.data = data;
2997+
env->instance_data.finalize_cb = finalize_cb;
2998+
env->instance_data.hint = finalize_hint;
2999+
3000+
return napi_clear_last_error(env);
3001+
}
3002+
3003+
napi_status napi_get_instance_data(napi_env env,
3004+
void** data) {
3005+
CHECK_ENV(env);
3006+
CHECK_ARG(env, data);
3007+
3008+
*data = env->instance_data.data;
3009+
3010+
return napi_clear_last_error(env);
3011+
}

src/js_native_api_v8.h

+35-22
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@
66
#include "js_native_api_types.h"
77
#include "js_native_api_v8_internals.h"
88

9+
static napi_status napi_clear_last_error(napi_env env);
10+
911
struct napi_env__ {
1012
explicit napi_env__(v8::Local<v8::Context> context)
1113
: isolate(context->GetIsolate()),
1214
context_persistent(isolate, context) {
1315
CHECK_EQ(isolate, context->GetIsolate());
1416
}
15-
virtual ~napi_env__() = default;
17+
virtual ~napi_env__() {
18+
if (instance_data.finalize_cb != nullptr) {
19+
CallIntoModuleThrow([&](napi_env env) {
20+
instance_data.finalize_cb(env, instance_data.data, instance_data.hint);
21+
});
22+
}
23+
}
1624
v8::Isolate* const isolate; // Shortcut for context()->GetIsolate()
1725
v8impl::Persistent<v8::Context> context_persistent;
1826

@@ -25,11 +33,37 @@ struct napi_env__ {
2533

2634
virtual bool can_call_into_js() const { return true; }
2735

36+
template <typename T, typename U>
37+
void CallIntoModule(T&& call, U&& handle_exception) {
38+
int open_handle_scopes_before = open_handle_scopes;
39+
int open_callback_scopes_before = open_callback_scopes;
40+
napi_clear_last_error(this);
41+
call(this);
42+
CHECK_EQ(open_handle_scopes, open_handle_scopes_before);
43+
CHECK_EQ(open_callback_scopes, open_callback_scopes_before);
44+
if (!last_exception.IsEmpty()) {
45+
handle_exception(this, last_exception.Get(this->isolate));
46+
last_exception.Reset();
47+
}
48+
}
49+
50+
template <typename T>
51+
void CallIntoModuleThrow(T&& call) {
52+
CallIntoModule(call, [&](napi_env env, v8::Local<v8::Value> value) {
53+
env->isolate->ThrowException(value);
54+
});
55+
}
56+
2857
v8impl::Persistent<v8::Value> last_exception;
2958
napi_extended_error_info last_error;
3059
int open_handle_scopes = 0;
3160
int open_callback_scopes = 0;
3261
int refs = 1;
62+
struct {
63+
void* data = nullptr;
64+
void* hint = nullptr;
65+
napi_finalize finalize_cb = nullptr;
66+
} instance_data;
3367
};
3468

3569
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,
114148
} \
115149
} while (0)
116150

117-
template <typename T, typename U>
118-
void NapiCallIntoModule(napi_env env, T&& call, U&& handle_exception) {
119-
int open_handle_scopes = env->open_handle_scopes;
120-
int open_callback_scopes = env->open_callback_scopes;
121-
napi_clear_last_error(env);
122-
call();
123-
CHECK_EQ(env->open_handle_scopes, open_handle_scopes);
124-
CHECK_EQ(env->open_callback_scopes, open_callback_scopes);
125-
if (!env->last_exception.IsEmpty()) {
126-
handle_exception(env->last_exception.Get(env->isolate));
127-
env->last_exception.Reset();
128-
}
129-
}
130-
131-
template <typename T>
132-
void NapiCallIntoModuleThrow(napi_env env, T&& call) {
133-
NapiCallIntoModule(env, call, [&](v8::Local<v8::Value> value) {
134-
env->isolate->ThrowException(value);
135-
});
136-
}
137-
138151
namespace v8impl {
139152

140153
//=== Conversion between V8 Handles and napi_value ========================

src/node_api.cc

+23-51
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ class BufferFinalizer : private Finalizer {
4646
v8::HandleScope handle_scope(finalizer->_env->isolate);
4747
v8::Context::Scope context_scope(finalizer->_env->context());
4848

49-
NapiCallIntoModuleThrow(finalizer->_env, [&]() {
49+
finalizer->_env->CallIntoModuleThrow([&](napi_env env) {
5050
finalizer->_finalize_callback(
51-
finalizer->_env,
51+
env,
5252
finalizer->_finalize_data,
5353
finalizer->_finalize_hint);
5454
});
@@ -59,44 +59,22 @@ class BufferFinalizer : private Finalizer {
5959
}
6060
};
6161

62-
static inline napi_env GetEnv(v8::Local<v8::Context> context) {
62+
static inline napi_env NewEnv(v8::Local<v8::Context> context) {
6363
node_napi_env result;
6464

65-
auto isolate = context->GetIsolate();
66-
auto global = context->Global();
67-
68-
// In the case of the string for which we grab the private and the value of
69-
// the private on the global object we can call .ToLocalChecked() directly
70-
// because we need to stop hard if either of them is empty.
71-
//
72-
// Re https://github.com./nodejs/node/pull/14217#discussion_r128775149
73-
auto value = global->GetPrivate(context, NAPI_PRIVATE_KEY(context, env))
74-
.ToLocalChecked();
75-
76-
if (value->IsExternal()) {
77-
result = static_cast<node_napi_env>(value.As<v8::External>()->Value());
78-
} else {
79-
result = new node_napi_env__(context);
80-
auto external = v8::External::New(isolate, result);
81-
82-
// We must also stop hard if the result of assigning the env to the global
83-
// is either nothing or false.
84-
CHECK(global->SetPrivate(context, NAPI_PRIVATE_KEY(context, env), external)
85-
.FromJust());
86-
87-
// TODO(addaleax): There was previously code that tried to delete the
88-
// napi_env when its v8::Context was garbage collected;
89-
// However, as long as N-API addons using this napi_env are in place,
90-
// the Context needs to be accessible and alive.
91-
// Ideally, we'd want an on-addon-unload hook that takes care of this
92-
// once all N-API addons using this napi_env are unloaded.
93-
// For now, a per-Environment cleanup hook is the best we can do.
94-
result->node_env()->AddCleanupHook(
95-
[](void* arg) {
96-
static_cast<napi_env>(arg)->Unref();
97-
},
98-
static_cast<void*>(result));
99-
}
65+
result = new node_napi_env__(context);
66+
// TODO(addaleax): There was previously code that tried to delete the
67+
// napi_env when its v8::Context was garbage collected;
68+
// However, as long as N-API addons using this napi_env are in place,
69+
// the Context needs to be accessible and alive.
70+
// Ideally, we'd want an on-addon-unload hook that takes care of this
71+
// once all N-API addons using this napi_env are unloaded.
72+
// For now, a per-Environment cleanup hook is the best we can do.
73+
result->node_env()->AddCleanupHook(
74+
[](void* arg) {
75+
static_cast<napi_env>(arg)->Unref();
76+
},
77+
static_cast<void*>(result));
10078

10179
return result;
10280
}
@@ -325,7 +303,7 @@ class ThreadSafeFunction : public node::AsyncResource {
325303
v8::Local<v8::Function>::New(env->isolate, ref);
326304
js_callback = v8impl::JsValueFromV8LocalValue(js_cb);
327305
}
328-
NapiCallIntoModuleThrow(env, [&]() {
306+
env->CallIntoModuleThrow([&](napi_env env) {
329307
call_js_cb(env, js_callback, context, data);
330308
});
331309
}
@@ -346,7 +324,7 @@ class ThreadSafeFunction : public node::AsyncResource {
346324
v8::HandleScope scope(env->isolate);
347325
if (finalize_cb) {
348326
CallbackScope cb_scope(this);
349-
NapiCallIntoModuleThrow(env, [&]() {
327+
env->CallIntoModuleThrow([&](napi_env env) {
350328
finalize_cb(env, finalize_data, context);
351329
});
352330
}
@@ -481,10 +459,10 @@ void napi_module_register_by_symbol(v8::Local<v8::Object> exports,
481459

482460
// Create a new napi_env for this module or reference one if a pre-existing
483461
// one is found.
484-
napi_env env = v8impl::GetEnv(context);
462+
napi_env env = v8impl::NewEnv(context);
485463

486464
napi_value _exports;
487-
NapiCallIntoModuleThrow(env, [&]() {
465+
env->CallIntoModuleThrow([&](napi_env env) {
488466
_exports = init(env, v8impl::JsValueFromV8LocalValue(exports));
489467
});
490468

@@ -889,15 +867,9 @@ class Work : public node::AsyncResource, public node::ThreadPoolWork {
889867

890868
CallbackScope callback_scope(this);
891869

892-
// We have to back up the env here because the `NAPI_CALL_INTO_MODULE` macro
893-
// makes use of it after the call into the module completes, but the module
894-
// may have deallocated **this**, and along with it the place where _env is
895-
// stored.
896-
napi_env env = _env;
897-
898-
NapiCallIntoModule(env, [&]() {
899-
_complete(_env, ConvertUVErrorCode(status), _data);
900-
}, [env](v8::Local<v8::Value> local_err) {
870+
_env->CallIntoModule([&](napi_env env) {
871+
_complete(env, ConvertUVErrorCode(status), _data);
872+
}, [](napi_env env, v8::Local<v8::Value> local_err) {
901873
// If there was an unhandled exception in the complete callback,
902874
// report it as a fatal exception. (There is no JavaScript on the
903875
// callstack that can possibly handle it.)

0 commit comments

Comments
 (0)