From b4aeecb046480eeaaf1c578a140f71ac0e77094f Mon Sep 17 00:00:00 2001 From: Kevin Eady <8634912+KevinEady@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:40:00 +0200 Subject: [PATCH] feat: add support for nogc types via `BasicEnv` (#1514) * src: introduce `NogcEnv`; support for nogc finalizers --- doc/README.md | 2 + doc/array_buffer.md | 30 +-- doc/basic_env.md | 200 ++++++++++++++++ doc/buffer.md | 50 ++-- doc/env.md | 142 +---------- doc/external.md | 17 +- doc/finalization.md | 151 ++++++++++++ doc/object_wrap.md | 22 +- napi-inl.h | 388 +++++++++++++++++++++++-------- napi.h | 82 +++++-- test/addon_build/tpl/binding.gyp | 2 +- test/binding.cc | 8 + test/binding.gyp | 1 + test/finalizer_order.cc | 152 ++++++++++++ test/finalizer_order.js | 98 ++++++++ 15 files changed, 1045 insertions(+), 300 deletions(-) create mode 100644 doc/basic_env.md create mode 100644 doc/finalization.md create mode 100644 test/finalizer_order.cc create mode 100644 test/finalizer_order.js diff --git a/doc/README.md b/doc/README.md index d6abb7107..bad1a5c5a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -37,6 +37,7 @@ The following is the documentation for node-addon-api. - [Full Class Hierarchy](hierarchy.md) - [Addon Structure](addon.md) - Data Types: + - [BasicEnv](basic_env.md) - [Env](env.md) - [CallbackInfo](callbackinfo.md) - [Reference](reference.md) @@ -70,6 +71,7 @@ The following is the documentation for node-addon-api. - [Object Lifetime Management](object_lifetime_management.md) - [HandleScope](handle_scope.md) - [EscapableHandleScope](escapable_handle_scope.md) + - [Finalization](finalization.md) - [Memory Management](memory_management.md) - [Async Operations](async_operations.md) - [AsyncWorker](async_worker.md) diff --git a/doc/array_buffer.md b/doc/array_buffer.md index 3be6b42a2..de05e55b3 100644 --- a/doc/array_buffer.md +++ b/doc/array_buffer.md @@ -31,13 +31,13 @@ Wraps the provided external data into a new `Napi::ArrayBuffer` instance. The `Napi::ArrayBuffer` instance does not assume ownership for the data and expects it to be valid for the lifetime of the instance. Since the `Napi::ArrayBuffer` is subject to garbage collection this overload is only -suitable for data which is static and never needs to be freed. -This factory method will not provide the caller with an opportunity to free the -data when the `Napi::ArrayBuffer` gets garbage-collected. If you need to free -the data retained by the `Napi::ArrayBuffer` object please use other -variants of the `Napi::ArrayBuffer::New` factory method that accept -`Napi::Finalizer`, which is a function that will be invoked when the -`Napi::ArrayBuffer` object has been destroyed. +suitable for data which is static and never needs to be freed. This factory +method will not provide the caller with an opportunity to free the data when the +`Napi::ArrayBuffer` gets garbage-collected. If you need to free the data +retained by the `Napi::ArrayBuffer` object please use other variants of the +`Napi::ArrayBuffer::New` factory method that accept `Napi::Finalizer`, which is +a function that will be invoked when the `Napi::ArrayBuffer` object has been +destroyed. See [Finalization][] for more details. ```cpp static Napi::ArrayBuffer Napi::ArrayBuffer::New(napi_env env, void* externalData, size_t byteLength); @@ -72,9 +72,9 @@ static Napi::ArrayBuffer Napi::ArrayBuffer::New(napi_env env, - `[in] env`: The environment in which to create the `Napi::ArrayBuffer` instance. - `[in] externalData`: The pointer to the external data to wrap. - `[in] byteLength`: The length of the `externalData`, in bytes. -- `[in] finalizeCallback`: A function to be called when the `Napi::ArrayBuffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `void*` (which is the - `externalData` pointer), and return `void`. +- `[in] finalizeCallback`: A function called when the engine destroys the + `Napi::ArrayBuffer` object, implementing `operator()(Napi::BasicEnv, void*)`. + See [Finalization][] for more details. Returns a new `Napi::ArrayBuffer` instance. @@ -102,11 +102,10 @@ static Napi::ArrayBuffer Napi::ArrayBuffer::New(napi_env env, - `[in] env`: The environment in which to create the `Napi::ArrayBuffer` instance. - `[in] externalData`: The pointer to the external data to wrap. - `[in] byteLength`: The length of the `externalData`, in bytes. -- `[in] finalizeCallback`: The function to be called when the `Napi::ArrayBuffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `void*` (which is the - `externalData` pointer) and `Hint*`, and return `void`. -- `[in] finalizeHint`: The hint to be passed as the second parameter of the - finalize callback. +- `[in] finalizeCallback`: A function called when the engine destroys the + `Napi::ArrayBuffer` object, implementing `operator()(Napi::BasicEnv, void*, + Hint*)`. See [Finalization][] for more details. +- `[in] finalizeHint`: The hint value passed to the `finalizeCallback` function. Returns a new `Napi::ArrayBuffer` instance. @@ -163,3 +162,4 @@ Returns `true` if this `ArrayBuffer` has been detached. [`Napi::Object`]: ./object.md [External Buffer]: ./external_buffer.md +[Finalization]: ./finalization.md diff --git a/doc/basic_env.md b/doc/basic_env.md new file mode 100644 index 000000000..7a5b430f1 --- /dev/null +++ b/doc/basic_env.md @@ -0,0 +1,200 @@ +# BasicEnv + +The data structure containing the environment in which the request is being run. + +The `Napi::BasicEnv` object is usually created and passed by the Node.js runtime +or node-addon-api infrastructure. + +The `Napi::BasicEnv` object represents an environment that has a limited subset +of APIs when compared to `Napi::Env` and can be used in basic finalizers. See +[Finalization][] for more details. + +## Methods + +### Constructor + +```cpp +Napi::BasicEnv::BasicEnv(node_api_nogc_env env); +``` + +- `[in] env`: The `node_api_nogc_env` environment from which to construct the + `Napi::BasicEnv` object. + +### node_api_nogc_env + +```cpp +operator node_api_nogc_env() const; +``` + +Returns the `node_api_nogc_env` opaque data structure representing the +environment. + +### GetInstanceData +```cpp +template T* GetInstanceData() const; +``` + +Returns the instance data that was previously associated with the environment, +or `nullptr` if none was associated. + +### SetInstanceData + + +```cpp +template using Finalizer = void (*)(Env, T*); +template fini = Env::DefaultFini> +void SetInstanceData(T* data) const; +``` + +- `[template] fini`: A function to call when the instance data is to be deleted. +Accepts a function of the form `void CleanupData(Napi::Env env, T* data)`. If +not given, the default finalizer will be used, which simply uses the `delete` +operator to destroy `T*` when the add-on instance is unloaded. +- `[in] data`: A pointer to data that will be associated with the instance of +the add-on for the duration of its lifecycle. + +Associates a data item stored at `T* data` with the current instance of the +add-on. The item will be passed to the function `fini` which gets called when an +instance of the add-on is unloaded. + +### SetInstanceData + +```cpp +template +using FinalizerWithHint = void (*)(Env, DataType*, HintType*); +template fini = + Env::DefaultFiniWithHint> +void SetInstanceData(DataType* data, HintType* hint) const; +``` + +- `[template] fini`: A function to call when the instance data is to be deleted. +Accepts a function of the form `void CleanupData(Napi::Env env, DataType* data, +HintType* hint)`. If not given, the default finalizer will be used, which simply +uses the `delete` operator to destroy `T*` when the add-on instance is unloaded. +- `[in] data`: A pointer to data that will be associated with the instance of +the add-on for the duration of its lifecycle. +- `[in] hint`: A pointer to data that will be associated with the instance of +the add-on for the duration of its lifecycle and will be passed as a hint to +`fini` when the add-on instance is unloaded. + +Associates a data item stored at `T* data` with the current instance of the +add-on. The item will be passed to the function `fini` which gets called when an +instance of the add-on is unloaded. This overload accepts an additional hint to +be passed to `fini`. + +### GetModuleFileName + +```cpp +const char* Napi::Env::GetModuleFileName() const; +``` + +Returns a URL containing the absolute path of the location from which the add-on +was loaded. For a file on the local file system it will start with `file://`. +The string is null-terminated and owned by env and must thus not be modified or +freed. It is only valid while the add-on is loaded. + +### AddCleanupHook + +```cpp +template +CleanupHook AddCleanupHook(Hook hook); +``` + +- `[in] hook`: A function to call when the environment exits. 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. + +### PostFinalizer + +```cpp +template +inline void PostFinalizer(FinalizerType finalizeCallback) const; +``` + +- `[in] finalizeCallback`: The function to queue for execution outside of the GC + finalization, implementing `operator()(Napi::Env)`. See [Finalization][] for + more details. + +### PostFinalizer + +```cpp +template +inline void PostFinalizer(FinalizerType finalizeCallback, T* data) const; +``` + +- `[in] finalizeCallback`: The function to queue for execution outside of the GC + finalization, implementing `operator()(Napi::Env, T*)`. See [Finalization][] + for more details. +- `[in] data`: The data to associate with the object. + +### PostFinalizer + +```cpp +template +inline void PostFinalizer(FinalizerType finalizeCallback, + T* data, + Hint* finalizeHint) const; +``` + +- `[in] finalizeCallback`: The function to queue for execution outside of the GC + finalization, implementing `operator()(Napi::Env, T*, Hint*)`. See + [Finalization][] for more details. +- `[in] data`: The data to associate with the object. +- `[in] finalizeHint`: The hint value passed to the `finalizeCallback` function. + +### AddCleanupHook + +```cpp +template +CleanupHook AddCleanupHook(Hook hook, Arg* arg); +``` + +- `[in] hook`: A function to call when the environment exits. 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. + +[Finalization]: ./finalization.md diff --git a/doc/buffer.md b/doc/buffer.md index 427eeee2f..548400481 100644 --- a/doc/buffer.md +++ b/doc/buffer.md @@ -27,16 +27,15 @@ Returns a new `Napi::Buffer` object. Wraps the provided external data into a new `Napi::Buffer` object. -The `Napi::Buffer` object does not assume ownership for the data and expects it to be -valid for the lifetime of the object. Since the `Napi::Buffer` is subject to garbage -collection this overload is only suitable for data which is static and never -needs to be freed. -This factory method will not provide the caller with an opportunity to free the -data when the `Napi::Buffer` gets garbage-collected. If you need to free the -data retained by the `Napi::Buffer` object please use other variants of the -`Napi::Buffer::New` factory method that accept `Napi::Finalizer`, which is a -function that will be invoked when the `Napi::Buffer` object has been -destroyed. +The `Napi::Buffer` object does not assume ownership for the data and expects it +to be valid for the lifetime of the object. Since the `Napi::Buffer` is subject +to garbage collection this overload is only suitable for data which is static +and never needs to be freed. This factory method will not provide the caller +with an opportunity to free the data when the `Napi::Buffer` gets +garbage-collected. If you need to free the data retained by the `Napi::Buffer` +object please use other variants of the `Napi::Buffer::New` factory method that +accept `Finalizer`, which is a function that will be invoked when the +`Napi::Buffer` object has been destroyed. See [Finalization][] for more details. ```cpp static Napi::Buffer Napi::Buffer::New(napi_env env, T* data, size_t length); @@ -70,9 +69,9 @@ static Napi::Buffer Napi::Buffer::New(napi_env env, - `[in] env`: The environment in which to create the `Napi::Buffer` object. - `[in] data`: The pointer to the external data to expose. - `[in] length`: The number of `T` elements in the external data. -- `[in] finalizeCallback`: The function to be called when the `Napi::Buffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `T*` (which is the - external data pointer), and return `void`. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::Buffer` object, implementing `operator()(Napi::BasicEnv, T*)`. See + [Finalization][] for more details. Returns a new `Napi::Buffer` object. @@ -99,11 +98,10 @@ static Napi::Buffer Napi::Buffer::New(napi_env env, - `[in] env`: The environment in which to create the `Napi::Buffer` object. - `[in] data`: The pointer to the external data to expose. - `[in] length`: The number of `T` elements in the external data. -- `[in] finalizeCallback`: The function to be called when the `Napi::Buffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `T*` (which is the - external data pointer) and `Hint*`, and return `void`. -- `[in] finalizeHint`: The hint to be passed as the second parameter of the - finalize callback. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::Buffer` object, implementing `operator()(Napi::BasicEnv, T*, Hint*)`. + See [Finalization][] for more details. +- `[in] finalizeHint`: The hint value passed to the `finalizeCallback` function. Returns a new `Napi::Buffer` object. @@ -157,9 +155,9 @@ static Napi::Buffer Napi::Buffer::NewOrCopy(napi_env env, - `[in] env`: The environment in which to create the `Napi::Buffer` object. - `[in] data`: The pointer to the external data to expose. - `[in] length`: The number of `T` elements in the external data. -- `[in] finalizeCallback`: The function to be called when the `Napi::Buffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `T*` (which is the - external data pointer), and return `void`. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::Buffer` object, implementing `operator()(Napi::BasicEnv, T*)`. See + [Finalization][] for more details. Returns a new `Napi::Buffer` object. @@ -186,11 +184,10 @@ static Napi::Buffer Napi::Buffer::NewOrCopy(napi_env env, - `[in] env`: The environment in which to create the `Napi::Buffer` object. - `[in] data`: The pointer to the external data to expose. - `[in] length`: The number of `T` elements in the external data. -- `[in] finalizeCallback`: The function to be called when the `Napi::Buffer` is - destroyed. It must implement `operator()`, accept an Napi::Env, a `T*` (which is the - external data pointer) and `Hint*`, and return `void`. -- `[in] finalizeHint`: The hint to be passed as the second parameter of the - finalize callback. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::Buffer` object, implementing `operator()(Napi::BasicEnv, T*, Hint*)`. + See [Finalization][] for more details. +- `[in] finalizeHint`: The hint value passed to the `finalizeCallback` function. Returns a new `Napi::Buffer` object. @@ -245,3 +242,4 @@ Returns the number of `T` elements in the external data. [`Napi::Uint8Array`]: ./typed_array_of.md [External Buffer]: ./external_buffer.md +[Finalization]: ./finalization.md diff --git a/doc/env.md b/doc/env.md index 29aa4459e..7773275ee 100644 --- a/doc/env.md +++ b/doc/env.md @@ -1,8 +1,15 @@ # Env -The opaque data structure containing the environment in which the request is being run. +Class `Napi::Env` inherits from class [`Napi::BasicEnv`][]. -The Env object is usually created and passed by the Node.js runtime or node-addon-api infrastructure. +The data structure containing the environment in which the request is being run. + +The `Napi::Env` object is usually created and passed by the Node.js runtime or +node-addon-api infrastructure. + +The `Napi::Env` object represents an environment that has a superset of APIs +when compared to `Napi::BasicEnv` and therefore _cannot_ be used in basic +finalizers. See [Finalization][] for more details. ## Methods @@ -76,132 +83,5 @@ The `script` can be any of the following types: - `const char *` - `const std::string &` -### GetInstanceData -```cpp -template T* GetInstanceData() const; -``` - -Returns the instance data that was previously associated with the environment, -or `nullptr` if none was associated. - -### SetInstanceData - -```cpp -template using Finalizer = void (*)(Env, T*); -template fini = Env::DefaultFini> -void SetInstanceData(T* data) const; -``` - -- `[template] fini`: A function to call when the instance data is to be deleted. -Accepts a function of the form `void CleanupData(Napi::Env env, T* data)`. If -not given, the default finalizer will be used, which simply uses the `delete` -operator to destroy `T*` when the addon instance is unloaded. -- `[in] data`: A pointer to data that will be associated with the instance of -the addon for the duration of its lifecycle. - -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. - -### SetInstanceData - -```cpp -template -using FinalizerWithHint = void (*)(Env, DataType*, HintType*); -template fini = - Env::DefaultFiniWithHint> -void SetInstanceData(DataType* data, HintType* hint) const; -``` - -- `[template] fini`: A function to call when the instance data is to be deleted. -Accepts a function of the form -`void CleanupData(Napi::Env env, DataType* data, HintType* hint)`. If not given, -the default finalizer will be used, which simply uses the `delete` operator to -destroy `T*` when the addon instance is unloaded. -- `[in] data`: A pointer to data that will be associated with the instance of -the addon for the duration of its lifecycle. -- `[in] hint`: A pointer to data that will be associated with the instance of -the addon for the duration of its lifecycle and will be passed as a hint to -`fini` when the addon instance is unloaded. - -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`. - -### GetModuleFileName - -```cpp -const char* Napi::Env::GetModuleFileName() const; -``` - -Returns a A URL containing the absolute path of the location from which the -add-on was loaded. For a file on the local file system it will start with -`file://`. The string is null-terminated and owned by env and must thus not be -modified or freed. It is only valid while the add-on is loaded. - -### AddCleanupHook - -```cpp -template -CleanupHook AddCleanupHook(Hook hook); -``` - -- `[in] hook`: A function to call when the environment exits. 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 exits. 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. +[`Napi::BasicEnv`]: ./basic_env.md +[Finalization]: ./finalization.md diff --git a/doc/external.md b/doc/external.md index ce42e112a..4b4603e8e 100644 --- a/doc/external.md +++ b/doc/external.md @@ -4,7 +4,11 @@ Class `Napi::External` inherits from class [`Napi::TypeTaggable`][]. The `Napi::External` template class implements the ability to create a `Napi::Value` object with arbitrary C++ data. It is the user's responsibility to manage the memory for the arbitrary C++ data. -`Napi::External` objects can be created with an optional Finalizer function and optional Hint value. The Finalizer function, if specified, is called when your `Napi::External` object is released by Node's garbage collector. It gives your code the opportunity to free any dynamically created data. If you specify a Hint value, it is passed to your Finalizer function. +`Napi::External` objects can be created with an optional Finalizer function and +optional Hint value. The `Finalizer` function, if specified, is called when your +`Napi::External` object is released by Node's garbage collector. It gives your +code the opportunity to free any dynamically created data. If you specify a Hint +value, it is passed to your `Finalizer` function. See [Finalization][] for more details. Note that `Napi::Value::IsExternal()` will return `true` for any external value. It does not differentiate between the templated parameter `T` in @@ -38,7 +42,9 @@ static Napi::External Napi::External::New(napi_env env, - `[in] env`: The `napi_env` environment in which to construct the `Napi::External` object. - `[in] data`: The arbitrary C++ data to be held by the `Napi::External` object. -- `[in] finalizeCallback`: A function called when the `Napi::External` object is released by the garbage collector accepting a T* and returning void. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::External` object, implementing `operator()(Napi::BasicEnv, T*)`. See + [Finalization][] for more details. Returns the created `Napi::External` object. @@ -54,8 +60,10 @@ static Napi::External Napi::External::New(napi_env env, - `[in] env`: The `napi_env` environment in which to construct the `Napi::External` object. - `[in] data`: The arbitrary C++ data to be held by the `Napi::External` object. -- `[in] finalizeCallback`: A function called when the `Napi::External` object is released by the garbage collector accepting T* and Hint* parameters and returning void. -- `[in] finalizeHint`: A hint value passed to the `finalizeCallback` function. +- `[in] finalizeCallback`: The function called when the engine destroys the + `Napi::External` object, implementing `operator()(Napi::BasicEnv, T*, Hint*)`. + See [Finalization][] for more details. +- `[in] finalizeHint`: The hint value passed to the `finalizeCallback` function. Returns the created `Napi::External` object. @@ -67,4 +75,5 @@ T* Napi::External::Data() const; Returns a pointer to the arbitrary C++ data held by the `Napi::External` object. +[Finalization]: ./finalization.md [`Napi::TypeTaggable`]: ./type_taggable.md diff --git a/doc/finalization.md b/doc/finalization.md new file mode 100644 index 000000000..825ff742a --- /dev/null +++ b/doc/finalization.md @@ -0,0 +1,151 @@ +# Finalization + +Various node-addon-api methods accept a templated `Finalizer finalizeCallback` +parameter. This parameter represents a native callback function that runs in +response to a garbage collection event. A finalizer is considered a _basic_ +finalizer if the callback only utilizes a certain subset of APIs, which may +provide more efficient memory management, optimizations, improved execution, or +other benefits. + +In general, it is best to use basic finalizers whenever possible (eg. when +access to JavaScript is _not_ needed). + +## Finalizers + +The callback takes `Napi::Env` as its first argument: + +### Example + +```cpp +Napi::External::New(Env(), new int(1), [](Napi::Env env, int* data) { + env.RunScript("console.log('Finalizer called')"); + delete data; +}); +``` + +## Basic Finalizers + +Use of basic finalizers may allow the engine to perform optimizations when +scheduling or executing the callback. For example, V8 does not allow access to +the engine heap during garbage collection. Restricting finalizers from accessing +the engine heap allows the callback to execute during garbage collection, +providing a chance to free native memory eagerly. + +In general, APIs that access engine heap are not allowed in basic finalizers. + +The callback takes `Napi::BasicEnv` as its first argument: + +### Example + +```cpp +Napi::ArrayBuffer::New( + Env(), data, length, [](Napi::BasicEnv /*env*/, void* finalizeData) { + delete[] static_cast(finalizeData); + }); +``` + +## Scheduling Finalizers + +In addition to passing finalizers to `Napi::External`s and other Node-API +constructs, `Napi::BasicEnv::PostFinalize(Napi::BasicEnv, Finalizer)` can be +used to schedule a callback to run outside of the garbage collector +finalization. Since the associated native memory may already be freed by the +basic finalizer, any additional data may be passed eg. via the finalizer's +parameters (`T data*`, `Hint hint*`) or via lambda capture. This allows for +freeing native data in a basic finalizer, while executing any JavaScript code in +an additional finalizer. + +### Example + +```cpp +// Native Add-on + +#include +#include +#include "napi.h" + +using namespace Napi; + +// A structure representing some data that uses a "large" amount of memory. +class LargeData { + public: + LargeData() : id(instances++) {} + size_t id; + + static size_t instances; +}; + +size_t LargeData::instances = 0; + +// Basic finalizer to free `LargeData`. Takes ownership of the pointer and +// frees its memory after use. +void MyBasicFinalizer(Napi::BasicEnv env, LargeData* data) { + std::unique_ptr instance(data); + std::cout << "Basic finalizer for instance " << instance->id + << " called\n"; + + // Register a finalizer. Since the instance will be deleted by + // the time this callback executes, pass the instance's `id` via lambda copy + // capture and _not_ a reference capture that accesses `this`. + env.PostFinalizer([instanceId = instance->id](Napi::Env env) { + env.RunScript("console.log('Finalizer for instance " + + std::to_string(instanceId) + " called');"); + }); + + // Free the `LargeData` held in `data` once `instance` goes out of scope. +} + +Value CreateExternal(const CallbackInfo& info) { + // Create a new instance of LargeData. + auto instance = std::make_unique(); + + // Wrap the instance in an External object, registering a basic + // finalizer that will delete the instance to free the "large" amount of + // memory. + return External::New(info.Env(), instance.release(), MyBasicFinalizer); +} + +Object Init(Napi::Env env, Object exports) { + exports["createExternal"] = Function::New(env, CreateExternal); + return exports; +} + +NODE_API_MODULE(addon, Init) +``` + +```js +// JavaScript + +const { createExternal } = require('./addon.node'); + +for (let i = 0; i < 5; i++) { + const ext = createExternal(); + // ... do something with `ext` .. +} + +console.log('Loop complete'); +await new Promise(resolve => setImmediate(resolve)); +console.log('Next event loop cycle'); +``` + +Possible output: + +``` +Basic finalizer for instance 0 called +Basic finalizer for instance 1 called +Basic finalizer for instance 2 called +Basic finalizer for instance 3 called +Basic finalizer for instance 4 called +Loop complete +Finalizer for instance 3 called +Finalizer for instance 4 called +Finalizer for instance 1 called +Finalizer for instance 2 called +Finalizer for instance 0 called +Next event loop cycle +``` + +If the garbage collector runs during the loop, the basic finalizers execute and +display their logging message synchronously during the loop execution. The +additional finalizers execute at some later point after the garbage collection +cycle. diff --git a/doc/object_wrap.md b/doc/object_wrap.md index 43546646a..40fb3bf12 100644 --- a/doc/object_wrap.md +++ b/doc/object_wrap.md @@ -241,9 +241,24 @@ request being made. ### Finalize -Provides an opportunity to run cleanup code that requires access to the -`Napi::Env` before the wrapped native object instance is freed. Override to -implement. +Provides an opportunity to run cleanup code that only utilizes basic Node APIs, if any. +Override to implement. See [Finalization][] for more details. + +```cpp +virtual void Finalize(Napi::BasicEnv env); +``` + +- `[in] env`: `Napi::Env`. + +### Finalize + +Provides an opportunity to run cleanup code that utilizes non-basic Node APIs. +Override to implement. + +*NOTE*: Defining this method causes the deletion of the underlying `T* data` to +be postponed until _after_ the garbage collection cycle. Since an `Napi::Env` +has access to non-basic Node APIs, it cannot run in the same current tick as the +garbage collector. ```cpp virtual void Finalize(Napi::Env env); @@ -586,3 +601,4 @@ Returns `Napi::PropertyDescriptor` object that represents an static value property of a JavaScript class [`Napi::InstanceWrap`]: ./instance_wrap.md +[Finalization]: ./finalization.md diff --git a/napi-inl.h b/napi-inl.h index eb520a022..a66f2cbcf 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -34,25 +34,10 @@ namespace details { // Node.js releases. Only necessary when they are used in napi.h and napi-inl.h. constexpr int napi_no_external_buffers_allowed = 22; -#if (defined(NAPI_EXPERIMENTAL) && \ - defined(NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER)) -template -inline void PostFinalizerWrapper(node_api_nogc_env nogc_env, - void* data, - void* hint) { - napi_status status = node_api_post_finalizer(nogc_env, finalizer, data, hint); - NAPI_FATAL_IF_FAILED( - status, "PostFinalizerWrapper", "node_api_post_finalizer failed"); -} -#else -template -inline void PostFinalizerWrapper(napi_env env, void* data, void* hint) { - finalizer(env, data, hint); -} -#endif - template -inline void default_finalizer(napi_env /*env*/, void* data, void* /*hint*/) { +inline void default_basic_finalizer(node_api_nogc_env /*env*/, + void* data, + void* /*hint*/) { delete static_cast(data); } @@ -61,7 +46,7 @@ inline void default_finalizer(napi_env /*env*/, void* data, void* /*hint*/) { // TODO: Replace this code with `napi_add_finalizer()` whenever it becomes // available on all supported versions of Node.js. template > + node_api_nogc_finalize finalizer = default_basic_finalizer> inline napi_status AttachData(napi_env env, napi_value obj, FreeType* data, @@ -85,8 +70,7 @@ inline napi_status AttachData(napi_env env, } } #else // NAPI_VERSION >= 5 - status = napi_add_finalizer( - env, obj, data, details::PostFinalizerWrapper, hint, nullptr); + status = napi_add_finalizer(env, obj, data, finalizer, hint, nullptr); #endif return status; } @@ -206,23 +190,92 @@ napi_value TemplatedInstanceVoidCallback(napi_env env, napi_callback_info info) template struct FinalizeData { - static inline void Wrapper(napi_env env, +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >> +#endif + static inline void Wrapper(node_api_nogc_env env, void* data, void* finalizeHint) NAPI_NOEXCEPT { WrapVoidCallback([&] { FinalizeData* finalizeData = static_cast(finalizeHint); - finalizeData->callback(Env(env), static_cast(data)); + finalizeData->callback(env, static_cast(data)); delete finalizeData; }); } - static inline void WrapperWithHint(napi_env env, +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >, + typename = void> + static inline void Wrapper(node_api_nogc_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + napi_status status = + node_api_post_finalizer(env, WrapperGC, data, finalizeHint); + NAPI_FATAL_IF_FAILED( + status, "FinalizeData::Wrapper", "node_api_post_finalizer failed"); + } +#endif + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >> +#endif + static inline void WrapperWithHint(node_api_nogc_env env, void* data, void* finalizeHint) NAPI_NOEXCEPT { WrapVoidCallback([&] { FinalizeData* finalizeData = static_cast(finalizeHint); - finalizeData->callback( - Env(env), static_cast(data), finalizeData->hint); + finalizeData->callback(env, static_cast(data), finalizeData->hint); + delete finalizeData; + }); + } + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template >, + typename = void> + static inline void WrapperWithHint(node_api_nogc_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + napi_status status = + node_api_post_finalizer(env, WrapperGCWithHint, data, finalizeHint); + NAPI_FATAL_IF_FAILED( + status, "FinalizeData::Wrapper", "node_api_post_finalizer failed"); + } +#endif + + static inline void WrapperGCWithoutData(napi_env env, + void* /*data*/, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env); + delete finalizeData; + }); + } + + static inline void WrapperGC(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data)); + delete finalizeData; + }); + } + + static inline void WrapperGCWithHint(napi_env env, + void* data, + void* finalizeHint) NAPI_NOEXCEPT { + WrapVoidCallback([&] { + FinalizeData* finalizeData = static_cast(finalizeHint); + finalizeData->callback(env, static_cast(data), finalizeData->hint); delete finalizeData; }); } @@ -373,6 +426,34 @@ inline std::string StringFormat(const char* format, ...) { return result; } +template +class HasExtendedFinalizer { + private: + template + struct SFINAE {}; + template + static char test(SFINAE*); + template + static int test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(char); +}; + +template +class HasBasicFinalizer { + private: + template + struct SFINAE {}; + template + static char test(SFINAE*); + template + static int test(...); + + public: + static constexpr bool value = sizeof(test(0)) == sizeof(char); +}; + } // namespace details #ifndef NODE_ADDON_API_DISABLE_DEPRECATED @@ -482,15 +563,21 @@ inline Maybe Just(const T& t) { } //////////////////////////////////////////////////////////////////////////////// -// Env class +// BasicEnv / Env class //////////////////////////////////////////////////////////////////////////////// -inline Env::Env(napi_env env) : _env(env) {} +inline BasicEnv::BasicEnv(node_api_nogc_env env) : _env(env) {} -inline Env::operator napi_env() const { +inline BasicEnv::operator node_api_nogc_env() const { return _env; } +inline Env::Env(napi_env env) : BasicEnv(env) {} + +inline Env::operator napi_env() const { + return const_cast(_env); +} + inline Object Env::Global() const { napi_value value; napi_status status = napi_get_global(*this, &value); @@ -514,7 +601,7 @@ inline Value Env::Null() const { inline bool Env::IsExceptionPending() const { bool result; - napi_status status = napi_is_exception_pending(_env, &result); + napi_status status = napi_is_exception_pending(*this, &result); if (status != napi_ok) result = false; // Checking for a pending exception shouldn't throw. return result; @@ -522,16 +609,16 @@ inline bool Env::IsExceptionPending() const { inline Error Env::GetAndClearPendingException() const { napi_value value; - napi_status status = napi_get_and_clear_last_exception(_env, &value); + napi_status status = napi_get_and_clear_last_exception(*this, &value); if (status != napi_ok) { // Don't throw another exception when failing to get the exception! return Error(); } - return Error(_env, value); + return Error(*this, value); } inline MaybeOrValue Env::RunScript(const char* utf8script) const { - String script = String::New(_env, utf8script); + String script = String::New(*this, utf8script); return RunScript(script); } @@ -541,46 +628,46 @@ inline MaybeOrValue Env::RunScript(const std::string& utf8script) const { inline MaybeOrValue Env::RunScript(String script) const { napi_value result; - napi_status status = napi_run_script(_env, script, &result); + napi_status status = napi_run_script(*this, script, &result); NAPI_RETURN_OR_THROW_IF_FAILED( - _env, status, Napi::Value(_env, result), Napi::Value); + *this, status, Napi::Value(*this, result), Napi::Value); } #if NAPI_VERSION > 2 template -void Env::CleanupHook::Wrapper(void* data) NAPI_NOEXCEPT { - auto* cleanupData = - static_cast::CleanupData*>( - data); +void BasicEnv::CleanupHook::Wrapper(void* data) NAPI_NOEXCEPT { + auto* cleanupData = static_cast< + typename Napi::BasicEnv::CleanupHook::CleanupData*>(data); cleanupData->hook(); delete cleanupData; } template -void Env::CleanupHook::WrapperWithArg(void* data) NAPI_NOEXCEPT { - auto* cleanupData = - static_cast::CleanupData*>( - data); +void BasicEnv::CleanupHook::WrapperWithArg(void* data) + NAPI_NOEXCEPT { + auto* cleanupData = static_cast< + typename Napi::BasicEnv::CleanupHook::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) const { +template fini> +inline void BasicEnv::SetInstanceData(T* data) const { napi_status status = napi_set_instance_data( _env, data, [](napi_env env, void* data, void*) { fini(env, static_cast(data)); }, nullptr); - NAPI_THROW_IF_FAILED_VOID(_env, status); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::SetInstanceData", "invalid arguments"); } template fini> -inline void Env::SetInstanceData(DataType* data, HintType* hint) const { + Napi::BasicEnv::FinalizerWithHint fini> +inline void BasicEnv::SetInstanceData(DataType* data, HintType* hint) const { napi_status status = napi_set_instance_data( _env, data, @@ -588,35 +675,38 @@ inline void Env::SetInstanceData(DataType* data, HintType* hint) const { fini(env, static_cast(data), static_cast(hint)); }, hint); - NAPI_THROW_IF_FAILED_VOID(_env, status); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::SetInstanceData", "invalid arguments"); } template -inline T* Env::GetInstanceData() const { +inline T* BasicEnv::GetInstanceData() const { void* data = nullptr; napi_status status = napi_get_instance_data(_env, &data); - NAPI_THROW_IF_FAILED(_env, status, nullptr); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::GetInstanceData", "invalid arguments"); return static_cast(data); } template -void Env::DefaultFini(Env, T* data) { +void BasicEnv::DefaultFini(Env, T* data) { delete data; } template -void Env::DefaultFiniWithHint(Env, DataType* data, HintType*) { +void BasicEnv::DefaultFiniWithHint(Env, DataType* data, HintType*) { delete data; } #endif // NAPI_VERSION > 5 #if NAPI_VERSION > 8 -inline const char* Env::GetModuleFileName() const { +inline const char* BasicEnv::GetModuleFileName() const { const char* result; napi_status status = node_api_get_module_file_name(_env, &result); - NAPI_THROW_IF_FAILED(*this, status, nullptr); + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::GetModuleFileName", "invalid arguments"); return result; } #endif // NAPI_VERSION > 8 @@ -1805,8 +1895,7 @@ inline External External::New(napi_env env, napi_status status = napi_create_external(env, data, - details::PostFinalizerWrapper< - details::FinalizeData::Wrapper>, + details::FinalizeData::Wrapper, finalizeData, &value); if (status != napi_ok) { @@ -1829,8 +1918,7 @@ inline External External::New(napi_env env, napi_status status = napi_create_external( env, data, - details::PostFinalizerWrapper< - details::FinalizeData::WrapperWithHint>, + details::FinalizeData::WrapperWithHint, finalizeData, &value); if (status != napi_ok) { @@ -1941,8 +2029,7 @@ inline ArrayBuffer ArrayBuffer::New(napi_env env, env, externalData, byteLength, - details::PostFinalizerWrapper< - details::FinalizeData::Wrapper>, + details::FinalizeData::Wrapper, finalizeData, &value); if (status != napi_ok) { @@ -1967,8 +2054,7 @@ inline ArrayBuffer ArrayBuffer::New(napi_env env, env, externalData, byteLength, - details::PostFinalizerWrapper< - details::FinalizeData::WrapperWithHint>, + details::FinalizeData::WrapperWithHint, finalizeData, &value); if (status != napi_ok) { @@ -2684,14 +2770,13 @@ inline Buffer Buffer::New(napi_env env, details::FinalizeData* finalizeData = new details::FinalizeData( {std::move(finalizeCallback), nullptr}); - napi_status status = napi_create_external_buffer( - env, - length * sizeof(T), - data, - details::PostFinalizerWrapper< - details::FinalizeData::Wrapper>, - finalizeData, - &value); + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); if (status != napi_ok) { delete finalizeData; NAPI_THROW_IF_FAILED(env, status, Buffer()); @@ -2714,8 +2799,7 @@ inline Buffer Buffer::New(napi_env env, env, length * sizeof(T), data, - details::PostFinalizerWrapper< - details::FinalizeData::WrapperWithHint>, + details::FinalizeData::WrapperWithHint, finalizeData, &value); if (status != napi_ok) { @@ -2754,19 +2838,18 @@ inline Buffer Buffer::NewOrCopy(napi_env env, {std::move(finalizeCallback), nullptr}); #ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED napi_value value; - napi_status status = napi_create_external_buffer( - env, - length * sizeof(T), - data, - details::PostFinalizerWrapper< - details::FinalizeData::Wrapper>, - finalizeData, - &value); + napi_status status = + napi_create_external_buffer(env, + length * sizeof(T), + data, + details::FinalizeData::Wrapper, + finalizeData, + &value); if (status == details::napi_no_external_buffers_allowed) { #endif // NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED // If we can't create an external buffer, we'll just copy the data. Buffer ret = Buffer::Copy(env, data, length); - details::FinalizeData::Wrapper(env, data, finalizeData); + details::FinalizeData::WrapperGC(env, data, finalizeData); return ret; #ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED } @@ -2794,15 +2877,14 @@ inline Buffer Buffer::NewOrCopy(napi_env env, env, length * sizeof(T), data, - details::PostFinalizerWrapper< - details::FinalizeData::WrapperWithHint>, + details::FinalizeData::WrapperWithHint, finalizeData, &value); if (status == details::napi_no_external_buffers_allowed) { #endif // If we can't create an external buffer, we'll just copy the data. Buffer ret = Buffer::Copy(env, data, length); - details::FinalizeData::WrapperWithHint( + details::FinalizeData::WrapperGCWithHint( env, data, finalizeData); return ret; #ifndef NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED @@ -3232,7 +3314,13 @@ template inline Reference::~Reference() { if (_ref != nullptr) { if (!_suppressDestruct) { +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + Env().PostFinalizer( + [](Napi::Env env, napi_ref ref) { napi_delete_reference(env, ref); }, + _ref); +#else napi_delete_reference(_env, _ref); +#endif } _ref = nullptr; @@ -4469,12 +4557,7 @@ inline ObjectWrap::ObjectWrap(const Napi::CallbackInfo& callbackInfo) { napi_status status; napi_ref ref; T* instance = static_cast(this); - status = napi_wrap(env, - wrapper, - instance, - details::PostFinalizerWrapper, - nullptr, - &ref); + status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref); NAPI_THROW_IF_FAILED_VOID(env, status); Reference* instanceRef = instance; @@ -4837,6 +4920,9 @@ inline Value ObjectWrap::OnCalledAsFunction( template inline void ObjectWrap::Finalize(Napi::Env /*env*/) {} +template +inline void ObjectWrap::Finalize(BasicEnv /*env*/) {} + template inline napi_value ObjectWrap::ConstructorCallbackWrapper( napi_env env, napi_callback_info info) { @@ -4922,10 +5008,55 @@ inline napi_value ObjectWrap::StaticSetterCallbackWrapper( } template -inline void ObjectWrap::FinalizeCallback(napi_env env, +inline void ObjectWrap::FinalizeCallback(node_api_nogc_env env, void* data, void* /*hint*/) { - HandleScope scope(env); + T* instance = static_cast(data); + + // Prevent ~ObjectWrap from calling napi_remove_wrap + instance->_ref = nullptr; + + // If class overrides the basic finalizer, execute it. + if constexpr (details::HasBasicFinalizer::value) { +#ifndef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + HandleScope scope(env); +#endif + + instance->Finalize(Napi::BasicEnv(env)); + } + + // If class overrides the (extended) finalizer, either schedule it or + // execute it immediately (depending on experimental features enabled). + if constexpr (details::HasExtendedFinalizer::value) { +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + // In experimental, attach via node_api_post_finalizer. + // `PostFinalizeCallback` is responsible for deleting the `T* instance`, + // after calling the user-provided finalizer. + napi_status status = + node_api_post_finalizer(env, PostFinalizeCallback, data, nullptr); + NAPI_FATAL_IF_FAILED(status, + "ObjectWrap::FinalizeCallback", + "node_api_post_finalizer failed"); +#else + // In non-experimental, this `FinalizeCallback` already executes from a + // non-basic environment. Execute the override directly. + // `PostFinalizeCallback` is responsible for deleting the `T* instance`, + // after calling the user-provided finalizer. + HandleScope scope(env); + PostFinalizeCallback(env, data, static_cast(nullptr)); +#endif + } + // If the instance does _not_ override the (extended) finalizer, delete the + // `T* instance` immediately. + else { + delete instance; + } +} + +template +inline void ObjectWrap::PostFinalizeCallback(napi_env env, + void* data, + void* /*hint*/) { T* instance = static_cast(data); instance->Finalize(Napi::Env(env)); delete instance; @@ -6605,12 +6736,12 @@ inline Napi::Object Addon::DefineProperties( #if NAPI_VERSION > 2 template -Env::CleanupHook Env::AddCleanupHook(Hook hook, Arg* arg) { +Env::CleanupHook BasicEnv::AddCleanupHook(Hook hook, Arg* arg) { return CleanupHook(*this, hook, arg); } template -Env::CleanupHook Env::AddCleanupHook(Hook hook) { +Env::CleanupHook BasicEnv::AddCleanupHook(Hook hook) { return CleanupHook(*this, hook); } @@ -6620,7 +6751,7 @@ Env::CleanupHook::CleanupHook() { } template -Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook) +Env::CleanupHook::CleanupHook(Napi::BasicEnv 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); @@ -6631,7 +6762,9 @@ Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook) } template -Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook, Arg* arg) +Env::CleanupHook::CleanupHook(Napi::BasicEnv 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); @@ -6642,7 +6775,7 @@ Env::CleanupHook::CleanupHook(Napi::Env env, Hook hook, Arg* arg) } template -bool Env::CleanupHook::Remove(Env env) { +bool Env::CleanupHook::Remove(BasicEnv env) { napi_status status = napi_remove_env_cleanup_hook(env, wrapper, data); delete data; data = nullptr; @@ -6655,6 +6788,65 @@ bool Env::CleanupHook::IsEmpty() const { } #endif // NAPI_VERSION > 2 +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback) const { + using T = void*; + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGCWithoutData, + static_cast(nullptr), + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} + +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback, + T* data) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), nullptr}); + + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGC, + data, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} + +template +inline void BasicEnv::PostFinalizer(FinalizerType finalizeCallback, + T* data, + Hint* finalizeHint) const { + details::FinalizeData* finalizeData = + new details::FinalizeData( + {std::move(finalizeCallback), finalizeHint}); + napi_status status = node_api_post_finalizer( + _env, + details::FinalizeData::WrapperGCWithHint, + data, + finalizeData); + if (status != napi_ok) { + delete finalizeData; + NAPI_FATAL_IF_FAILED( + status, "BasicEnv::PostFinalizer", "invalid arguments"); + } +} +#endif // NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + #ifdef NAPI_CPP_CUSTOM_NAMESPACE } // namespace NAPI_CPP_CUSTOM_NAMESPACE #endif diff --git a/napi.h b/napi.h index 24298ad9a..edec5111e 100644 --- a/napi.h +++ b/napi.h @@ -312,9 +312,9 @@ using MaybeOrValue = T; /// /// In the V8 JavaScript engine, a Node-API environment approximately /// corresponds to an Isolate. -class Env { +class BasicEnv { private: - napi_env _env; + node_api_nogc_env _env; #if NAPI_VERSION > 5 template static void DefaultFini(Env, T* data); @@ -322,20 +322,21 @@ class Env { static void DefaultFiniWithHint(Env, DataType* data, HintType* hint); #endif // NAPI_VERSION > 5 public: - Env(napi_env env); - - operator napi_env() const; - - Object Global() const; - Value Undefined() const; - Value Null() const; - - bool IsExceptionPending() const; - Error GetAndClearPendingException() const; - - MaybeOrValue RunScript(const char* utf8script) const; - MaybeOrValue RunScript(const std::string& utf8script) const; - MaybeOrValue RunScript(String script) const; + BasicEnv(node_api_nogc_env env); + operator node_api_nogc_env() const; + + // Without these operator overloads, the error: + // + // Use of overloaded operator '==' is ambiguous (with operand types + // 'Napi::Env' and 'Napi::Env') + // + // ... occurs when comparing foo.Env() == bar.Env() or foo.Env() == nullptr + bool operator==(const BasicEnv& other) const { + return _env == other._env; + }; + bool operator==(std::nullptr_t /*other*/) const { + return _env == nullptr; + }; #if NAPI_VERSION > 2 template @@ -354,7 +355,7 @@ class Env { template using Finalizer = void (*)(Env, T*); - template fini = Env::DefaultFini> + template fini = BasicEnv::DefaultFini> void SetInstanceData(T* data) const; template @@ -362,7 +363,7 @@ class Env { template fini = - Env::DefaultFiniWithHint> + BasicEnv::DefaultFiniWithHint> void SetInstanceData(DataType* data, HintType* hint) const; #endif // NAPI_VERSION > 5 @@ -371,9 +372,9 @@ class Env { class CleanupHook { public: CleanupHook(); - CleanupHook(Env env, Hook hook, Arg* arg); - CleanupHook(Env env, Hook hook); - bool Remove(Env env); + CleanupHook(BasicEnv env, Hook hook, Arg* arg); + CleanupHook(BasicEnv env, Hook hook); + bool Remove(BasicEnv env); bool IsEmpty() const; private: @@ -391,6 +392,39 @@ class Env { #if NAPI_VERSION > 8 const char* GetModuleFileName() const; #endif // NAPI_VERSION > 8 + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + template + inline void PostFinalizer(FinalizerType finalizeCallback) const; + + template + inline void PostFinalizer(FinalizerType finalizeCallback, T* data) const; + + template + inline void PostFinalizer(FinalizerType finalizeCallback, + T* data, + Hint* finalizeHint) const; +#endif // NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + + friend class Env; +}; + +class Env : public BasicEnv { + public: + Env(napi_env env); + + operator napi_env() const; + + Object Global() const; + Value Undefined() const; + Value Null() const; + + bool IsExceptionPending() const; + Error GetAndClearPendingException() const; + + MaybeOrValue RunScript(const char* utf8script) const; + MaybeOrValue RunScript(const std::string& utf8script) const; + MaybeOrValue RunScript(String script) const; }; /// A JavaScript value of unknown type. @@ -2415,6 +2449,7 @@ class ObjectWrap : public InstanceWrap, public Reference { napi_property_attributes attributes = napi_default); static Napi::Value OnCalledAsFunction(const Napi::CallbackInfo& callbackInfo); virtual void Finalize(Napi::Env env); + virtual void Finalize(BasicEnv env); private: using This = ObjectWrap; @@ -2429,7 +2464,10 @@ class ObjectWrap : public InstanceWrap, public Reference { napi_callback_info info); static napi_value StaticSetterCallbackWrapper(napi_env env, napi_callback_info info); - static void FinalizeCallback(napi_env env, void* data, void* hint); + static void FinalizeCallback(node_api_nogc_env env, void* data, void* hint); + + static void PostFinalizeCallback(napi_env env, void* data, void* hint); + static Function DefineClass(Napi::Env env, const char* utf8name, const size_t props_count, diff --git a/test/addon_build/tpl/binding.gyp b/test/addon_build/tpl/binding.gyp index 448fb9465..5b4f9f8ad 100644 --- a/test/addon_build/tpl/binding.gyp +++ b/test/addon_build/tpl/binding.gyp @@ -4,7 +4,7 @@ " 5) @@ -186,6 +187,13 @@ Object Init(Env env, Object exports) { #if defined(NODE_ADDON_API_ENABLE_MAYBE) exports.Set("maybe_check", InitMaybeCheck(env)); #endif + + exports.Set("finalizer_order", InitFinalizerOrder(env)); + + exports.Set( + "isExperimental", + Napi::Boolean::New(env, NAPI_VERSION == NAPI_VERSION_EXPERIMENTAL)); + return exports; } diff --git a/test/binding.gyp b/test/binding.gyp index e7bf253d0..28de3fe96 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -30,6 +30,7 @@ 'error.cc', 'error_handling_for_primitives.cc', 'external.cc', + 'finalizer_order.cc', 'function.cc', 'function_reference.cc', 'handlescope.cc', diff --git a/test/finalizer_order.cc b/test/finalizer_order.cc new file mode 100644 index 000000000..0767ced70 --- /dev/null +++ b/test/finalizer_order.cc @@ -0,0 +1,152 @@ +#include + +namespace { +class Test : public Napi::ObjectWrap { + public: + Test(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { + basicFinalizerCalled = false; + finalizerCalled = false; + + if (info.Length() > 0) { + finalizeCb_ = Napi::Persistent(info[0].As()); + } + } + + static void Initialize(Napi::Env env, Napi::Object exports) { + exports.Set("Test", + DefineClass(env, + "Test", + { + StaticAccessor("isBasicFinalizerCalled", + &IsBasicFinalizerCalled, + nullptr, + napi_default), + StaticAccessor("isFinalizerCalled", + &IsFinalizerCalled, + nullptr, + napi_default), + })); + } + + void Finalize(Napi::BasicEnv /*env*/) { basicFinalizerCalled = true; } + + void Finalize(Napi::Env /*env*/) { + finalizerCalled = true; + if (!finalizeCb_.IsEmpty()) { + finalizeCb_.Call({}); + } + } + + static Napi::Value IsBasicFinalizerCalled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), basicFinalizerCalled); + } + + static Napi::Value IsFinalizerCalled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), finalizerCalled); + } + + private: + Napi::FunctionReference finalizeCb_; + + static bool basicFinalizerCalled; + static bool finalizerCalled; +}; + +bool Test::basicFinalizerCalled = false; +bool Test::finalizerCalled = false; + +bool externalBasicFinalizerCalled = false; +bool externalFinalizerCalled = false; + +Napi::Value CreateExternalBasicFinalizer(const Napi::CallbackInfo& info) { + externalBasicFinalizerCalled = false; + return Napi::External::New( + info.Env(), new int(1), [](Napi::BasicEnv /*env*/, int* data) { + externalBasicFinalizerCalled = true; + delete data; + }); +} + +Napi::Value CreateExternalFinalizer(const Napi::CallbackInfo& info) { + externalFinalizerCalled = false; + return Napi::External::New( + info.Env(), new int(1), [](Napi::Env /*env*/, int* data) { + externalFinalizerCalled = true; + delete data; + }); +} + +Napi::Value isExternalBasicFinalizerCalled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), externalBasicFinalizerCalled); +} + +Napi::Value IsExternalFinalizerCalled(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), externalFinalizerCalled); +} + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER +Napi::Value PostFinalizer(const Napi::CallbackInfo& info) { + auto env = info.Env(); + + env.PostFinalizer([callback = Napi::Persistent(info[0].As())]( + Napi::Env /*env*/) { callback.Call({}); }); + + return env.Undefined(); +} + +Napi::Value PostFinalizerWithData(const Napi::CallbackInfo& info) { + auto env = info.Env(); + + env.PostFinalizer( + [callback = Napi::Persistent(info[0].As())]( + Napi::Env /*env*/, Napi::Reference* data) { + callback.Call({data->Value()}); + delete data; + }, + new Napi::Reference(Napi::Persistent(info[1]))); + + return env.Undefined(); +} + +Napi::Value PostFinalizerWithDataAndHint(const Napi::CallbackInfo& info) { + auto env = info.Env(); + + env.PostFinalizer( + [callback = Napi::Persistent(info[0].As())]( + Napi::Env /*env*/, + Napi::Reference* data, + Napi::Reference* hint) { + callback.Call({data->Value(), hint->Value()}); + delete data; + delete hint; + }, + new Napi::Reference(Napi::Persistent(info[1])), + new Napi::Reference(Napi::Persistent(info[2]))); + + return env.Undefined(); +} +#endif + +} // namespace + +Napi::Object InitFinalizerOrder(Napi::Env env) { + Napi::Object exports = Napi::Object::New(env); + Test::Initialize(env, exports); + exports["createExternalBasicFinalizer"] = + Napi::Function::New(env, CreateExternalBasicFinalizer); + exports["createExternalFinalizer"] = + Napi::Function::New(env, CreateExternalFinalizer); + exports["isExternalBasicFinalizerCalled"] = + Napi::Function::New(env, isExternalBasicFinalizerCalled); + exports["isExternalFinalizerCalled"] = + Napi::Function::New(env, IsExternalFinalizerCalled); + +#ifdef NODE_API_EXPERIMENTAL_HAS_POST_FINALIZER + exports["PostFinalizer"] = Napi::Function::New(env, PostFinalizer); + exports["PostFinalizerWithData"] = + Napi::Function::New(env, PostFinalizerWithData); + exports["PostFinalizerWithDataAndHint"] = + Napi::Function::New(env, PostFinalizerWithDataAndHint); +#endif + return exports; +} diff --git a/test/finalizer_order.js b/test/finalizer_order.js new file mode 100644 index 000000000..4b267a0d0 --- /dev/null +++ b/test/finalizer_order.js @@ -0,0 +1,98 @@ +'use strict'; + +/* eslint-disable no-unused-vars */ + +const assert = require('assert'); +const common = require('./common'); +const testUtil = require('./testUtil'); + +module.exports = require('./common').runTest(test); + +function test (binding) { + const { isExperimental } = binding; + + let isCallbackCalled = false; + + const tests = [ + 'Finalizer Order - ObjectWrap', + () => { + let test = new binding.finalizer_order.Test(() => { isCallbackCalled = true; }); + test = null; + + global.gc(); + + if (isExperimental) { + assert.strictEqual(binding.finalizer_order.Test.isBasicFinalizerCalled, true, 'Expected basic finalizer to be called [before ticking]'); + assert.strictEqual(binding.finalizer_order.Test.isFinalizerCalled, false, 'Expected (extended) finalizer to not be called [before ticking]'); + assert.strictEqual(isCallbackCalled, false, 'Expected callback to not be called [before ticking]'); + } else { + assert.strictEqual(binding.finalizer_order.Test.isBasicFinalizerCalled, false, 'Expected basic finalizer to not be called [before ticking]'); + assert.strictEqual(binding.finalizer_order.Test.isFinalizerCalled, false, 'Expected (extended) finalizer to not be called [before ticking]'); + assert.strictEqual(isCallbackCalled, false, 'Expected callback to not be called [before ticking]'); + } + }, + () => { + assert.strictEqual(binding.finalizer_order.Test.isBasicFinalizerCalled, true, 'Expected basic finalizer to be called [after ticking]'); + assert.strictEqual(binding.finalizer_order.Test.isFinalizerCalled, true, 'Expected (extended) finalizer to be called [after ticking]'); + assert.strictEqual(isCallbackCalled, true, 'Expected callback to be called [after ticking]'); + }, + + 'Finalizer Order - External with Basic Finalizer', + () => { + let ext = binding.finalizer_order.createExternalBasicFinalizer(); + ext = null; + global.gc(); + + if (isExperimental) { + assert.strictEqual(binding.finalizer_order.isExternalBasicFinalizerCalled(), true, 'Expected External basic finalizer to be called [before ticking]'); + } else { + assert.strictEqual(binding.finalizer_order.isExternalBasicFinalizerCalled(), false, 'Expected External basic finalizer to not be called [before ticking]'); + } + }, + () => { + assert.strictEqual(binding.finalizer_order.isExternalBasicFinalizerCalled(), true, 'Expected External basic finalizer to be called [after ticking]'); + }, + + 'Finalizer Order - External with Finalizer', + () => { + let ext = binding.finalizer_order.createExternalFinalizer(); + ext = null; + global.gc(); + assert.strictEqual(binding.finalizer_order.isExternalFinalizerCalled(), false, 'Expected External extended finalizer to not be called [before ticking]'); + }, + () => { + assert.strictEqual(binding.finalizer_order.isExternalFinalizerCalled(), true, 'Expected External extended finalizer to be called [after ticking]'); + } + ]; + + if (binding.isExperimental) { + tests.push(...[ + 'PostFinalizer', + () => { + binding.finalizer_order.PostFinalizer(common.mustCall()); + }, + + 'PostFinalizerWithData', + () => { + const data = {}; + const callback = (callbackData) => { + assert.strictEqual(callbackData, data); + }; + binding.finalizer_order.PostFinalizerWithData(common.mustCall(callback), data); + }, + + 'PostFinalizerWithDataAndHint', + () => { + const data = {}; + const hint = {}; + const callback = (callbackData, callbackHint) => { + assert.strictEqual(callbackData, data); + assert.strictEqual(callbackHint, hint); + }; + binding.finalizer_order.PostFinalizerWithDataAndHint(common.mustCall(callback), data, hint); + } + ]); + } + + return testUtil.runGCTests(tests); +}