mirror of
https://github.com/nodejs/node.git
synced 2024-12-01 16:10:02 +01:00
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: https://github.com/nodejs/abi-stable-node/issues/313 PR-URL: https://github.com/nodejs/node/pull/22244 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
This commit is contained in:
parent
82b752a302
commit
cf0e881b33
@ -3238,6 +3238,11 @@ JavaScript functions from native code. One can either call a function
|
||||
like a regular JavaScript function call, or as a constructor
|
||||
function.
|
||||
|
||||
Any non-`NULL` data which is passed to this API via the `data` field of the
|
||||
`napi_property_descriptor` items can be associated with `object` and freed
|
||||
whenever `object` is garbage-collected by passing both `object` and the data to
|
||||
[`napi_add_finalizer`][].
|
||||
|
||||
### napi_call_function
|
||||
<!-- YAML
|
||||
added: v8.0.0
|
||||
@ -3375,6 +3380,11 @@ myaddon.sayHello();
|
||||
The string passed to `require()` is the name of the target in `binding.gyp`
|
||||
responsible for creating the `.node` file.
|
||||
|
||||
Any non-`NULL` data which is passed to this API via the `data` parameter can
|
||||
be associated with the resulting JavaScript function (which is returned in the
|
||||
`result` parameter) and freed whenever the function is garbage-collected by
|
||||
passing both the JavaScript function and the data to [`napi_add_finalizer`][].
|
||||
|
||||
JavaScript `Function`s are described in
|
||||
[Section 19.2](https://tc39.github.io/ecma262/#sec-function-objects)
|
||||
of the ECMAScript Language Specification.
|
||||
@ -3581,6 +3591,12 @@ case, to prevent the function value from being garbage-collected, create a
|
||||
persistent reference to it using [`napi_create_reference`][] and ensure the
|
||||
reference count is kept >= 1.
|
||||
|
||||
Any non-`NULL` data which is passed to this API via the `data` parameter or via
|
||||
the `data` field of the `napi_property_descriptor` array items can be associated
|
||||
with the resulting JavaScript constructor (which is returned in the `result`
|
||||
parameter) and freed whenever the class is garbage-collected by passing both
|
||||
the JavaScript function and the data to [`napi_add_finalizer`][].
|
||||
|
||||
### napi_wrap
|
||||
<!-- YAML
|
||||
added: v8.0.0
|
||||
@ -3685,6 +3701,47 @@ object `js_object` using `napi_wrap()` and removes the wrapping. If a finalize
|
||||
callback was associated with the wrapping, it will no longer be called when the
|
||||
JavaScript object becomes garbage-collected.
|
||||
|
||||
### napi_add_finalizer
|
||||
<!-- YAML
|
||||
added: v8.0.0
|
||||
napiVersion: 1
|
||||
-->
|
||||
```C
|
||||
napi_status napi_add_finalizer(napi_env env,
|
||||
napi_value js_object,
|
||||
void* native_object,
|
||||
napi_finalize finalize_cb,
|
||||
void* finalize_hint,
|
||||
napi_ref* result);
|
||||
```
|
||||
|
||||
- `[in] env`: The environment that the API is invoked under.
|
||||
- `[in] js_object`: The JavaScript object to which the native data will be
|
||||
attached.
|
||||
- `[in] native_object`: The native data that will be attached to the JavaScript
|
||||
object.
|
||||
- `[in] finalize_cb`: Native callback that will be used to free the
|
||||
native data when the JavaScript object is ready for garbage-collection.
|
||||
- `[in] finalize_hint`: Optional contextual hint that is passed to the
|
||||
finalize callback.
|
||||
- `[out] result`: Optional reference to the JavaScript object.
|
||||
|
||||
Returns `napi_ok` if the API succeeded.
|
||||
|
||||
Adds a `napi_finalize` callback which will be called when the JavaScript object
|
||||
in `js_object` is ready for garbage collection. This API is similar to
|
||||
`napi_wrap()` except that
|
||||
* the native data cannot be retrieved later using `napi_unwrap()`,
|
||||
* nor can it be removed later using `napi_remove_wrap()`, and
|
||||
* the API can be called multiple times with different data items in order to
|
||||
attach each of them to the JavaScript object.
|
||||
|
||||
*Caution*: The optional returned reference (if obtained) should be deleted via
|
||||
[`napi_delete_reference`][] ONLY in response to the finalize callback
|
||||
invocation. If it is deleted before then, then the finalize callback may never
|
||||
be invoked. Therefore, when obtaining a reference a finalize callback is also
|
||||
required in order to enable correct disposal of the reference.
|
||||
|
||||
## Simple Asynchronous Operations
|
||||
|
||||
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.
|
||||
[Working with JavaScript Values]: #n_api_working_with_javascript_values
|
||||
[Working with JavaScript Values - Abstract Operations]: #n_api_working_with_javascript_values_abstract_operations
|
||||
|
||||
[`napi_add_finalizer`]: #n_api_napi_add_finalizer
|
||||
[`napi_async_init`]: #n_api_napi_async_init
|
||||
[`napi_cancel_async_work`]: #n_api_napi_cancel_async_work
|
||||
[`napi_close_escapable_handle_scope`]: #n_api_napi_close_escapable_handle_scope
|
||||
|
112
src/node_api.cc
112
src/node_api.cc
@ -1157,6 +1157,63 @@ class ThreadSafeFunction : public node::AsyncResource {
|
||||
bool handles_closing;
|
||||
};
|
||||
|
||||
enum WrapType {
|
||||
retrievable,
|
||||
anonymous
|
||||
};
|
||||
|
||||
template <WrapType wrap_type> static inline
|
||||
napi_status Wrap(napi_env env,
|
||||
napi_value js_object,
|
||||
void* native_object,
|
||||
napi_finalize finalize_cb,
|
||||
void* finalize_hint,
|
||||
napi_ref* result) {
|
||||
NAPI_PREAMBLE(env);
|
||||
CHECK_ARG(env, js_object);
|
||||
|
||||
v8::Isolate* isolate = env->isolate;
|
||||
v8::Local<v8::Context> context = isolate->GetCurrentContext();
|
||||
|
||||
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
|
||||
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
|
||||
v8::Local<v8::Object> obj = value.As<v8::Object>();
|
||||
|
||||
if (wrap_type == retrievable) {
|
||||
// If we've already wrapped this object, we error out.
|
||||
RETURN_STATUS_IF_FALSE(env,
|
||||
!obj->HasPrivate(context, NAPI_PRIVATE_KEY(context, wrapper))
|
||||
.FromJust(),
|
||||
napi_invalid_arg);
|
||||
} else if (wrap_type == anonymous) {
|
||||
// If no finalize callback is provided, we error out.
|
||||
CHECK_ARG(env, finalize_cb);
|
||||
}
|
||||
|
||||
v8impl::Reference* reference = nullptr;
|
||||
if (result != nullptr) {
|
||||
// The returned reference should be deleted via napi_delete_reference()
|
||||
// ONLY in response to the finalize callback invocation. (If it is deleted
|
||||
// before then, then the finalize callback will never be invoked.)
|
||||
// Therefore a finalize callback is required when returning a reference.
|
||||
CHECK_ARG(env, finalize_cb);
|
||||
reference = v8impl::Reference::New(
|
||||
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
|
||||
*result = reinterpret_cast<napi_ref>(reference);
|
||||
} else {
|
||||
// Create a self-deleting reference.
|
||||
reference = v8impl::Reference::New(env, obj, 0, true, finalize_cb,
|
||||
native_object, finalize_cb == nullptr ? nullptr : finalize_hint);
|
||||
}
|
||||
|
||||
if (wrap_type == retrievable) {
|
||||
CHECK(obj->SetPrivate(context, NAPI_PRIVATE_KEY(context, wrapper),
|
||||
v8::External::New(isolate, reference)).FromJust());
|
||||
}
|
||||
|
||||
return GET_RETURN_STATUS(env);
|
||||
}
|
||||
|
||||
} // end of namespace v8impl
|
||||
|
||||
// Intercepts the Node-V8 module registration callback. Converts parameters
|
||||
@ -2859,41 +2916,12 @@ napi_status napi_wrap(napi_env env,
|
||||
napi_finalize finalize_cb,
|
||||
void* finalize_hint,
|
||||
napi_ref* result) {
|
||||
NAPI_PREAMBLE(env);
|
||||
CHECK_ARG(env, js_object);
|
||||
|
||||
v8::Isolate* isolate = env->isolate;
|
||||
v8::Local<v8::Context> context = isolate->GetCurrentContext();
|
||||
|
||||
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
|
||||
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
|
||||
v8::Local<v8::Object> obj = value.As<v8::Object>();
|
||||
|
||||
// If we've already wrapped this object, we error out.
|
||||
RETURN_STATUS_IF_FALSE(env,
|
||||
!obj->HasPrivate(context, NAPI_PRIVATE_KEY(context, wrapper)).FromJust(),
|
||||
napi_invalid_arg);
|
||||
|
||||
v8impl::Reference* reference = nullptr;
|
||||
if (result != nullptr) {
|
||||
// The returned reference should be deleted via napi_delete_reference()
|
||||
// ONLY in response to the finalize callback invocation. (If it is deleted
|
||||
// before then, then the finalize callback will never be invoked.)
|
||||
// Therefore a finalize callback is required when returning a reference.
|
||||
CHECK_ARG(env, finalize_cb);
|
||||
reference = v8impl::Reference::New(
|
||||
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
|
||||
*result = reinterpret_cast<napi_ref>(reference);
|
||||
} else {
|
||||
// Create a self-deleting reference.
|
||||
reference = v8impl::Reference::New(env, obj, 0, true, finalize_cb,
|
||||
native_object, finalize_cb == nullptr ? nullptr : finalize_hint);
|
||||
}
|
||||
|
||||
CHECK(obj->SetPrivate(context, NAPI_PRIVATE_KEY(context, wrapper),
|
||||
v8::External::New(isolate, reference)).FromJust());
|
||||
|
||||
return GET_RETURN_STATUS(env);
|
||||
return v8impl::Wrap<v8impl::retrievable>(env,
|
||||
js_object,
|
||||
native_object,
|
||||
finalize_cb,
|
||||
finalize_hint,
|
||||
result);
|
||||
}
|
||||
|
||||
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) {
|
||||
CHECK(func != nullptr);
|
||||
return reinterpret_cast<v8impl::ThreadSafeFunction*>(func)->Ref();
|
||||
}
|
||||
|
||||
napi_status napi_add_finalizer(napi_env env,
|
||||
napi_value js_object,
|
||||
void* native_object,
|
||||
napi_finalize finalize_cb,
|
||||
void* finalize_hint,
|
||||
napi_ref* result) {
|
||||
return v8impl::Wrap<v8impl::anonymous>(env,
|
||||
js_object,
|
||||
native_object,
|
||||
finalize_cb,
|
||||
finalize_hint,
|
||||
result);
|
||||
}
|
||||
|
@ -695,6 +695,12 @@ NAPI_EXTERN napi_status napi_get_value_bigint_words(napi_env env,
|
||||
int* sign_bit,
|
||||
size_t* word_count,
|
||||
uint64_t* words);
|
||||
NAPI_EXTERN napi_status napi_add_finalizer(napi_env env,
|
||||
napi_value js_object,
|
||||
void* native_object,
|
||||
napi_finalize finalize_cb,
|
||||
void* finalize_hint,
|
||||
napi_ref* result);
|
||||
#endif // NAPI_EXPERIMENTAL
|
||||
|
||||
EXTERN_C_END
|
||||
|
33
test/addons-napi/test_general/testFinalizer.js
Normal file
33
test/addons-napi/test_general/testFinalizer.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
// Flags: --expose-gc
|
||||
|
||||
const common = require('../../common');
|
||||
const test_general = require(`./build/${common.buildType}/test_general`);
|
||||
const assert = require('assert');
|
||||
|
||||
let finalized = {};
|
||||
const callback = common.mustCall(2);
|
||||
|
||||
// Add two items to be finalized and ensure the callback is called for each.
|
||||
test_general.addFinalizerOnly(finalized, callback);
|
||||
test_general.addFinalizerOnly(finalized, callback);
|
||||
|
||||
// Ensure attached items cannot be retrieved.
|
||||
common.expectsError(() => test_general.unwrap(finalized),
|
||||
{ type: Error, message: 'Invalid argument' });
|
||||
|
||||
// Ensure attached items cannot be removed.
|
||||
common.expectsError(() => test_general.removeWrap(finalized),
|
||||
{ type: Error, message: 'Invalid argument' });
|
||||
finalized = null;
|
||||
global.gc();
|
||||
|
||||
// Add an item to an object that is already wrapped, and ensure that its
|
||||
// finalizer as well as the wrap finalizer gets called.
|
||||
let finalizeAndWrap = {};
|
||||
test_general.wrap(finalizeAndWrap);
|
||||
test_general.addFinalizerOnly(finalizeAndWrap, common.mustCall());
|
||||
finalizeAndWrap = null;
|
||||
global.gc();
|
||||
assert.strictEqual(test_general.derefItemWasCalled(), true,
|
||||
'finalize callback was called');
|
@ -1,3 +1,4 @@
|
||||
#define NAPI_EXPERIMENTAL
|
||||
#include <node_api.h>
|
||||
#include <stdlib.h>
|
||||
#include "../common.h"
|
||||
@ -177,6 +178,17 @@ static napi_value wrap(napi_env env, napi_callback_info info) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static napi_value unwrap(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value wrapped;
|
||||
void* data;
|
||||
|
||||
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL));
|
||||
NAPI_CALL(env, napi_unwrap(env, wrapped, &data));
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static napi_value remove_wrap(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 1;
|
||||
napi_value wrapped;
|
||||
@ -232,6 +244,33 @@ static napi_value testNapiRun(napi_env env, napi_callback_info info) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static void finalizer_only_callback(napi_env env, void* data, void* hint) {
|
||||
napi_ref js_cb_ref = data;
|
||||
napi_value js_cb, undefined;
|
||||
NAPI_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_cb_ref, &js_cb));
|
||||
NAPI_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
|
||||
NAPI_CALL_RETURN_VOID(env,
|
||||
napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
|
||||
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, js_cb_ref));
|
||||
}
|
||||
|
||||
static napi_value add_finalizer_only(napi_env env, napi_callback_info info) {
|
||||
size_t argc = 2;
|
||||
napi_value argv[2];
|
||||
napi_ref js_cb_ref;
|
||||
|
||||
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
||||
NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &js_cb_ref));
|
||||
NAPI_CALL(env,
|
||||
napi_add_finalizer(env,
|
||||
argv[0],
|
||||
js_cb_ref,
|
||||
finalizer_only_callback,
|
||||
NULL,
|
||||
NULL));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor descriptors[] = {
|
||||
DECLARE_NAPI_PROPERTY("testStrictEquals", testStrictEquals),
|
||||
@ -246,7 +285,9 @@ static napi_value Init(napi_env env, napi_value exports) {
|
||||
DECLARE_NAPI_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup),
|
||||
DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof),
|
||||
DECLARE_NAPI_PROPERTY("wrap", wrap),
|
||||
DECLARE_NAPI_PROPERTY("unwrap", unwrap),
|
||||
DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap),
|
||||
DECLARE_NAPI_PROPERTY("addFinalizerOnly", add_finalizer_only),
|
||||
DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap),
|
||||
DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called),
|
||||
DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called),
|
||||
|
Loading…
Reference in New Issue
Block a user