Skip to content

Commit c38e5b3

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
1 parent a1fc528 commit c38e5b3

11 files changed

Lines changed: 233 additions & 1 deletion

File tree

doc/api/n-api.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,58 @@ If still valid, this API returns the `napi_value` representing the
905905
JavaScript Object associated with the `napi_ref`. Otherwise, result
906906
will be NULL.
907907

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

src/env-inl.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,29 @@ inline void Environment::SetTemplateMethod(v8::Local<v8::FunctionTemplate> that,
626626
t->SetClassName(name_string); // NODE_SET_METHOD() compatibility.
627627
}
628628

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

src/env.cc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,27 @@ 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+
cleanup_hooks_.clear();
314+
315+
std::sort(callbacks.begin(), callbacks.end(),
316+
[](const CleanupHookCallback& a, const CleanupHookCallback& b) {
317+
// Sort in descending order so that the most recently inserted callbacks
318+
// are run first.
319+
return a.insertion_order_counter_ > b.insertion_order_counter_;
320+
});
321+
322+
for (const CleanupHookCallback& cb : callbacks) {
323+
cb.fn_(cb.arg_);
324+
CleanupHandles();
325+
}
326+
}
327+
}
328+
308329
void Environment::RunBeforeExitCallbacks() {
309330
for (ExitCallback before_exit : before_exit_functions_) {
310331
before_exit.cb_(before_exit.arg_);

src/env.h

Lines changed: 31 additions & 0 deletions
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

@@ -773,6 +774,10 @@ class Environment {
773774

774775
v8::Local<v8::Value> GetNow();
775776

777+
inline void AddCleanupHook(void (*fn)(void*), void* arg);
778+
inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
779+
void RunCleanup();
780+
776781
private:
777782
inline void CreateImmediate(native_immediate_callback cb,
778783
void* data,
@@ -861,6 +866,32 @@ class Environment {
861866
void RunAndClearNativeImmediates();
862867
static void CheckImmediate(uv_check_t* handle);
863868

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

src/node.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,22 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
897897
env->AddPromiseHook(fn, arg);
898898
}
899899

900+
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
901+
void (*fun)(void* arg),
902+
void* arg) {
903+
Environment* env = Environment::GetCurrent(isolate);
904+
env->AddCleanupHook(fun, arg);
905+
}
906+
907+
908+
void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
909+
void (*fun)(void* arg),
910+
void* arg) {
911+
Environment* env = Environment::GetCurrent(isolate);
912+
env->RemoveCleanupHook(fun, arg);
913+
}
914+
915+
900916
CallbackScope::CallbackScope(Isolate* isolate,
901917
Local<Object> object,
902918
async_context asyncContext)
@@ -4429,7 +4445,7 @@ Environment* CreateEnvironment(IsolateData* isolate_data,
44294445

44304446

44314447
void FreeEnvironment(Environment* env) {
4432-
env->CleanupHandles();
4448+
env->RunCleanup();
44334449
delete env;
44344450
}
44354451

@@ -4522,6 +4538,8 @@ inline int Start(Isolate* isolate, IsolateData* isolate_data,
45224538
env.set_trace_sync_io(false);
45234539

45244540
const int exit_code = EmitExit(&env);
4541+
4542+
env.RunCleanup();
45254543
RunAtExit(&env);
45264544

45274545
v8_platform.DrainVMTasks(isolate);

src/node.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,19 @@ NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
578578
promise_hook_func fn,
579579
void* arg);
580580

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

src/node_api.cc

Lines changed: 22 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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);
Lines changed: 24 additions & 0 deletions
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)
Lines 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+
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
6+
'sources': [ 'binding.cc' ]
7+
}
8+
]
9+
}

0 commit comments

Comments
 (0)