Skip to content

Commit f29fb14

Browse files
Gabriel SchulhofBethGriggs
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 Backport-PR-URL: #30537 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
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+
}

0 commit comments

Comments
 (0)