Skip to content

Commit bb2bbc8

Browse files
Gabriel Schulhoftargos
Gabriel Schulhof
authored andcommitted
n-api: add generic finalizer callback
Add `napi_add_finalizer()`, which provides the ability to attach data to an arbitrary object and be notified when that object is garbage- collected so as to have an opportunity to delete the data previously attached. This differs from `napi_wrap()` in that it does not use up the private slot on the object, and is therefore neither removable, nor retrievable after the call to `napi_add_finalizer()`. It is assumed that the data is accessible by other means, yet it must be tied to the lifetime of the object. This is the case for data passed to a dynamically created function which is itself heap-allocated and must therefore be freed along with the function. Fixes: nodejs/abi-stable-node#313 PR-URL: #22244 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent cf95b61 commit bb2bbc8

File tree

5 files changed

+215
-35
lines changed

5 files changed

+215
-35
lines changed

doc/api/n-api.md

+58
Original file line numberDiff line numberDiff line change
@@ -3238,6 +3238,11 @@ JavaScript functions from native code. One can either call a function
32383238
like a regular JavaScript function call, or as a constructor
32393239
function.
32403240

3241+
Any non-`NULL` data which is passed to this API via the `data` field of the
3242+
`napi_property_descriptor` items can be associated with `object` and freed
3243+
whenever `object` is garbage-collected by passing both `object` and the data to
3244+
[`napi_add_finalizer`][].
3245+
32413246
### napi_call_function
32423247
<!-- YAML
32433248
added: v8.0.0
@@ -3375,6 +3380,11 @@ myaddon.sayHello();
33753380
The string passed to `require()` is the name of the target in `binding.gyp`
33763381
responsible for creating the `.node` file.
33773382

3383+
Any non-`NULL` data which is passed to this API via the `data` parameter can
3384+
be associated with the resulting JavaScript function (which is returned in the
3385+
`result` parameter) and freed whenever the function is garbage-collected by
3386+
passing both the JavaScript function and the data to [`napi_add_finalizer`][].
3387+
33783388
JavaScript `Function`s are described in
33793389
[Section 19.2](https://tc39.github.io/ecma262/#sec-function-objects)
33803390
of the ECMAScript Language Specification.
@@ -3581,6 +3591,12 @@ case, to prevent the function value from being garbage-collected, create a
35813591
persistent reference to it using [`napi_create_reference`][] and ensure the
35823592
reference count is kept >= 1.
35833593

3594+
Any non-`NULL` data which is passed to this API via the `data` parameter or via
3595+
the `data` field of the `napi_property_descriptor` array items can be associated
3596+
with the resulting JavaScript constructor (which is returned in the `result`
3597+
parameter) and freed whenever the class is garbage-collected by passing both
3598+
the JavaScript function and the data to [`napi_add_finalizer`][].
3599+
35843600
### napi_wrap
35853601
<!-- YAML
35863602
added: v8.0.0
@@ -3685,6 +3701,47 @@ object `js_object` using `napi_wrap()` and removes the wrapping. If a finalize
36853701
callback was associated with the wrapping, it will no longer be called when the
36863702
JavaScript object becomes garbage-collected.
36873703

3704+
### napi_add_finalizer
3705+
<!-- YAML
3706+
added: v8.0.0
3707+
napiVersion: 1
3708+
-->
3709+
```C
3710+
napi_status napi_add_finalizer(napi_env env,
3711+
napi_value js_object,
3712+
void* native_object,
3713+
napi_finalize finalize_cb,
3714+
void* finalize_hint,
3715+
napi_ref* result);
3716+
```
3717+
3718+
- `[in] env`: The environment that the API is invoked under.
3719+
- `[in] js_object`: The JavaScript object to which the native data will be
3720+
attached.
3721+
- `[in] native_object`: The native data that will be attached to the JavaScript
3722+
object.
3723+
- `[in] finalize_cb`: Native callback that will be used to free the
3724+
native data when the JavaScript object is ready for garbage-collection.
3725+
- `[in] finalize_hint`: Optional contextual hint that is passed to the
3726+
finalize callback.
3727+
- `[out] result`: Optional reference to the JavaScript object.
3728+
3729+
Returns `napi_ok` if the API succeeded.
3730+
3731+
Adds a `napi_finalize` callback which will be called when the JavaScript object
3732+
in `js_object` is ready for garbage collection. This API is similar to
3733+
`napi_wrap()` except that
3734+
* the native data cannot be retrieved later using `napi_unwrap()`,
3735+
* nor can it be removed later using `napi_remove_wrap()`, and
3736+
* the API can be called multiple times with different data items in order to
3737+
attach each of them to the JavaScript object.
3738+
3739+
*Caution*: The optional returned reference (if obtained) should be deleted via
3740+
[`napi_delete_reference`][] ONLY in response to the finalize callback
3741+
invocation. If it is deleted before then, then the finalize callback may never
3742+
be invoked. Therefore, when obtaining a reference a finalize callback is also
3743+
required in order to enable correct disposal of the reference.
3744+
36883745
## Simple Asynchronous Operations
36893746

36903747
Addon modules often need to leverage async helpers from libuv as part of their
@@ -4559,6 +4616,7 @@ This API may only be called from the main thread.
45594616
[Working with JavaScript Values]: #n_api_working_with_javascript_values
45604617
[Working with JavaScript Values - Abstract Operations]: #n_api_working_with_javascript_values_abstract_operations
45614618

4619+
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
45624620
[`napi_async_init`]: #n_api_napi_async_init
45634621
[`napi_cancel_async_work`]: #n_api_napi_cancel_async_work
45644622
[`napi_close_escapable_handle_scope`]: #n_api_napi_close_escapable_handle_scope

src/node_api.cc

+77-35
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,63 @@ class ThreadSafeFunction : public node::AsyncResource {
11571157
bool handles_closing;
11581158
};
11591159

1160+
enum WrapType {
1161+
retrievable,
1162+
anonymous
1163+
};
1164+
1165+
template <WrapType wrap_type> static inline
1166+
napi_status Wrap(napi_env env,
1167+
napi_value js_object,
1168+
void* native_object,
1169+
napi_finalize finalize_cb,
1170+
void* finalize_hint,
1171+
napi_ref* result) {
1172+
NAPI_PREAMBLE(env);
1173+
CHECK_ARG(env, js_object);
1174+
1175+
v8::Isolate* isolate = env->isolate;
1176+
v8::Local<v8::Context> context = isolate->GetCurrentContext();
1177+
1178+
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
1179+
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
1180+
v8::Local<v8::Object> obj = value.As<v8::Object>();
1181+
1182+
if (wrap_type == retrievable) {
1183+
// If we've already wrapped this object, we error out.
1184+
RETURN_STATUS_IF_FALSE(env,
1185+
!obj->HasPrivate(context, NAPI_PRIVATE_KEY(context, wrapper))
1186+
.FromJust(),
1187+
napi_invalid_arg);
1188+
} else if (wrap_type == anonymous) {
1189+
// If no finalize callback is provided, we error out.
1190+
CHECK_ARG(env, finalize_cb);
1191+
}
1192+
1193+
v8impl::Reference* reference = nullptr;
1194+
if (result != nullptr) {
1195+
// The returned reference should be deleted via napi_delete_reference()
1196+
// ONLY in response to the finalize callback invocation. (If it is deleted
1197+
// before then, then the finalize callback will never be invoked.)
1198+
// Therefore a finalize callback is required when returning a reference.
1199+
CHECK_ARG(env, finalize_cb);
1200+
reference = v8impl::Reference::New(
1201+
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
1202+
*result = reinterpret_cast<napi_ref>(reference);
1203+
} else {
1204+
// Create a self-deleting reference.
1205+
reference = v8impl::Reference::New(env, obj, 0, true, finalize_cb,
1206+
native_object, finalize_cb == nullptr ? nullptr : finalize_hint);
1207+
}
1208+
1209+
if (wrap_type == retrievable) {
1210+
CHECK(obj->SetPrivate(context, NAPI_PRIVATE_KEY(context, wrapper),
1211+
v8::External::New(isolate, reference)).FromJust());
1212+
}
1213+
1214+
return GET_RETURN_STATUS(env);
1215+
}
1216+
11601217
} // end of namespace v8impl
11611218

11621219
// Intercepts the Node-V8 module registration callback. Converts parameters
@@ -2859,41 +2916,12 @@ napi_status napi_wrap(napi_env env,
28592916
napi_finalize finalize_cb,
28602917
void* finalize_hint,
28612918
napi_ref* result) {
2862-
NAPI_PREAMBLE(env);
2863-
CHECK_ARG(env, js_object);
2864-
2865-
v8::Isolate* isolate = env->isolate;
2866-
v8::Local<v8::Context> context = isolate->GetCurrentContext();
2867-
2868-
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
2869-
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
2870-
v8::Local<v8::Object> obj = value.As<v8::Object>();
2871-
2872-
// If we've already wrapped this object, we error out.
2873-
RETURN_STATUS_IF_FALSE(env,
2874-
!obj->HasPrivate(context, NAPI_PRIVATE_KEY(context, wrapper)).FromJust(),
2875-
napi_invalid_arg);
2876-
2877-
v8impl::Reference* reference = nullptr;
2878-
if (result != nullptr) {
2879-
// The returned reference should be deleted via napi_delete_reference()
2880-
// ONLY in response to the finalize callback invocation. (If it is deleted
2881-
// before then, then the finalize callback will never be invoked.)
2882-
// Therefore a finalize callback is required when returning a reference.
2883-
CHECK_ARG(env, finalize_cb);
2884-
reference = v8impl::Reference::New(
2885-
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
2886-
*result = reinterpret_cast<napi_ref>(reference);
2887-
} else {
2888-
// Create a self-deleting reference.
2889-
reference = v8impl::Reference::New(env, obj, 0, true, finalize_cb,
2890-
native_object, finalize_cb == nullptr ? nullptr : finalize_hint);
2891-
}
2892-
2893-
CHECK(obj->SetPrivate(context, NAPI_PRIVATE_KEY(context, wrapper),
2894-
v8::External::New(isolate, reference)).FromJust());
2895-
2896-
return GET_RETURN_STATUS(env);
2919+
return v8impl::Wrap<v8impl::retrievable>(env,
2920+
js_object,
2921+
native_object,
2922+
finalize_cb,
2923+
finalize_hint,
2924+
result);
28972925
}
28982926

28992927
napi_status napi_unwrap(napi_env env, napi_value obj, void** result) {
@@ -4138,3 +4166,17 @@ napi_ref_threadsafe_function(napi_env env, napi_threadsafe_function func) {
41384166
CHECK(func != nullptr);
41394167
return reinterpret_cast<v8impl::ThreadSafeFunction*>(func)->Ref();
41404168
}
4169+
4170+
napi_status napi_add_finalizer(napi_env env,
4171+
napi_value js_object,
4172+
void* native_object,
4173+
napi_finalize finalize_cb,
4174+
void* finalize_hint,
4175+
napi_ref* result) {
4176+
return v8impl::Wrap<v8impl::anonymous>(env,
4177+
js_object,
4178+
native_object,
4179+
finalize_cb,
4180+
finalize_hint,
4181+
result);
4182+
}

src/node_api.h

+6
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,12 @@ NAPI_EXTERN napi_status napi_get_value_bigint_words(napi_env env,
695695
int* sign_bit,
696696
size_t* word_count,
697697
uint64_t* words);
698+
NAPI_EXTERN napi_status napi_add_finalizer(napi_env env,
699+
napi_value js_object,
700+
void* native_object,
701+
napi_finalize finalize_cb,
702+
void* finalize_hint,
703+
napi_ref* result);
698704
#endif // NAPI_EXPERIMENTAL
699705

700706
EXTERN_C_END
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
// Flags: --expose-gc
3+
4+
const common = require('../../common');
5+
const test_general = require(`./build/${common.buildType}/test_general`);
6+
const assert = require('assert');
7+
8+
let finalized = {};
9+
const callback = common.mustCall(2);
10+
11+
// Add two items to be finalized and ensure the callback is called for each.
12+
test_general.addFinalizerOnly(finalized, callback);
13+
test_general.addFinalizerOnly(finalized, callback);
14+
15+
// Ensure attached items cannot be retrieved.
16+
common.expectsError(() => test_general.unwrap(finalized),
17+
{ type: Error, message: 'Invalid argument' });
18+
19+
// Ensure attached items cannot be removed.
20+
common.expectsError(() => test_general.removeWrap(finalized),
21+
{ type: Error, message: 'Invalid argument' });
22+
finalized = null;
23+
global.gc();
24+
25+
// Add an item to an object that is already wrapped, and ensure that its
26+
// finalizer as well as the wrap finalizer gets called.
27+
let finalizeAndWrap = {};
28+
test_general.wrap(finalizeAndWrap);
29+
test_general.addFinalizerOnly(finalizeAndWrap, common.mustCall());
30+
finalizeAndWrap = null;
31+
global.gc();
32+
assert.strictEqual(test_general.derefItemWasCalled(), true,
33+
'finalize callback was called');

test/addons-napi/test_general/test_general.c

+41
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#define NAPI_EXPERIMENTAL
12
#include <node_api.h>
23
#include <stdlib.h>
34
#include "../common.h"
@@ -177,6 +178,17 @@ static napi_value wrap(napi_env env, napi_callback_info info) {
177178
return NULL;
178179
}
179180

181+
static napi_value unwrap(napi_env env, napi_callback_info info) {
182+
size_t argc = 1;
183+
napi_value wrapped;
184+
void* data;
185+
186+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL));
187+
NAPI_CALL(env, napi_unwrap(env, wrapped, &data));
188+
189+
return NULL;
190+
}
191+
180192
static napi_value remove_wrap(napi_env env, napi_callback_info info) {
181193
size_t argc = 1;
182194
napi_value wrapped;
@@ -232,6 +244,33 @@ static napi_value testNapiRun(napi_env env, napi_callback_info info) {
232244
return result;
233245
}
234246

247+
static void finalizer_only_callback(napi_env env, void* data, void* hint) {
248+
napi_ref js_cb_ref = data;
249+
napi_value js_cb, undefined;
250+
NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_cb_ref, &js_cb));
251+
NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
252+
NAPI_CALL_RETURN_VOID(env,
253+
napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
254+
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, js_cb_ref));
255+
}
256+
257+
static napi_value add_finalizer_only(napi_env env, napi_callback_info info) {
258+
size_t argc = 2;
259+
napi_value argv[2];
260+
napi_ref js_cb_ref;
261+
262+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
263+
NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &js_cb_ref));
264+
NAPI_CALL(env,
265+
napi_add_finalizer(env,
266+
argv[0],
267+
js_cb_ref,
268+
finalizer_only_callback,
269+
NULL,
270+
NULL));
271+
return NULL;
272+
}
273+
235274
static napi_value Init(napi_env env, napi_value exports) {
236275
napi_property_descriptor descriptors[] = {
237276
DECLARE_NAPI_PROPERTY("testStrictEquals", testStrictEquals),
@@ -246,7 +285,9 @@ static napi_value Init(napi_env env, napi_value exports) {
246285
DECLARE_NAPI_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup),
247286
DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof),
248287
DECLARE_NAPI_PROPERTY("wrap", wrap),
288+
DECLARE_NAPI_PROPERTY("unwrap", unwrap),
249289
DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap),
290+
DECLARE_NAPI_PROPERTY("addFinalizerOnly", add_finalizer_only),
250291
DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap),
251292
DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called),
252293
DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called),

0 commit comments

Comments
 (0)