Skip to content

Commit

Permalink
src: add AddCleanupHook
Browse files Browse the repository at this point in the history
Add CleanupHook support to Env

PR-URL: #1014
Reviewed-By: Michael Dawson <[email protected]>
  • Loading branch information
KevinEady authored and mhdawson committed Jul 27, 2021
1 parent a459f5c commit 10564a4
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 1 deletion.
64 changes: 64 additions & 0 deletions doc/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,67 @@ Associates a data item stored at `T* data` with the current instance of the
addon. The item will be passed to the function `fini` which gets called when an
instance of the addon is unloaded. This overload accepts an additional hint to
be passed to `fini`.

### AddCleanupHook

```cpp
template <typename Hook>
CleanupHook<Hook> AddCleanupHook(Hook hook);
```
- `[in] hook`: A function to call when the environment exists. Accepts a
function of the form `void ()`.
Registers `hook` as a function to be run once the current Node.js environment
exits. Unlike the underlying C-based Node-API, providing the same `hook`
multiple times **is** allowed. The hooks will be called in reverse order, i.e.
the most recently added one will be called first.
Returns an `Env::CleanupHook` object, which can be used to remove the hook via
its `Remove()` method.
### AddCleanupHook
```cpp
template <typename Hook, typename Arg>
CleanupHook<Hook, Arg> AddCleanupHook(Hook hook, Arg* arg);
```

- `[in] hook`: A function to call when the environment exists. Accepts a
function of the form `void (Arg* arg)`.
- `[in] arg`: A pointer to data that will be passed as the argument to `hook`.

Registers `hook` as a function to be run with the `arg` parameter once the
current Node.js environment exits. Unlike the underlying C-based Node-API,
providing the same `hook` and `arg` pair multiple times **is** allowed. The
hooks will be called in reverse order, i.e. the most recently added one will be
called first.

Returns an `Env::CleanupHook` object, which can be used to remove the hook via
its `Remove()` method.

# Env::CleanupHook

The `Env::CleanupHook` object allows removal of the hook added via
`Env::AddCleanupHook()`

## Methods

### IsEmpty

```cpp
bool IsEmpty();
```

Returns `true` if the cleanup hook was **not** successfully registered.

### Remove

```cpp
bool Remove(Env env);
```
Unregisters the hook from running once the current Node.js environment exits.
Returns `true` if the hook was successfully removed from the Node.js
environment.
67 changes: 67 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,26 @@ inline Value Env::RunScript(String script) {
return Value(_env, result);
}

#if NAPI_VERSION > 2
template <typename Hook, typename Arg>
void Env::CleanupHook<Hook, Arg>::Wrapper(void* data) NAPI_NOEXCEPT {
auto* cleanupData =
static_cast<typename Napi::Env::CleanupHook<Hook, Arg>::CleanupData*>(
data);
cleanupData->hook();
delete cleanupData;
}

template <typename Hook, typename Arg>
void Env::CleanupHook<Hook, Arg>::WrapperWithArg(void* data) NAPI_NOEXCEPT {
auto* cleanupData =
static_cast<typename Napi::Env::CleanupHook<Hook, Arg>::CleanupData*>(
data);
cleanupData->hook(static_cast<Arg*>(cleanupData->arg));
delete cleanupData;
}
#endif // NAPI_VERSION > 2

#if NAPI_VERSION > 5
template <typename T, Env::Finalizer<T> fini>
inline void Env::SetInstanceData(T* data) {
Expand Down Expand Up @@ -5725,6 +5745,53 @@ Addon<T>::DefineProperties(Object object,
}
#endif // NAPI_VERSION > 5

#if NAPI_VERSION > 2
template <typename Hook, typename Arg>
Env::CleanupHook<Hook, Arg> Env::AddCleanupHook(Hook hook, Arg* arg) {
return CleanupHook<Hook, Arg>(*this, hook, arg);
}

template <typename Hook>
Env::CleanupHook<Hook> Env::AddCleanupHook(Hook hook) {
return CleanupHook<Hook>(*this, hook);
}

template <typename Hook, typename Arg>
Env::CleanupHook<Hook, Arg>::CleanupHook(Napi::Env env, Hook hook)
: wrapper(Env::CleanupHook<Hook, Arg>::Wrapper) {
data = new CleanupData{std::move(hook), nullptr};
napi_status status = napi_add_env_cleanup_hook(env, wrapper, data);
if (status != napi_ok) {
delete data;
data = nullptr;
}
}

template <typename Hook, typename Arg>
Env::CleanupHook<Hook, Arg>::CleanupHook(Napi::Env env, Hook hook, Arg* arg)
: wrapper(Env::CleanupHook<Hook, Arg>::WrapperWithArg) {
data = new CleanupData{std::move(hook), arg};
napi_status status = napi_add_env_cleanup_hook(env, wrapper, data);
if (status != napi_ok) {
delete data;
data = nullptr;
}
}

template <class Hook, class Arg>
bool Env::CleanupHook<Hook, Arg>::Remove(Env env) {
napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data);
delete data;
data = nullptr;
return status == napi_ok;
}

template <class Hook, class Arg>
bool Env::CleanupHook<Hook, Arg>::IsEmpty() const {
return data == nullptr;
}
#endif // NAPI_VERSION > 2

} // namespace Napi

#endif // SRC_NAPI_INL_H_
35 changes: 34 additions & 1 deletion napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,12 @@ namespace Napi {
/// In the V8 JavaScript engine, a Node-API environment approximately
/// corresponds to an Isolate.
class Env {
private:
#if NAPI_VERSION > 2
template <typename Hook, typename Arg = void>
class CleanupHook;
#endif // NAPI_VERSION > 2
#if NAPI_VERSION > 5
private:
template <typename T> static void DefaultFini(Env, T* data);
template <typename DataType, typename HintType>
static void DefaultFiniWithHint(Env, DataType* data, HintType* hint);
Expand All @@ -201,6 +205,14 @@ namespace Napi {
Value RunScript(const std::string& utf8script);
Value RunScript(String script);

#if NAPI_VERSION > 2
template <typename Hook>
CleanupHook<Hook> AddCleanupHook(Hook hook);

template <typename Hook, typename Arg>
CleanupHook<Hook, Arg> AddCleanupHook(Hook hook, Arg* arg);
#endif // NAPI_VERSION > 2

#if NAPI_VERSION > 5
template <typename T> T* GetInstanceData();

Expand All @@ -219,7 +231,28 @@ namespace Napi {

private:
napi_env _env;

#if NAPI_VERSION > 2
template <typename Hook, typename Arg>
class CleanupHook {
public:
CleanupHook(Env env, Hook hook, Arg* arg);
CleanupHook(Env env, Hook hook);
bool Remove(Env env);
bool IsEmpty() const;

private:
static inline void Wrapper(void* data) NAPI_NOEXCEPT;
static inline void WrapperWithArg(void* data) NAPI_NOEXCEPT;

void (*wrapper)(void* arg);
struct CleanupData {
Hook hook;
Arg* arg;
} * data;
};
};
#endif // NAPI_VERSION > 2

/// A JavaScript value of unknown type.
///
Expand Down
4 changes: 4 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Object InitDate(Env env);
#endif
Object InitDataView(Env env);
Object InitDataViewReadWrite(Env env);
Object InitEnvCleanup(Env env);
Object InitError(Env env);
Object InitExternal(Env env);
Object InitFunction(Env env);
Expand Down Expand Up @@ -104,6 +105,9 @@ Object Init(Env env, Object exports) {
exports.Set("dataview", InitDataView(env));
exports.Set("dataview_read_write", InitDataView(env));
exports.Set("dataview_read_write", InitDataViewReadWrite(env));
#if (NAPI_VERSION > 2)
exports.Set("env_cleanup", InitEnvCleanup(env));
#endif
exports.Set("error", InitError(env));
exports.Set("external", InitExternal(env));
exports.Set("function", InitFunction(env));
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'callbackscope.cc',
'dataview/dataview.cc',
'dataview/dataview_read_write.cc',
'env_cleanup.cc',
'error.cc',
'external.cc',
'function.cc',
Expand Down
88 changes: 88 additions & 0 deletions test/env_cleanup.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include <stdio.h>
#include "napi.h"

using namespace Napi;

#if (NAPI_VERSION > 2)
namespace {

static void cleanup(void* arg) {
printf("static cleanup(%d)\n", *(int*)(arg));
}
static void cleanupInt(int* arg) {
printf("static cleanup(%d)\n", *(arg));
}

static void cleanupVoid() {
printf("static cleanup()\n");
}

static int secret1 = 42;
static int secret2 = 43;

Value AddHooks(const CallbackInfo& info) {
auto env = info.Env();

bool shouldRemove = info[0].As<Boolean>().Value();

// hook: void (*)(void *arg), hint: int
auto hook1 = env.AddCleanupHook(cleanup, &secret1);
// test using same hook+arg pair
auto hook1b = env.AddCleanupHook(cleanup, &secret1);

// hook: void (*)(int *arg), hint: int
auto hook2 = env.AddCleanupHook(cleanupInt, &secret2);

// hook: void (*)(int *arg), hint: void (default)
auto hook3 = env.AddCleanupHook(cleanupVoid);
// test using the same hook
auto hook3b = env.AddCleanupHook(cleanupVoid);

// hook: lambda []void (int *arg)->void, hint: int
auto hook4 = env.AddCleanupHook(
[&](int* arg) { printf("lambda cleanup(%d)\n", *arg); }, &secret1);

// hook: lambda []void (void *)->void, hint: void
auto hook5 =
env.AddCleanupHook([&](void*) { printf("lambda cleanup(void)\n"); },
static_cast<void*>(nullptr));

// hook: lambda []void ()->void, hint: void (default)
auto hook6 = env.AddCleanupHook([&]() { printf("lambda cleanup()\n"); });

if (shouldRemove) {
hook1.Remove(env);
hook1b.Remove(env);
hook2.Remove(env);
hook3.Remove(env);
hook3b.Remove(env);
hook4.Remove(env);
hook5.Remove(env);
hook6.Remove(env);
}

int added = 0;

added += !hook1.IsEmpty();
added += !hook1b.IsEmpty();
added += !hook2.IsEmpty();
added += !hook3.IsEmpty();
added += !hook3b.IsEmpty();
added += !hook4.IsEmpty();
added += !hook5.IsEmpty();
added += !hook6.IsEmpty();

return Number::New(env, added);
}

} // anonymous namespace

Object InitEnvCleanup(Env env) {
Object exports = Object::New(env);

exports["addHooks"] = Function::New(env, AddHooks);

return exports;
}

#endif
56 changes: 56 additions & 0 deletions test/env_cleanup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

const assert = require('assert');

if (process.argv[2] === 'runInChildProcess') {
const binding_path = process.argv[3];
const remove_hooks = process.argv[4] === 'true';

const binding = require(binding_path);
const actualAdded = binding.env_cleanup.addHooks(remove_hooks);
const expectedAdded = remove_hooks === true ? 0 : 8;
assert(actualAdded === expectedAdded, 'Incorrect number of hooks added');
}
else {
module.exports = require('./common').runTestWithBindingPath(test);
}

function test(bindingPath) {
for (const remove_hooks of [false, true]) {
const { status, output } = require('./napi_child').spawnSync(
process.execPath,
[
__filename,
'runInChildProcess',
bindingPath,
remove_hooks,
],
{ encoding: 'utf8' }
);

const stdout = output[1].trim();
/**
* There is no need to sort the lines, as per Node-API documentation:
* > The hooks will be called in reverse order, i.e. the most recently
* > added one will be called first.
*/
const lines = stdout.split(/[\r\n]+/);

assert(status === 0, `Process aborted with status ${status}`);

if (remove_hooks) {
assert.deepStrictEqual(lines, [''], 'Child process had console output when none expected')
} else {
assert.deepStrictEqual(lines, [
'lambda cleanup()',
'lambda cleanup(void)',
'lambda cleanup(42)',
'static cleanup()',
'static cleanup()',
'static cleanup(43)',
'static cleanup(42)',
'static cleanup(42)'
], 'Child process console output mismisatch')
}
}
}
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ if (process.env.NAPI_VERSION) {
console.log('napiVersion:' + napiVersion);

if (napiVersion < 3) {
testModules.splice(testModules.indexOf('env_cleanup'), 1);
testModules.splice(testModules.indexOf('callbackscope'), 1);
testModules.splice(testModules.indexOf('version_management'), 1);
}
Expand Down

0 comments on commit 10564a4

Please sign in to comment.