diff --git a/doc/env.md b/doc/env.md index 66575ea2c..c04b72529 100644 --- a/doc/env.md +++ b/doc/env.md @@ -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 +CleanupHook 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 +CleanupHook 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. diff --git a/napi-inl.h b/napi-inl.h index a0569eb4a..cc02af3ac 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -442,6 +442,26 @@ inline Value Env::RunScript(String script) { return Value(_env, result); } +#if NAPI_VERSION > 2 +template +void Env::CleanupHook::Wrapper(void* data) NAPI_NOEXCEPT { + auto* cleanupData = + static_cast::CleanupData*>( + data); + cleanupData->hook(); + delete cleanupData; +} + +template +void Env::CleanupHook::WrapperWithArg(void* data) NAPI_NOEXCEPT { + auto* cleanupData = + static_cast::CleanupData*>( + data); + cleanupData->hook(static_cast(cleanupData->arg)); + delete cleanupData; +} +#endif // NAPI_VERSION > 2 + #if NAPI_VERSION > 5 template fini> inline void Env::SetInstanceData(T* data) { @@ -5725,6 +5745,53 @@ Addon::DefineProperties(Object object, } #endif // NAPI_VERSION > 5 +#if NAPI_VERSION > 2 +template +Env::CleanupHook Env::AddCleanupHook(Hook hook, Arg* arg) { + return CleanupHook(*this, hook, arg); +} + +template +Env::CleanupHook Env::AddCleanupHook(Hook hook) { + return CleanupHook(*this, hook); +} + +template +Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook) + : wrapper(Env::CleanupHook::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 +Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook, Arg* arg) + : wrapper(Env::CleanupHook::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 +bool Env::CleanupHook::Remove(Env env) { + napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data); + delete data; + data = nullptr; + return status == napi_ok; +} + +template +bool Env::CleanupHook::IsEmpty() const { + return data == nullptr; +} +#endif // NAPI_VERSION > 2 + } // namespace Napi #endif // SRC_NAPI_INL_H_ diff --git a/napi.h b/napi.h index c3e02cb88..9f12a76bf 100644 --- a/napi.h +++ b/napi.h @@ -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 + class CleanupHook; +#endif // NAPI_VERSION > 2 #if NAPI_VERSION > 5 - private: template static void DefaultFini(Env, T* data); template static void DefaultFiniWithHint(Env, DataType* data, HintType* hint); @@ -201,6 +205,14 @@ namespace Napi { Value RunScript(const std::string& utf8script); Value RunScript(String script); +#if NAPI_VERSION > 2 + template + CleanupHook AddCleanupHook(Hook hook); + + template + CleanupHook AddCleanupHook(Hook hook, Arg* arg); +#endif // NAPI_VERSION > 2 + #if NAPI_VERSION > 5 template T* GetInstanceData(); @@ -219,7 +231,28 @@ namespace Napi { private: napi_env _env; + +#if NAPI_VERSION > 2 + template + 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. /// diff --git a/test/binding.cc b/test/binding.cc index 09bd110bc..267749394 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -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); @@ -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)); diff --git a/test/binding.gyp b/test/binding.gyp index 5cef552c8..f30d081d7 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -22,6 +22,7 @@ 'callbackscope.cc', 'dataview/dataview.cc', 'dataview/dataview_read_write.cc', + 'env_cleanup.cc', 'error.cc', 'external.cc', 'function.cc', diff --git a/test/env_cleanup.cc b/test/env_cleanup.cc new file mode 100644 index 000000000..44be0d5f7 --- /dev/null +++ b/test/env_cleanup.cc @@ -0,0 +1,88 @@ +#include +#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().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(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 diff --git a/test/env_cleanup.js b/test/env_cleanup.js new file mode 100644 index 000000000..48113caac --- /dev/null +++ b/test/env_cleanup.js @@ -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') + } + } +} diff --git a/test/index.js b/test/index.js index 69d13bbc0..2fe09ac2b 100644 --- a/test/index.js +++ b/test/index.js @@ -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); }