Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
gabrielschulhof marked this conversation as resolved.
Show resolved Hide resolved

### 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
Expand Down Expand Up @@ -4872,6 +4948,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
gabrielschulhof marked this conversation as resolved.
Show resolved Hide resolved
[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
Expand Down Expand Up @@ -4926,3 +5003,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
1 change: 0 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,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") \

Expand Down
9 changes: 9 additions & 0 deletions src/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2984,3 +2984,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,
bnoordhuis marked this conversation as resolved.
Show resolved Hide resolved
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);
}
57 changes: 35 additions & 22 deletions src/js_native_api_v8.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -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 ========================
Expand Down
74 changes: 23 additions & 51 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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;
}
Expand Down Expand Up @@ -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);
});
}
Expand All @@ -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);
});
}
Expand Down Expand Up @@ -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));
});

Expand Down Expand Up @@ -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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By changing the expected signature of the lambda(s) being passed into CallIntoModule*() to receive a napi_env env we can avoid having to back up env like this, because the lambdas no longer access data from the instance.


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.)
Expand Down
Loading