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:
Gabriel Schulhof 2018-08-10 11:43:39 -04:00
parent 82b752a302
commit cf0e881b33
5 changed files with 215 additions and 35 deletions

View File

@ -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 like a regular JavaScript function call, or as a constructor
function. 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 ### napi_call_function
<!-- YAML <!-- YAML
added: v8.0.0 added: v8.0.0
@ -3375,6 +3380,11 @@ myaddon.sayHello();
The string passed to `require()` is the name of the target in `binding.gyp` The string passed to `require()` is the name of the target in `binding.gyp`
responsible for creating the `.node` file. 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 JavaScript `Function`s are described in
[Section 19.2](https://tc39.github.io/ecma262/#sec-function-objects) [Section 19.2](https://tc39.github.io/ecma262/#sec-function-objects)
of the ECMAScript Language Specification. 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 persistent reference to it using [`napi_create_reference`][] and ensure the
reference count is kept >= 1. 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 ### napi_wrap
<!-- YAML <!-- YAML
added: v8.0.0 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 callback was associated with the wrapping, it will no longer be called when the
JavaScript object becomes garbage-collected. 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 ## Simple Asynchronous Operations
Addon modules often need to leverage async helpers from libuv as part of their 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]: #n_api_working_with_javascript_values
[Working with JavaScript Values - Abstract Operations]: #n_api_working_with_javascript_values_abstract_operations [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_async_init`]: #n_api_napi_async_init
[`napi_cancel_async_work`]: #n_api_napi_cancel_async_work [`napi_cancel_async_work`]: #n_api_napi_cancel_async_work
[`napi_close_escapable_handle_scope`]: #n_api_napi_close_escapable_handle_scope [`napi_close_escapable_handle_scope`]: #n_api_napi_close_escapable_handle_scope

View File

@ -1157,6 +1157,63 @@ class ThreadSafeFunction : public node::AsyncResource {
bool handles_closing; 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 } // end of namespace v8impl
// Intercepts the Node-V8 module registration callback. Converts parameters // Intercepts the Node-V8 module registration callback. Converts parameters
@ -2859,41 +2916,12 @@ napi_status napi_wrap(napi_env env,
napi_finalize finalize_cb, napi_finalize finalize_cb,
void* finalize_hint, void* finalize_hint,
napi_ref* result) { napi_ref* result) {
NAPI_PREAMBLE(env); return v8impl::Wrap<v8impl::retrievable>(env,
CHECK_ARG(env, js_object); js_object,
native_object,
v8::Isolate* isolate = env->isolate; finalize_cb,
v8::Local<v8::Context> context = isolate->GetCurrentContext(); finalize_hint,
result);
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);
} }
napi_status napi_unwrap(napi_env env, napi_value obj, void** 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); CHECK(func != nullptr);
return reinterpret_cast<v8impl::ThreadSafeFunction*>(func)->Ref(); 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);
}

View File

@ -695,6 +695,12 @@ NAPI_EXTERN napi_status napi_get_value_bigint_words(napi_env env,
int* sign_bit, int* sign_bit,
size_t* word_count, size_t* word_count,
uint64_t* words); 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 #endif // NAPI_EXPERIMENTAL
EXTERN_C_END EXTERN_C_END

View 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');

View File

@ -1,3 +1,4 @@
#define NAPI_EXPERIMENTAL
#include <node_api.h> #include <node_api.h>
#include <stdlib.h> #include <stdlib.h>
#include "../common.h" #include "../common.h"
@ -177,6 +178,17 @@ static napi_value wrap(napi_env env, napi_callback_info info) {
return NULL; 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) { static napi_value remove_wrap(napi_env env, napi_callback_info info) {
size_t argc = 1; size_t argc = 1;
napi_value wrapped; napi_value wrapped;
@ -232,6 +244,33 @@ static napi_value testNapiRun(napi_env env, napi_callback_info info) {
return result; 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) { static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor descriptors[] = { napi_property_descriptor descriptors[] = {
DECLARE_NAPI_PROPERTY("testStrictEquals", testStrictEquals), 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("testNapiErrorCleanup", testNapiErrorCleanup),
DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof), DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof),
DECLARE_NAPI_PROPERTY("wrap", wrap), DECLARE_NAPI_PROPERTY("wrap", wrap),
DECLARE_NAPI_PROPERTY("unwrap", unwrap),
DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap), DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap),
DECLARE_NAPI_PROPERTY("addFinalizerOnly", add_finalizer_only),
DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap), DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap),
DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called), DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called),
DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called), DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called),