Skip to content

Commit 10564a4

Browse files
KevinEadymhdawson
authored andcommitted
src: add AddCleanupHook
Add CleanupHook support to Env PR-URL: #1014 Reviewed-By: Michael Dawson <[email protected]>
1 parent a459f5c commit 10564a4

File tree

8 files changed

+315
-1
lines changed

8 files changed

+315
-1
lines changed

doc/env.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,67 @@ Associates a data item stored at `T* data` with the current instance of the
130130
addon. The item will be passed to the function `fini` which gets called when an
131131
instance of the addon is unloaded. This overload accepts an additional hint to
132132
be passed to `fini`.
133+
134+
### AddCleanupHook
135+
136+
```cpp
137+
template <typename Hook>
138+
CleanupHook<Hook> AddCleanupHook(Hook hook);
139+
```
140+
141+
- `[in] hook`: A function to call when the environment exists. Accepts a
142+
function of the form `void ()`.
143+
144+
Registers `hook` as a function to be run once the current Node.js environment
145+
exits. Unlike the underlying C-based Node-API, providing the same `hook`
146+
multiple times **is** allowed. The hooks will be called in reverse order, i.e.
147+
the most recently added one will be called first.
148+
149+
Returns an `Env::CleanupHook` object, which can be used to remove the hook via
150+
its `Remove()` method.
151+
152+
### AddCleanupHook
153+
154+
```cpp
155+
template <typename Hook, typename Arg>
156+
CleanupHook<Hook, Arg> AddCleanupHook(Hook hook, Arg* arg);
157+
```
158+
159+
- `[in] hook`: A function to call when the environment exists. Accepts a
160+
function of the form `void (Arg* arg)`.
161+
- `[in] arg`: A pointer to data that will be passed as the argument to `hook`.
162+
163+
Registers `hook` as a function to be run with the `arg` parameter once the
164+
current Node.js environment exits. Unlike the underlying C-based Node-API,
165+
providing the same `hook` and `arg` pair multiple times **is** allowed. The
166+
hooks will be called in reverse order, i.e. the most recently added one will be
167+
called first.
168+
169+
Returns an `Env::CleanupHook` object, which can be used to remove the hook via
170+
its `Remove()` method.
171+
172+
# Env::CleanupHook
173+
174+
The `Env::CleanupHook` object allows removal of the hook added via
175+
`Env::AddCleanupHook()`
176+
177+
## Methods
178+
179+
### IsEmpty
180+
181+
```cpp
182+
bool IsEmpty();
183+
```
184+
185+
Returns `true` if the cleanup hook was **not** successfully registered.
186+
187+
### Remove
188+
189+
```cpp
190+
bool Remove(Env env);
191+
```
192+
193+
Unregisters the hook from running once the current Node.js environment exits.
194+
195+
Returns `true` if the hook was successfully removed from the Node.js
196+
environment.

napi-inl.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,26 @@ inline Value Env::RunScript(String script) {
442442
return Value(_env, result);
443443
}
444444

445+
#if NAPI_VERSION > 2
446+
template <typename Hook, typename Arg>
447+
void Env::CleanupHook<Hook, Arg>::Wrapper(void* data) NAPI_NOEXCEPT {
448+
auto* cleanupData =
449+
static_cast<typename Napi::Env::CleanupHook<Hook, Arg>::CleanupData*>(
450+
data);
451+
cleanupData->hook();
452+
delete cleanupData;
453+
}
454+
455+
template <typename Hook, typename Arg>
456+
void Env::CleanupHook<Hook, Arg>::WrapperWithArg(void* data) NAPI_NOEXCEPT {
457+
auto* cleanupData =
458+
static_cast<typename Napi::Env::CleanupHook<Hook, Arg>::CleanupData*>(
459+
data);
460+
cleanupData->hook(static_cast<Arg*>(cleanupData->arg));
461+
delete cleanupData;
462+
}
463+
#endif // NAPI_VERSION > 2
464+
445465
#if NAPI_VERSION > 5
446466
template <typename T, Env::Finalizer<T> fini>
447467
inline void Env::SetInstanceData(T* data) {
@@ -5725,6 +5745,53 @@ Addon<T>::DefineProperties(Object object,
57255745
}
57265746
#endif // NAPI_VERSION > 5
57275747

5748+
#if NAPI_VERSION > 2
5749+
template <typename Hook, typename Arg>
5750+
Env::CleanupHook<Hook, Arg> Env::AddCleanupHook(Hook hook, Arg* arg) {
5751+
return CleanupHook<Hook, Arg>(*this, hook, arg);
5752+
}
5753+
5754+
template <typename Hook>
5755+
Env::CleanupHook<Hook> Env::AddCleanupHook(Hook hook) {
5756+
return CleanupHook<Hook>(*this, hook);
5757+
}
5758+
5759+
template <typename Hook, typename Arg>
5760+
Env::CleanupHook<Hook, Arg>::CleanupHook(Napi::Env env, Hook hook)
5761+
: wrapper(Env::CleanupHook<Hook, Arg>::Wrapper) {
5762+
data = new CleanupData{std::move(hook), nullptr};
5763+
napi_status status = napi_add_env_cleanup_hook(env, wrapper, data);
5764+
if (status != napi_ok) {
5765+
delete data;
5766+
data = nullptr;
5767+
}
5768+
}
5769+
5770+
template <typename Hook, typename Arg>
5771+
Env::CleanupHook<Hook, Arg>::CleanupHook(Napi::Env env, Hook hook, Arg* arg)
5772+
: wrapper(Env::CleanupHook<Hook, Arg>::WrapperWithArg) {
5773+
data = new CleanupData{std::move(hook), arg};
5774+
napi_status status = napi_add_env_cleanup_hook(env, wrapper, data);
5775+
if (status != napi_ok) {
5776+
delete data;
5777+
data = nullptr;
5778+
}
5779+
}
5780+
5781+
template <class Hook, class Arg>
5782+
bool Env::CleanupHook<Hook, Arg>::Remove(Env env) {
5783+
napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data);
5784+
delete data;
5785+
data = nullptr;
5786+
return status == napi_ok;
5787+
}
5788+
5789+
template <class Hook, class Arg>
5790+
bool Env::CleanupHook<Hook, Arg>::IsEmpty() const {
5791+
return data == nullptr;
5792+
}
5793+
#endif // NAPI_VERSION > 2
5794+
57285795
} // namespace Napi
57295796

57305797
#endif // SRC_NAPI_INL_H_

napi.h

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,12 @@ namespace Napi {
179179
/// In the V8 JavaScript engine, a Node-API environment approximately
180180
/// corresponds to an Isolate.
181181
class Env {
182+
private:
183+
#if NAPI_VERSION > 2
184+
template <typename Hook, typename Arg = void>
185+
class CleanupHook;
186+
#endif // NAPI_VERSION > 2
182187
#if NAPI_VERSION > 5
183-
private:
184188
template <typename T> static void DefaultFini(Env, T* data);
185189
template <typename DataType, typename HintType>
186190
static void DefaultFiniWithHint(Env, DataType* data, HintType* hint);
@@ -201,6 +205,14 @@ namespace Napi {
201205
Value RunScript(const std::string& utf8script);
202206
Value RunScript(String script);
203207

208+
#if NAPI_VERSION > 2
209+
template <typename Hook>
210+
CleanupHook<Hook> AddCleanupHook(Hook hook);
211+
212+
template <typename Hook, typename Arg>
213+
CleanupHook<Hook, Arg> AddCleanupHook(Hook hook, Arg* arg);
214+
#endif // NAPI_VERSION > 2
215+
204216
#if NAPI_VERSION > 5
205217
template <typename T> T* GetInstanceData();
206218

@@ -219,7 +231,28 @@ namespace Napi {
219231

220232
private:
221233
napi_env _env;
234+
235+
#if NAPI_VERSION > 2
236+
template <typename Hook, typename Arg>
237+
class CleanupHook {
238+
public:
239+
CleanupHook(Env env, Hook hook, Arg* arg);
240+
CleanupHook(Env env, Hook hook);
241+
bool Remove(Env env);
242+
bool IsEmpty() const;
243+
244+
private:
245+
static inline void Wrapper(void* data) NAPI_NOEXCEPT;
246+
static inline void WrapperWithArg(void* data) NAPI_NOEXCEPT;
247+
248+
void (*wrapper)(void* arg);
249+
struct CleanupData {
250+
Hook hook;
251+
Arg* arg;
252+
} * data;
253+
};
222254
};
255+
#endif // NAPI_VERSION > 2
223256

224257
/// A JavaScript value of unknown type.
225258
///

test/binding.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Object InitDate(Env env);
3030
#endif
3131
Object InitDataView(Env env);
3232
Object InitDataViewReadWrite(Env env);
33+
Object InitEnvCleanup(Env env);
3334
Object InitError(Env env);
3435
Object InitExternal(Env env);
3536
Object InitFunction(Env env);
@@ -104,6 +105,9 @@ Object Init(Env env, Object exports) {
104105
exports.Set("dataview", InitDataView(env));
105106
exports.Set("dataview_read_write", InitDataView(env));
106107
exports.Set("dataview_read_write", InitDataViewReadWrite(env));
108+
#if (NAPI_VERSION > 2)
109+
exports.Set("env_cleanup", InitEnvCleanup(env));
110+
#endif
107111
exports.Set("error", InitError(env));
108112
exports.Set("external", InitExternal(env));
109113
exports.Set("function", InitFunction(env));

test/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
'callbackscope.cc',
2323
'dataview/dataview.cc',
2424
'dataview/dataview_read_write.cc',
25+
'env_cleanup.cc',
2526
'error.cc',
2627
'external.cc',
2728
'function.cc',

test/env_cleanup.cc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#include <stdio.h>
2+
#include "napi.h"
3+
4+
using namespace Napi;
5+
6+
#if (NAPI_VERSION > 2)
7+
namespace {
8+
9+
static void cleanup(void* arg) {
10+
printf("static cleanup(%d)\n", *(int*)(arg));
11+
}
12+
static void cleanupInt(int* arg) {
13+
printf("static cleanup(%d)\n", *(arg));
14+
}
15+
16+
static void cleanupVoid() {
17+
printf("static cleanup()\n");
18+
}
19+
20+
static int secret1 = 42;
21+
static int secret2 = 43;
22+
23+
Value AddHooks(const CallbackInfo& info) {
24+
auto env = info.Env();
25+
26+
bool shouldRemove = info[0].As<Boolean>().Value();
27+
28+
// hook: void (*)(void *arg), hint: int
29+
auto hook1 = env.AddCleanupHook(cleanup, &secret1);
30+
// test using same hook+arg pair
31+
auto hook1b = env.AddCleanupHook(cleanup, &secret1);
32+
33+
// hook: void (*)(int *arg), hint: int
34+
auto hook2 = env.AddCleanupHook(cleanupInt, &secret2);
35+
36+
// hook: void (*)(int *arg), hint: void (default)
37+
auto hook3 = env.AddCleanupHook(cleanupVoid);
38+
// test using the same hook
39+
auto hook3b = env.AddCleanupHook(cleanupVoid);
40+
41+
// hook: lambda []void (int *arg)->void, hint: int
42+
auto hook4 = env.AddCleanupHook(
43+
[&](int* arg) { printf("lambda cleanup(%d)\n", *arg); }, &secret1);
44+
45+
// hook: lambda []void (void *)->void, hint: void
46+
auto hook5 =
47+
env.AddCleanupHook([&](void*) { printf("lambda cleanup(void)\n"); },
48+
static_cast<void*>(nullptr));
49+
50+
// hook: lambda []void ()->void, hint: void (default)
51+
auto hook6 = env.AddCleanupHook([&]() { printf("lambda cleanup()\n"); });
52+
53+
if (shouldRemove) {
54+
hook1.Remove(env);
55+
hook1b.Remove(env);
56+
hook2.Remove(env);
57+
hook3.Remove(env);
58+
hook3b.Remove(env);
59+
hook4.Remove(env);
60+
hook5.Remove(env);
61+
hook6.Remove(env);
62+
}
63+
64+
int added = 0;
65+
66+
added += !hook1.IsEmpty();
67+
added += !hook1b.IsEmpty();
68+
added += !hook2.IsEmpty();
69+
added += !hook3.IsEmpty();
70+
added += !hook3b.IsEmpty();
71+
added += !hook4.IsEmpty();
72+
added += !hook5.IsEmpty();
73+
added += !hook6.IsEmpty();
74+
75+
return Number::New(env, added);
76+
}
77+
78+
} // anonymous namespace
79+
80+
Object InitEnvCleanup(Env env) {
81+
Object exports = Object::New(env);
82+
83+
exports["addHooks"] = Function::New(env, AddHooks);
84+
85+
return exports;
86+
}
87+
88+
#endif

test/env_cleanup.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
5+
if (process.argv[2] === 'runInChildProcess') {
6+
const binding_path = process.argv[3];
7+
const remove_hooks = process.argv[4] === 'true';
8+
9+
const binding = require(binding_path);
10+
const actualAdded = binding.env_cleanup.addHooks(remove_hooks);
11+
const expectedAdded = remove_hooks === true ? 0 : 8;
12+
assert(actualAdded === expectedAdded, 'Incorrect number of hooks added');
13+
}
14+
else {
15+
module.exports = require('./common').runTestWithBindingPath(test);
16+
}
17+
18+
function test(bindingPath) {
19+
for (const remove_hooks of [false, true]) {
20+
const { status, output } = require('./napi_child').spawnSync(
21+
process.execPath,
22+
[
23+
__filename,
24+
'runInChildProcess',
25+
bindingPath,
26+
remove_hooks,
27+
],
28+
{ encoding: 'utf8' }
29+
);
30+
31+
const stdout = output[1].trim();
32+
/**
33+
* There is no need to sort the lines, as per Node-API documentation:
34+
* > The hooks will be called in reverse order, i.e. the most recently
35+
* > added one will be called first.
36+
*/
37+
const lines = stdout.split(/[\r\n]+/);
38+
39+
assert(status === 0, `Process aborted with status ${status}`);
40+
41+
if (remove_hooks) {
42+
assert.deepStrictEqual(lines, [''], 'Child process had console output when none expected')
43+
} else {
44+
assert.deepStrictEqual(lines, [
45+
'lambda cleanup()',
46+
'lambda cleanup(void)',
47+
'lambda cleanup(42)',
48+
'static cleanup()',
49+
'static cleanup()',
50+
'static cleanup(43)',
51+
'static cleanup(42)',
52+
'static cleanup(42)'
53+
], 'Child process console output mismisatch')
54+
}
55+
}
56+
}

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ if (process.env.NAPI_VERSION) {
8282
console.log('napiVersion:' + napiVersion);
8383

8484
if (napiVersion < 3) {
85+
testModules.splice(testModules.indexOf('env_cleanup'), 1);
8586
testModules.splice(testModules.indexOf('callbackscope'), 1);
8687
testModules.splice(testModules.indexOf('version_management'), 1);
8788
}

0 commit comments

Comments
 (0)