Skip to content

Commit ba26958

Browse files
committed
src: add environment cleanup hooks
This adds pairs of methods to the `Environment` class and to public APIs which can add and remove cleanup handlers. Unlike `AtExit`, this API targets addon developers rather than embedders, giving them (and Node’s internals) the ability to register per-`Environment` cleanup work. We may want to replace `AtExit` with this API at some point. Many thanks for Stephen Belanger for reviewing the original version of this commit in the Ayo.js project. Refs: ayojs/ayo#82 PR-URL: #19377 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent cecec46 commit ba26958

File tree

11 files changed

+241
-1
lines changed

11 files changed

+241
-1
lines changed

doc/api/n-api.md

+52
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,58 @@ If still valid, this API returns the `napi_value` representing the
886886
JavaScript `Object` associated with the `napi_ref`. Otherwise, result
887887
will be NULL.
888888

889+
### Cleanup on exit of the current Node.js instance
890+
891+
While a Node.js process typically releases all its resources when exiting,
892+
embedders of Node.js, or future Worker support, may require addons to register
893+
clean-up hooks that will be run once the current Node.js instance exits.
894+
895+
N-API provides functions for registering and un-registering such callbacks.
896+
When those callbacks are run, all resources that are being held by the addon
897+
should be freed up.
898+
899+
#### napi_add_env_cleanup_hook
900+
<!-- YAML
901+
added: REPLACEME
902+
-->
903+
```C
904+
NODE_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
905+
void (*fun)(void* arg),
906+
void* arg);
907+
```
908+
909+
Registers `fun` as a function to be run with the `arg` parameter once the
910+
current Node.js environment exits.
911+
912+
A function can safely be specified multiple times with different
913+
`arg` values. In that case, it will be called multiple times as well.
914+
Providing the same `fun` and `arg` values multiple times is not allowed
915+
and will lead the process to abort.
916+
917+
The hooks will be called in reverse order, i.e. the most recently added one
918+
will be called first.
919+
920+
Removing this hook can be done by using `napi_remove_env_cleanup_hook`.
921+
Typically, that happens when the resource for which this hook was added
922+
is being torn down anyway.
923+
924+
#### napi_remove_env_cleanup_hook
925+
<!-- YAML
926+
added: REPLACEME
927+
-->
928+
```C
929+
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
930+
void (*fun)(void* arg),
931+
void* arg);
932+
```
933+
934+
Unregisters `fun` as a function to be run with the `arg` parameter once the
935+
current Node.js environment exits. Both the argument and the function value
936+
need to be exact matches.
937+
938+
The function must have originally been registered
939+
with `napi_add_env_cleanup_hook`, otherwise the process will abort.
940+
889941
## Module registration
890942
N-API modules are registered in a manner similar to other modules
891943
except that instead of using the `NODE_MODULE` macro the following

src/env-inl.h

+23
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,29 @@ inline void Environment::SetTemplateMethod(v8::Local<v8::FunctionTemplate> that,
629629
t->SetClassName(name_string); // NODE_SET_METHOD() compatibility.
630630
}
631631

632+
void Environment::AddCleanupHook(void (*fn)(void*), void* arg) {
633+
auto insertion_info = cleanup_hooks_.emplace(CleanupHookCallback {
634+
fn, arg, cleanup_hook_counter_++
635+
});
636+
// Make sure there was no existing element with these values.
637+
CHECK_EQ(insertion_info.second, true);
638+
}
639+
640+
void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) {
641+
CleanupHookCallback search { fn, arg, 0 };
642+
cleanup_hooks_.erase(search);
643+
}
644+
645+
size_t Environment::CleanupHookCallback::Hash::operator()(
646+
const CleanupHookCallback& cb) const {
647+
return std::hash<void*>()(cb.arg_);
648+
}
649+
650+
bool Environment::CleanupHookCallback::Equal::operator()(
651+
const CleanupHookCallback& a, const CleanupHookCallback& b) const {
652+
return a.fn_ == b.fn_ && a.arg_ == b.arg_;
653+
}
654+
632655
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
633656
#define VS(PropertyName, StringValue) V(v8::String, PropertyName)
634657
#define V(TypeName, PropertyName) \

src/env.cc

+29
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,35 @@ void Environment::PrintSyncTrace() const {
305305
fflush(stderr);
306306
}
307307

308+
void Environment::RunCleanup() {
309+
while (!cleanup_hooks_.empty()) {
310+
// Copy into a vector, since we can't sort an unordered_set in-place.
311+
std::vector<CleanupHookCallback> callbacks(
312+
cleanup_hooks_.begin(), cleanup_hooks_.end());
313+
// We can't erase the copied elements from `cleanup_hooks_` yet, because we
314+
// need to be able to check whether they were un-scheduled by another hook.
315+
316+
std::sort(callbacks.begin(), callbacks.end(),
317+
[](const CleanupHookCallback& a, const CleanupHookCallback& b) {
318+
// Sort in descending order so that the most recently inserted callbacks
319+
// are run first.
320+
return a.insertion_order_counter_ > b.insertion_order_counter_;
321+
});
322+
323+
for (const CleanupHookCallback& cb : callbacks) {
324+
if (cleanup_hooks_.count(cb) == 0) {
325+
// This hook was removed from the `cleanup_hooks_` set during another
326+
// hook that was run earlier. Nothing to do here.
327+
continue;
328+
}
329+
330+
cb.fn_(cb.arg_);
331+
cleanup_hooks_.erase(cb);
332+
CleanupHandles();
333+
}
334+
}
335+
}
336+
308337
void Environment::RunBeforeExitCallbacks() {
309338
for (ExitCallback before_exit : before_exit_functions_) {
310339
before_exit.cb_(before_exit.arg_);

src/env.h

+31
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include <stdint.h>
4343
#include <vector>
4444
#include <unordered_map>
45+
#include <unordered_set>
4546

4647
struct nghttp2_rcbuf;
4748

@@ -775,6 +776,10 @@ class Environment {
775776

776777
v8::Local<v8::Value> GetNow();
777778

779+
inline void AddCleanupHook(void (*fn)(void*), void* arg);
780+
inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
781+
void RunCleanup();
782+
778783
private:
779784
inline void CreateImmediate(native_immediate_callback cb,
780785
void* data,
@@ -863,6 +868,32 @@ class Environment {
863868
void RunAndClearNativeImmediates();
864869
static void CheckImmediate(uv_check_t* handle);
865870

871+
struct CleanupHookCallback {
872+
void (*fn_)(void*);
873+
void* arg_;
874+
875+
// We keep track of the insertion order for these objects, so that we can
876+
// call the callbacks in reverse order when we are cleaning up.
877+
uint64_t insertion_order_counter_;
878+
879+
// Only hashes `arg_`, since that is usually enough to identify the hook.
880+
struct Hash {
881+
inline size_t operator()(const CleanupHookCallback& cb) const;
882+
};
883+
884+
// Compares by `fn_` and `arg_` being equal.
885+
struct Equal {
886+
inline bool operator()(const CleanupHookCallback& a,
887+
const CleanupHookCallback& b) const;
888+
};
889+
};
890+
891+
// Use an unordered_set, so that we have efficient insertion and removal.
892+
std::unordered_set<CleanupHookCallback,
893+
CleanupHookCallback::Hash,
894+
CleanupHookCallback::Equal> cleanup_hooks_;
895+
uint64_t cleanup_hook_counter_ = 0;
896+
866897
static void EnvPromiseHook(v8::PromiseHookType type,
867898
v8::Local<v8::Promise> promise,
868899
v8::Local<v8::Value> parent);

src/node.cc

+19-1
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,22 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
908908
env->AddPromiseHook(fn, arg);
909909
}
910910

911+
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
912+
void (*fun)(void* arg),
913+
void* arg) {
914+
Environment* env = Environment::GetCurrent(isolate);
915+
env->AddCleanupHook(fun, arg);
916+
}
917+
918+
919+
void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
920+
void (*fun)(void* arg),
921+
void* arg) {
922+
Environment* env = Environment::GetCurrent(isolate);
923+
env->RemoveCleanupHook(fun, arg);
924+
}
925+
926+
911927
CallbackScope::CallbackScope(Isolate* isolate,
912928
Local<Object> object,
913929
async_context asyncContext)
@@ -4463,7 +4479,7 @@ Environment* CreateEnvironment(IsolateData* isolate_data,
44634479

44644480

44654481
void FreeEnvironment(Environment* env) {
4466-
env->CleanupHandles();
4482+
env->RunCleanup();
44674483
delete env;
44684484
}
44694485

@@ -4561,6 +4577,8 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data,
45614577
env.set_trace_sync_io(false);
45624578

45634579
const int exit_code = EmitExit(&env);
4580+
4581+
env.RunCleanup();
45644582
RunAtExit(&env);
45654583

45664584
v8_platform.DrainVMTasks(isolate);

src/node.h

+13
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,19 @@ NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
583583
promise_hook_func fn,
584584
void* arg);
585585

586+
/* This is a lot like node::AtExit, except that the hooks added via this
587+
* function are run before the AtExit ones and will always be registered
588+
* for the current Environment instance.
589+
* These functions are safe to use in an addon supporting multiple
590+
* threads/isolates. */
591+
NODE_EXTERN void AddEnvironmentCleanupHook(v8::Isolate* isolate,
592+
void (*fun)(void* arg),
593+
void* arg);
594+
595+
NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
596+
void (*fun)(void* arg),
597+
void* arg);
598+
586599
/* Returns the id of the current execution context. If the return value is
587600
* zero then no execution has been set. This will happen if the user handles
588601
* I/O from native code. */

src/node_api.cc

+22
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,28 @@ void napi_module_register(napi_module* mod) {
902902
node::node_module_register(nm);
903903
}
904904

905+
napi_status napi_add_env_cleanup_hook(napi_env env,
906+
void (*fun)(void* arg),
907+
void* arg) {
908+
CHECK_ENV(env);
909+
CHECK_ARG(env, fun);
910+
911+
node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
912+
913+
return napi_ok;
914+
}
915+
916+
napi_status napi_remove_env_cleanup_hook(napi_env env,
917+
void (*fun)(void* arg),
918+
void* arg) {
919+
CHECK_ENV(env);
920+
CHECK_ARG(env, fun);
921+
922+
node::RemoveEnvironmentCleanupHook(env->isolate, fun, arg);
923+
924+
return napi_ok;
925+
}
926+
905927
// Warning: Keep in-sync with napi_status enum
906928
static
907929
const char* error_messages[] = {nullptr,

src/node_api.h

+7
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ EXTERN_C_START
118118

119119
NAPI_EXTERN void napi_module_register(napi_module* mod);
120120

121+
NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
122+
void (*fun)(void* arg),
123+
void* arg);
124+
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
125+
void (*fun)(void* arg),
126+
void* arg);
127+
121128
NAPI_EXTERN napi_status
122129
napi_get_last_error_info(napi_env env,
123130
const napi_extended_error_info** result);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include "node_api.h"
2+
#include "uv.h"
3+
#include "../common.h"
4+
5+
namespace {
6+
7+
void cleanup(void* arg) {
8+
printf("cleanup(%d)\n", *static_cast<int*>(arg));
9+
}
10+
11+
int secret = 42;
12+
int wrong_secret = 17;
13+
14+
napi_value Init(napi_env env, napi_value exports) {
15+
napi_add_env_cleanup_hook(env, cleanup, &wrong_secret);
16+
napi_add_env_cleanup_hook(env, cleanup, &secret);
17+
napi_remove_env_cleanup_hook(env, cleanup, &wrong_secret);
18+
19+
return nullptr;
20+
}
21+
22+
} // anonymous namespace
23+
24+
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
6+
'sources': [ 'binding.cc' ]
7+
}
8+
]
9+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const assert = require('assert');
4+
const child_process = require('child_process');
5+
6+
if (process.argv[2] === 'child') {
7+
require(`./build/${common.buildType}/binding`);
8+
} else {
9+
const { stdout } =
10+
child_process.spawnSync(process.execPath, [__filename, 'child']);
11+
assert.strictEqual(stdout.toString().trim(), 'cleanup(42)');
12+
}

0 commit comments

Comments
 (0)