Skip to content

Commit

Permalink
switch to "basic" finalizer in docs
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinEady committed Jul 10, 2024
1 parent 6591a36 commit 7d7b821
Showing 1 changed file with 60 additions and 76 deletions.
136 changes: 60 additions & 76 deletions doc/finalization.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
# Finalization

Various node-addon-api methods accept a templated `Finalizer finalizeCallback`
parameter. This parameter represents a callback function that runs in a
_synchronous_ or _asynchronous_ manner in response to a garbage collection
event. The callback executes either during the garbage collection cycle
(_synchronously_) or after the cycle has completed (_asynchronously_).
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 optimizations, improved execution, or other benefits.

In general, it is best to use synchronous finalizers whenever a callback must
free native data.
In general, it is best to use basic finalizers whenever possible (eg. when
access to JavaScript is _not_ needed).

## Synchronous Finalizers
*NOTE*: Optimizations via basic finalizers will only occur if using
`NAPI_EXPERIMENTAL` and the `NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT` define flag
has _not_ been set. Otherwise, the engine will not differentiate between basic
and (extended) finalizers.

Synchronous finalizers execute during the garbage collection cycle, and
therefore must not manipulate the engine heap. The finalizer executes in the
current tick, providing a chance to free native memory. The callback takes
`Napi::BasicEnv` as its first argument.
## Finalizers

### Example

```cpp
Napi::ArrayBuffer::New(
Env(), data, length, [](Napi::BasicEnv /*env*/, void* finalizeData) {
delete[] static_cast<uint8_t*>(finalizeData);
});
```
## Asynchronous Finalizers
Asynchronous finalizers execute outside the context of garbage collection later
in the event loop. Care must be taken when creating several objects in
succession if the finalizer must free native memory. The engine may garbage
collect several objects created in one tick, but any asynchronous finalizers --
and therefore the freeing of native memory -- will not execute in the same tick.
The callback takes `Napi::Env` as its first argument.
The callback takes `Napi::Env` as its first argument:

### Example

Expand All @@ -43,32 +27,36 @@ Napi::External<int>::New(Env(), new int(1), [](Napi::Env env, int* data) {
});
```

#### Caveats
## Basic Finalizers

```js
while (conditional()) {
const external = createExternal();
doSomethingWith(external);
}
```
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 on the current tick.

In general, APIs that access engine heap are not allowed in basic finalizers.

The callback takes `Napi::BasicEnv` as its first argument:

The engine may determine to run the garbage collector within the loop, freeing
the JavaScript engine's resources for all objects created by `createExternal()`.
However, asynchronous finalizers will not execute during this loop, and the
native memory held in `data` will not be freed.
### Example

## Scheduling Asynchronous Finalizers
```cpp
Napi::ArrayBuffer::New(
Env(), data, length, [](Napi::BasicEnv /*env*/, void* finalizeData) {
delete[] static_cast<uint8_t*>(finalizeData);
});
```
In addition to passing asynchronous finalizers to externals and other Node-API
constructs, use `Napi::BasicEnv::PostFinalize(Napi::Env, Finalizer)` to schedule
an asynchronous finalizer to run after the next garbage collection cycle
completes.
## Scheduling Finalizers
Free native data in a synchronous finalizer, while executing any JavaScript code
in an asynchronous finalizer attached via this method. Since the associated
native memory may already be freed by the synchronous finalizer, any additional
data may be passed eg. via the finalizer's parameters (`T data*`, `Hint hint*`)
or via lambda capture.
In addition to passing finalizers to `Napi::External`s and other Node-API
constructs, use `Napi::BasicEnv::PostFinalize(Napi::BasicEnv, Finalizer)` 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
Expand All @@ -92,19 +80,19 @@ class LargeData {
size_t LargeData::instances = 0;
// Synchronous finalizer to free `LargeData`. Takes ownership of the pointer and
// Basic finalizer to free `LargeData`. Takes ownership of the pointer and
// frees its memory after use.
void MySyncFinalizer(Napi::BasicEnv env, LargeData* data) {
void MyBasicFinalizer(Napi::BasicEnv env, LargeData* data) {
std::unique_ptr<LargeData> instance(data);
std::cout << "Synchronous finalizer for instance " << instance->id
std::cout << "Basic finalizer for instance " << instance->id
<< " called\n";
// Register the asynchronous callback. Since the instance will be deleted by
// 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) {
std::cout << "Asynchronous finalizer for instance " << instanceId
<< " called\n";
env.RunScript("console.log('Finalizer for instance " +
std::to_string(instanceId) + " called');");
});
// Free the `LargeData` held in `data` once `instance` goes out of scope.
Expand All @@ -114,13 +102,10 @@ Value CreateExternal(const CallbackInfo& info) {
// Create a new instance of LargeData.
auto instance = std::make_unique<LargeData>();
// Wrap the instance in an External object, registering a synchronous
// Wrap the instance in an External object, registering a basic
// finalizer that will delete the instance to free the "large" amount of
// memory.
auto ext =
External<LargeData>::New(info.Env(), instance.release(), MySyncFinalizer);

return ext;
return External<LargeData>::New(info.Env(), instance.release(), MyBasicFinalizer);
}
Object Init(Napi::Env env, Object exports) {
Expand All @@ -138,33 +123,32 @@ const { createExternal } = require('./addon.node');

for (let i = 0; i < 5; i++) {
const ext = createExternal();
doSomethingWith(ext);
// ... do something with `ext` ..
}

console.log('Loop complete');
await new Promise(resolve => setImmediate(resolve));
console.log('Next event loop cycle');
```

Possible output:

```
Synchronous finalizer for instance 0 called
Synchronous finalizer for instance 1 called
Synchronous finalizer for instance 2 called
Synchronous finalizer for instance 3 called
Synchronous finalizer for instance 4 called
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
Asynchronous finalizer for instance 3 called
Asynchronous finalizer for instance 4 called
Asynchronous finalizer for instance 1 called
Asynchronous finalizer for instance 2 called
Asynchronous finalizer for instance 0 called
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 synchronous finalizers
execute and display their logging message before the loop completes. The
asynchronous finalizers execute at some later point after the garbage collection
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.

0 comments on commit 7d7b821

Please sign in to comment.