Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2cf2316

Browse filesBrowse files
committed
n-api,src: provide asynchronous cleanup hooks
Sometimes addons need to perform cleanup actions, for example closing libuv handles or waiting for requests to finish, that cannot be performed synchronously. Add C++ API and N-API functions that allow providing such asynchronous cleanup hooks. Fixes: #34567 PR-URL: #34572 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gabriel Schulhof <gabriel.schulhof@intel.com>
1 parent 0a44017 commit 2cf2316
Copy full SHA for 2cf2316

File tree

Expand file treeCollapse file tree

13 files changed

+368
-3
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

13 files changed

+368
-3
lines changed
Open diff view settings
Collapse file

‎doc/api/addons.md‎

Copy file name to clipboardExpand all lines: doc/api/addons.md
+11Lines changed: 11 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ NODE_MODULE_INIT(/* exports, module, context */) {
234234
```
235235
236236
#### Worker support
237+
<!-- YAML
238+
changes:
239+
- version: REPLACEME
240+
pr-url: https://github.com/nodejs/node/pull/34572
241+
description: Cleanup hooks may now be asynchronous.
242+
-->
237243
238244
In order to be loaded from multiple Node.js environments,
239245
such as a main thread and a Worker thread, an add-on needs to either:
@@ -256,6 +262,11 @@ down. If necessary, such hooks can be removed using
256262
`RemoveEnvironmentCleanupHook()` before they are run, which has the same
257263
signature. Callbacks are run in last-in first-out order.
258264

265+
If necessary, there is an additional pair of `AddEnvironmentCleanupHook()`
266+
and `RemoveEnvironmentCleanupHook()` overloads, where the cleanup hook takes a
267+
callback function. This can be used for shutting down asynchronous resources,
268+
for example any libuv handles registered by the addon.
269+
259270
The following `addon.cc` uses `AddEnvironmentCleanupHook`:
260271

261272
```cpp
Collapse file

‎doc/api/n-api.md‎

Copy file name to clipboardExpand all lines: doc/api/n-api.md
+52-1Lines changed: 52 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1543,10 +1543,12 @@ and will lead the process to abort.
15431543
The hooks will be called in reverse order, i.e. the most recently added one
15441544
will be called first.
15451545

1546-
Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
1546+
Removing this hook can be done by using [`napi_remove_env_cleanup_hook`][].
15471547
Typically, that happens when the resource for which this hook was added
15481548
is being torn down anyway.
15491549

1550+
For asynchronous cleanup, [`napi_add_async_cleanup_hook`][] is available.
1551+
15501552
#### napi_remove_env_cleanup_hook
15511553
<!-- YAML
15521554
added: v10.2.0
@@ -1566,6 +1568,52 @@ need to be exact matches.
15661568
The function must have originally been registered
15671569
with `napi_add_env_cleanup_hook`, otherwise the process will abort.
15681570

1571+
#### napi_add_async_cleanup_hook
1572+
<!-- YAML
1573+
added: REPLACEME
1574+
-->
1575+
1576+
> Stability: 1 - Experimental
1577+
1578+
```c
1579+
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
1580+
napi_env env,
1581+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
1582+
void* arg,
1583+
napi_async_cleanup_hook_handle* remove_handle);
1584+
```
1585+
1586+
Registers `fun` as a function to be run with the `arg` parameter once the
1587+
current Node.js environment exits. Unlike [`napi_add_env_cleanup_hook`][],
1588+
the hook is allowed to be asynchronous in this case, and must invoke the passed
1589+
`cb()` function with `cbarg` once all asynchronous activity is finished.
1590+
1591+
Otherwise, behavior generally matches that of [`napi_add_env_cleanup_hook`][].
1592+
1593+
If `remove_handle` is not `NULL`, an opaque value will be stored in it
1594+
that must later be passed to [`napi_remove_async_cleanup_hook`][],
1595+
regardless of whether the hook has already been invoked.
1596+
Typically, that happens when the resource for which this hook was added
1597+
is being torn down anyway.
1598+
1599+
#### napi_remove_async_cleanup_hook
1600+
<!-- YAML
1601+
added: REPLACEME
1602+
-->
1603+
1604+
> Stability: 1 - Experimental
1605+
1606+
```c
1607+
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
1608+
napi_env env,
1609+
napi_async_cleanup_hook_handle remove_handle);
1610+
```
1611+
1612+
Unregisters the cleanup hook corresponding to `remove_handle`. This will prevent
1613+
the hook from being executed, unless it has already started executing.
1614+
This must be called on any `napi_async_cleanup_hook_handle` value retrieved
1615+
from [`napi_add_async_cleanup_hook`][].
1616+
15691617
## Module registration
15701618
N-API modules are registered in a manner similar to other modules
15711619
except that instead of using the `NODE_MODULE` macro the following
@@ -5659,6 +5707,7 @@ This API may only be called from the main thread.
56595707
[`Worker`]: worker_threads.html#worker_threads_class_worker
56605708
[`global`]: globals.html#globals_global
56615709
[`init` hooks]: async_hooks.html#async_hooks_init_asyncid_type_triggerasyncid_resource
5710+
[`napi_add_async_cleanup_hook`]: #n_api_napi_add_async_cleanup_hook
56625711
[`napi_add_env_cleanup_hook`]: #n_api_napi_add_env_cleanup_hook
56635712
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
56645713
[`napi_async_complete_callback`]: #n_api_napi_async_complete_callback
@@ -5699,6 +5748,8 @@ This API may only be called from the main thread.
56995748
[`napi_queue_async_work`]: #n_api_napi_queue_async_work
57005749
[`napi_reference_ref`]: #n_api_napi_reference_ref
57015750
[`napi_reference_unref`]: #n_api_napi_reference_unref
5751+
[`napi_remove_async_cleanup_hook`]: #n_api_napi_remove_async_cleanup_hook
5752+
[`napi_remove_env_cleanup_hook`]: #n_api_napi_remove_env_cleanup_hook
57025753
[`napi_set_instance_data`]: #n_api_napi_set_instance_data
57035754
[`napi_set_property`]: #n_api_napi_set_property
57045755
[`napi_threadsafe_function_call_js`]: #n_api_napi_threadsafe_function_call_js
Collapse file

‎src/api/hooks.cc‎

Copy file name to clipboardExpand all lines: src/api/hooks.cc
+66-2Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,86 @@ int EmitExit(Environment* env) {
7373
.ToChecked();
7474
}
7575

76+
typedef void (*CleanupHook)(void* arg);
77+
typedef void (*AsyncCleanupHook)(void* arg, void(*)(void*), void*);
78+
79+
struct AsyncCleanupHookInfo final {
80+
Environment* env;
81+
AsyncCleanupHook fun;
82+
void* arg;
83+
bool started = false;
84+
// Use a self-reference to make sure the storage is kept alive while the
85+
// cleanup hook is registered but not yet finished.
86+
std::shared_ptr<AsyncCleanupHookInfo> self;
87+
};
88+
89+
// Opaque type that is basically an alias for `shared_ptr<AsyncCleanupHookInfo>`
90+
// (but not publicly so for easier ABI/API changes). In particular,
91+
// std::shared_ptr does not generally maintain a consistent ABI even on a
92+
// specific platform.
93+
struct ACHHandle final {
94+
std::shared_ptr<AsyncCleanupHookInfo> info;
95+
};
96+
// This is implemented as an operator on a struct because otherwise you can't
97+
// default-initialize AsyncCleanupHookHandle, because in C++ for a
98+
// std::unique_ptr to be default-initializable the deleter type also needs
99+
// to be default-initializable; in particular, function types don't satisfy
100+
// this.
101+
void DeleteACHHandle::operator ()(ACHHandle* handle) const { delete handle; }
102+
76103
void AddEnvironmentCleanupHook(Isolate* isolate,
77-
void (*fun)(void* arg),
104+
CleanupHook fun,
78105
void* arg) {
79106
Environment* env = Environment::GetCurrent(isolate);
80107
CHECK_NOT_NULL(env);
81108
env->AddCleanupHook(fun, arg);
82109
}
83110

84111
void RemoveEnvironmentCleanupHook(Isolate* isolate,
85-
void (*fun)(void* arg),
112+
CleanupHook fun,
86113
void* arg) {
87114
Environment* env = Environment::GetCurrent(isolate);
88115
CHECK_NOT_NULL(env);
89116
env->RemoveCleanupHook(fun, arg);
90117
}
91118

119+
static void FinishAsyncCleanupHook(void* arg) {
120+
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
121+
std::shared_ptr<AsyncCleanupHookInfo> keep_alive = info->self;
122+
123+
info->env->DecreaseWaitingRequestCounter();
124+
info->self.reset();
125+
}
126+
127+
static void RunAsyncCleanupHook(void* arg) {
128+
AsyncCleanupHookInfo* info = static_cast<AsyncCleanupHookInfo*>(arg);
129+
info->env->IncreaseWaitingRequestCounter();
130+
info->started = true;
131+
info->fun(info->arg, FinishAsyncCleanupHook, info);
132+
}
133+
134+
AsyncCleanupHookHandle AddEnvironmentCleanupHook(
135+
Isolate* isolate,
136+
AsyncCleanupHook fun,
137+
void* arg) {
138+
Environment* env = Environment::GetCurrent(isolate);
139+
CHECK_NOT_NULL(env);
140+
auto info = std::make_shared<AsyncCleanupHookInfo>();
141+
info->env = env;
142+
info->fun = fun;
143+
info->arg = arg;
144+
info->self = info;
145+
env->AddCleanupHook(RunAsyncCleanupHook, info.get());
146+
return AsyncCleanupHookHandle(new ACHHandle { info });
147+
}
148+
149+
void RemoveEnvironmentCleanupHook(
150+
AsyncCleanupHookHandle handle) {
151+
if (handle->info->started) return;
152+
handle->info->self.reset();
153+
handle->info->env->RemoveCleanupHook(RunAsyncCleanupHook, handle->info.get());
154+
}
155+
92156
async_id AsyncHooksGetExecutionAsyncId(Isolate* isolate) {
93157
Environment* env = Environment::GetCurrent(isolate);
94158
if (env == nullptr) return -1;
Collapse file

‎src/node.h‎

Copy file name to clipboardExpand all lines: src/node.h
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,20 @@ NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
735735
void (*fun)(void* arg),
736736
void* arg);
737737

738+
/* These are async equivalents of the above. After the cleanup hook is invoked,
739+
* `cb(cbarg)` *must* be called, and attempting to remove the cleanup hook will
740+
* have no effect. */
741+
struct ACHHandle;
742+
struct NODE_EXTERN DeleteACHHandle { void operator()(ACHHandle*) const; };
743+
typedef std::unique_ptr<ACHHandle, DeleteACHHandle> AsyncCleanupHookHandle;
744+
745+
NODE_EXTERN AsyncCleanupHookHandle AddEnvironmentCleanupHook(
746+
v8::Isolate* isolate,
747+
void (*fun)(void* arg, void (*cb)(void*), void* cbarg),
748+
void* arg);
749+
750+
NODE_EXTERN void RemoveEnvironmentCleanupHook(AsyncCleanupHookHandle holder);
751+
738752
/* Returns the id of the current execution context. If the return value is
739753
* zero then no execution has been set. This will happen if the user handles
740754
* I/O from native code. */
Collapse file

‎src/node_api.cc‎

Copy file name to clipboardExpand all lines: src/node_api.cc
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,38 @@ napi_status napi_remove_env_cleanup_hook(napi_env env,
518518
return napi_ok;
519519
}
520520

521+
struct napi_async_cleanup_hook_handle__ {
522+
node::AsyncCleanupHookHandle handle;
523+
};
524+
525+
napi_status napi_add_async_cleanup_hook(
526+
napi_env env,
527+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
528+
void* arg,
529+
napi_async_cleanup_hook_handle* remove_handle) {
530+
CHECK_ENV(env);
531+
CHECK_ARG(env, fun);
532+
533+
auto handle = node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
534+
if (remove_handle != nullptr) {
535+
*remove_handle = new napi_async_cleanup_hook_handle__ { std::move(handle) };
536+
}
537+
538+
return napi_clear_last_error(env);
539+
}
540+
541+
napi_status napi_remove_async_cleanup_hook(
542+
napi_env env,
543+
napi_async_cleanup_hook_handle remove_handle) {
544+
CHECK_ENV(env);
545+
CHECK_ARG(env, remove_handle);
546+
547+
node::RemoveEnvironmentCleanupHook(std::move(remove_handle->handle));
548+
delete remove_handle;
549+
550+
return napi_clear_last_error(env);
551+
}
552+
521553
napi_status napi_fatal_exception(napi_env env, napi_value err) {
522554
NAPI_PREAMBLE(env);
523555
CHECK_ARG(env, err);
Collapse file

‎src/node_api.h‎

Copy file name to clipboardExpand all lines: src/node_api.h
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,20 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func);
250250

251251
#endif // NAPI_VERSION >= 4
252252

253+
#ifdef NAPI_EXPERIMENTAL
254+
255+
NAPI_EXTERN napi_status napi_add_async_cleanup_hook(
256+
napi_env env,
257+
void (*fun)(void* arg, void(* cb)(void*), void* cbarg),
258+
void* arg,
259+
napi_async_cleanup_hook_handle* remove_handle);
260+
261+
NAPI_EXTERN napi_status napi_remove_async_cleanup_hook(
262+
napi_env env,
263+
napi_async_cleanup_hook_handle remove_handle);
264+
265+
#endif // NAPI_EXPERIMENTAL
266+
253267
EXTERN_C_END
254268

255269
#endif // SRC_NODE_API_H_
Collapse file

‎src/node_api_types.h‎

Copy file name to clipboardExpand all lines: src/node_api_types.h
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,8 @@ typedef struct {
4141
const char* release;
4242
} napi_node_version;
4343

44+
#ifdef NAPI_EXPERIMENTAL
45+
typedef struct napi_async_cleanup_hook_handle__* napi_async_cleanup_hook_handle;
46+
#endif // NAPI_EXPERIMENTAL
47+
4448
#endif // SRC_NODE_API_TYPES_H_
Collapse file
+59Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#include <assert.h>
2+
#include <node.h>
3+
#include <uv.h>
4+
5+
void MustNotCall(void* arg, void(*cb)(void*), void* cbarg) {
6+
assert(0);
7+
}
8+
9+
struct AsyncData {
10+
uv_async_t async;
11+
v8::Isolate* isolate;
12+
node::AsyncCleanupHookHandle handle;
13+
void (*done_cb)(void*);
14+
void* done_arg;
15+
};
16+
17+
void AsyncCleanupHook(void* arg, void(*cb)(void*), void* cbarg) {
18+
AsyncData* data = static_cast<AsyncData*>(arg);
19+
uv_loop_t* loop = node::GetCurrentEventLoop(data->isolate);
20+
assert(loop != nullptr);
21+
int err = uv_async_init(loop, &data->async, [](uv_async_t* async) {
22+
AsyncData* data = static_cast<AsyncData*>(async->data);
23+
// Attempting to remove the cleanup hook here should be a no-op since it
24+
// has already been started.
25+
node::RemoveEnvironmentCleanupHook(std::move(data->handle));
26+
27+
uv_close(reinterpret_cast<uv_handle_t*>(async), [](uv_handle_t* handle) {
28+
AsyncData* data = static_cast<AsyncData*>(handle->data);
29+
data->done_cb(data->done_arg);
30+
delete data;
31+
});
32+
});
33+
assert(err == 0);
34+
35+
data->async.data = data;
36+
data->done_cb = cb;
37+
data->done_arg = cbarg;
38+
uv_async_send(&data->async);
39+
}
40+
41+
void Initialize(v8::Local<v8::Object> exports,
42+
v8::Local<v8::Value> module,
43+
v8::Local<v8::Context> context) {
44+
AsyncData* data = new AsyncData();
45+
data->isolate = context->GetIsolate();
46+
auto handle = node::AddEnvironmentCleanupHook(
47+
context->GetIsolate(),
48+
AsyncCleanupHook,
49+
data);
50+
data->handle = std::move(handle);
51+
52+
auto must_not_call_handle = node::AddEnvironmentCleanupHook(
53+
context->GetIsolate(),
54+
MustNotCall,
55+
nullptr);
56+
node::RemoveEnvironmentCleanupHook(std::move(must_not_call_handle));
57+
}
58+
59+
NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
Collapse file
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
}
8+
]
9+
}
Collapse file
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const path = require('path');
4+
const { Worker } = require('worker_threads');
5+
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
6+
7+
const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });
8+
w.on('exit', common.mustCall(() => require(binding)));

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.