Skip to content

Commit 668ba1a

Browse files
addaleaxGabriel Schulhof
authored andcommitted
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: nodejs#19377 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent b5d4649 commit 668ba1a

File tree

11 files changed

+241
-0
lines changed

11 files changed

+241
-0
lines changed

doc/api/n-api.md

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

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

568+
void Environment::AddCleanupHook(void (*fn)(void*), void* arg) {
569+
auto insertion_info = cleanup_hooks_.emplace(CleanupHookCallback {
570+
fn, arg, cleanup_hook_counter_++
571+
});
572+
// Make sure there was no existing element with these values.
573+
CHECK_EQ(insertion_info.second, true);
574+
}
575+
576+
void Environment::RemoveCleanupHook(void (*fn)(void*), void* arg) {
577+
CleanupHookCallback search { fn, arg, 0 };
578+
cleanup_hooks_.erase(search);
579+
}
580+
581+
size_t Environment::CleanupHookCallback::Hash::operator()(
582+
const CleanupHookCallback& cb) const {
583+
return std::hash<void*>()(cb.arg_);
584+
}
585+
586+
bool Environment::CleanupHookCallback::Equal::operator()(
587+
const CleanupHookCallback& a, const CleanupHookCallback& b) const {
588+
return a.fn_ == b.fn_ && a.arg_ == b.arg_;
589+
}
590+
568591
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName, StringValue)
569592
#define VS(PropertyName, StringValue) V(v8::String, PropertyName, StringValue)
570593
#define V(TypeName, PropertyName, StringValue) \

src/env.cc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#endif
99

1010
#include <stdio.h>
11+
#include <algorithm>
1112

1213
namespace node {
1314

@@ -61,4 +62,34 @@ void Environment::PrintSyncTrace() const {
6162
fflush(stderr);
6263
}
6364

65+
void Environment::RunCleanup() {
66+
while (!cleanup_hooks_.empty()) {
67+
// Copy into a vector, since we can't sort an unordered_set in-place.
68+
std::vector<CleanupHookCallback> callbacks(
69+
cleanup_hooks_.begin(), cleanup_hooks_.end());
70+
// We can't erase the copied elements from `cleanup_hooks_` yet, because we
71+
// need to be able to check whether they were un-scheduled by another hook.
72+
73+
std::sort(callbacks.begin(), callbacks.end(),
74+
[](const CleanupHookCallback& a, const CleanupHookCallback& b) {
75+
// Sort in descending order so that the most recently inserted callbacks
76+
// are run first.
77+
return a.insertion_order_counter_ > b.insertion_order_counter_;
78+
});
79+
80+
for (const CleanupHookCallback& cb : callbacks) {
81+
if (cleanup_hooks_.count(cb) == 0) {
82+
// This hook was removed from the `cleanup_hooks_` set during another
83+
// hook that was run earlier. Nothing to do here.
84+
continue;
85+
}
86+
87+
cb.fn_(cb.arg_);
88+
cleanup_hooks_.erase(cb);
89+
// TODO(addaleax): Not calling CleanupHandles() here because it hangs in a
90+
// busy loop.
91+
}
92+
}
93+
}
94+
6495
} // namespace node

src/env.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <stdint.h>
1919
#include <vector>
20+
#include <unordered_set>
2021

2122
// Caveat emptor: we're going slightly crazy with macros here but the end
2223
// hopefully justifies the means. We have a lot of per-context properties
@@ -537,6 +538,10 @@ class Environment {
537538

538539
static const int kContextEmbedderDataIndex = NODE_CONTEXT_EMBEDDER_DATA_INDEX;
539540

541+
inline void AddCleanupHook(void (*fn)(void*), void* arg);
542+
inline void RemoveCleanupHook(void (*fn)(void*), void* arg);
543+
void RunCleanup();
544+
540545
private:
541546
inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>),
542547
const char* errmsg);
@@ -633,6 +638,32 @@ class Environment {
633638
DISALLOW_COPY_AND_ASSIGN(IsolateData);
634639
};
635640

641+
struct CleanupHookCallback {
642+
void (*fn_)(void*);
643+
void* arg_;
644+
645+
// We keep track of the insertion order for these objects, so that we can
646+
// call the callbacks in reverse order when we are cleaning up.
647+
uint64_t insertion_order_counter_;
648+
649+
// Only hashes `arg_`, since that is usually enough to identify the hook.
650+
struct Hash {
651+
inline size_t operator()(const CleanupHookCallback& cb) const;
652+
};
653+
654+
// Compares by `fn_` and `arg_` being equal.
655+
struct Equal {
656+
inline bool operator()(const CleanupHookCallback& a,
657+
const CleanupHookCallback& b) const;
658+
};
659+
};
660+
661+
// Use an unordered_set, so that we have efficient insertion and removal.
662+
std::unordered_set<CleanupHookCallback,
663+
CleanupHookCallback::Hash,
664+
CleanupHookCallback::Equal> cleanup_hooks_;
665+
uint64_t cleanup_hook_counter_ = 0;
666+
636667
DISALLOW_COPY_AND_ASSIGN(Environment);
637668
};
638669

src/node.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,20 @@ void SetupPromises(const FunctionCallbackInfo<Value>& args) {
12411241
FIXED_ONE_BYTE_STRING(isolate, "_setupPromises")).FromJust();
12421242
}
12431243

1244+
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
1245+
void (*fun)(void* arg),
1246+
void* arg) {
1247+
Environment* env = Environment::GetCurrent(isolate);
1248+
env->AddCleanupHook(fun, arg);
1249+
}
1250+
1251+
1252+
void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
1253+
void (*fun)(void* arg),
1254+
void* arg) {
1255+
Environment* env = Environment::GetCurrent(isolate);
1256+
env->RemoveCleanupHook(fun, arg);
1257+
}
12441258

12451259
Local<Value> MakeCallback(Environment* env,
12461260
Local<Value> recv,
@@ -3616,6 +3630,7 @@ void LoadEnvironment(Environment* env) {
36163630

36173631
void FreeEnvironment(Environment* env) {
36183632
CHECK_NE(env, nullptr);
3633+
env->RunCleanup();
36193634
env->Dispose();
36203635
}
36213636

@@ -4913,6 +4928,8 @@ static void StartNodeInstance(void* arg) {
49134928
int exit_code = EmitExit(env);
49144929
if (instance_data->is_main())
49154930
instance_data->set_exit_code(exit_code);
4931+
4932+
env->RunCleanup();
49164933
RunAtExit(env);
49174934

49184935
WaitForInspectorDisconnect(env);

src/node.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,19 @@ extern "C" NODE_EXTERN void node_module_register(void* mod);
495495
*/
496496
NODE_EXTERN void AtExit(void (*cb)(void* arg), void* arg = 0);
497497

498+
/* This is a lot like node::AtExit, except that the hooks added via this
499+
* function are run before the AtExit ones and will always be registered
500+
* for the current Environment instance.
501+
* These functions are safe to use in an addon supporting multiple
502+
* threads/isolates. */
503+
NODE_EXTERN void AddEnvironmentCleanupHook(v8::Isolate* isolate,
504+
void (*fun)(void* arg),
505+
void* arg);
506+
507+
NODE_EXTERN void RemoveEnvironmentCleanupHook(v8::Isolate* isolate,
508+
void (*fun)(void* arg),
509+
void* arg);
510+
498511
} // namespace node
499512

500513
#endif // SRC_NODE_H_

src/node_api.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,28 @@ void napi_module_register(napi_module* mod) {
922922
node::node_module_register(nm);
923923
}
924924

925+
napi_status napi_add_env_cleanup_hook(napi_env env,
926+
void (*fun)(void* arg),
927+
void* arg) {
928+
CHECK_ENV(env);
929+
CHECK_ARG(env, fun);
930+
931+
node::AddEnvironmentCleanupHook(env->isolate, fun, arg);
932+
933+
return napi_ok;
934+
}
935+
936+
napi_status napi_remove_env_cleanup_hook(napi_env env,
937+
void (*fun)(void* arg),
938+
void* arg) {
939+
CHECK_ENV(env);
940+
CHECK_ARG(env, fun);
941+
942+
node::RemoveEnvironmentCleanupHook(env->isolate, fun, arg);
943+
944+
return napi_ok;
945+
}
946+
925947
// Warning: Keep in-sync with napi_status enum
926948
static
927949
const char* error_messages[] = {nullptr,

src/node_api.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ EXTERN_C_START
9999

100100
NAPI_EXTERN void napi_module_register(napi_module* mod);
101101

102+
NAPI_EXTERN napi_status napi_add_env_cleanup_hook(napi_env env,
103+
void (*fun)(void* arg),
104+
void* arg);
105+
NAPI_EXTERN napi_status napi_remove_env_cleanup_hook(napi_env env,
106+
void (*fun)(void* arg),
107+
void* arg);
108+
102109
NAPI_EXTERN napi_status
103110
napi_get_last_error_info(napi_env env,
104111
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)