Skip to content

Commit a6344d5

Browse files
Gabriel SchulhofMylesBorins
Gabriel Schulhof
authored andcommitted
n-api: add ability to remove a wrapping
Calling napi_wrap() twice on the same object has the result of returning napi_invalid_arg on the second call. However, sometimes it is necessary to replace the native pointer associated with a given object. This new API allows one to remove an existing pointer, returning the object to its pristine, non-wrapped state. PR-URL: #14658 Reviewed-By: Michael Dawson <[email protected]> Fixes: nodejs/abi-stable-node#266
1 parent e244f84 commit a6344d5

File tree

5 files changed

+178
-25
lines changed

5 files changed

+178
-25
lines changed

doc/api/n-api.md

+27-3
Original file line numberDiff line numberDiff line change
@@ -3152,7 +3152,9 @@ Afterward, additional manipulation of the wrapper's prototype chain may cause
31523152
31533153
*Note*: Calling `napi_wrap()` a second time on an object that already has a
31543154
native instance associated with it by virtue of a previous call to
3155-
`napi_wrap()` will cause an error to be returned.
3155+
`napi_wrap()` will cause an error to be returned. If you wish to associate
3156+
another native instance with the given object, call `napi_remove_wrap()` on it
3157+
first.
31563158
31573159
### *napi_unwrap*
31583160
<!-- YAML
@@ -3165,8 +3167,8 @@ napi_status napi_unwrap(napi_env env,
31653167
```
31663168

31673169
- `[in] env`: The environment that the API is invoked under.
3168-
- `[in] js_object`: The object associated with the C++ class instance.
3169-
- `[out] result`: Pointer to the wrapped C++ class instance.
3170+
- `[in] js_object`: The object associated with the native instance.
3171+
- `[out] result`: Pointer to the wrapped native instance.
31703172

31713173
Returns `napi_ok` if the API succeeded.
31723174

@@ -3179,6 +3181,28 @@ method or accessor, then the `this` argument to the callback is the wrapper
31793181
object; the wrapped C++ instance that is the target of the call can be obtained
31803182
then by calling `napi_unwrap()` on the wrapper object.
31813183

3184+
### *napi_remove_wrap*
3185+
<!-- YAML
3186+
added: REPLACEME
3187+
-->
3188+
```C
3189+
napi_status napi_remove_wrap(napi_env env,
3190+
napi_value js_object,
3191+
void** result);
3192+
```
3193+
3194+
- `[in] env`: The environment that the API is invoked under.
3195+
- `[in] js_object`: The object associated with the native instance.
3196+
- `[out] result`: Pointer to the wrapped native instance.
3197+
3198+
Returns `napi_ok` if the API succeeded.
3199+
3200+
Retrieves a native instance that was previously wrapped in the JavaScript
3201+
object `js_object` using `napi_wrap()` and removes the wrapping, thereby
3202+
restoring the JavaScript object's prototype chain. If a finalize callback was
3203+
associated with the wrapping, it will no longer be called when the JavaScript
3204+
object becomes garbage-collected.
3205+
31823206
## Asynchronous Operations
31833207
31843208
Addon modules often need to leverage async helpers from libuv as part of their

src/node_api.cc

+66-21
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ v8::Local<v8::Object> CreateAccessorCallbackData(napi_env env,
674674
return cbdata;
675675
}
676676

677+
int kWrapperFields = 3;
678+
677679
// Pointer used to identify items wrapped by N-API. Used by FindWrapper and
678680
// napi_wrap().
679681
const char napi_wrap_name[] = "N-API Wrapper";
@@ -682,16 +684,20 @@ const char napi_wrap_name[] = "N-API Wrapper";
682684
// wrapper would be the first in the chain, but it is OK for other objects to
683685
// be inserted in the prototype chain.
684686
bool FindWrapper(v8::Local<v8::Object> obj,
685-
v8::Local<v8::Object>* result = nullptr) {
687+
v8::Local<v8::Object>* result = nullptr,
688+
v8::Local<v8::Object>* parent = nullptr) {
686689
v8::Local<v8::Object> wrapper = obj;
687690

688691
do {
689692
v8::Local<v8::Value> proto = wrapper->GetPrototype();
690693
if (proto.IsEmpty() || !proto->IsObject()) {
691694
return false;
692695
}
696+
if (parent != nullptr) {
697+
*parent = wrapper;
698+
}
693699
wrapper = proto.As<v8::Object>();
694-
if (wrapper->InternalFieldCount() == 2) {
700+
if (wrapper->InternalFieldCount() == kWrapperFields) {
695701
v8::Local<v8::Value> external = wrapper->GetInternalField(1);
696702
if (external->IsExternal() &&
697703
external.As<v8::External>()->Value() == v8impl::napi_wrap_name) {
@@ -745,6 +751,29 @@ napi_env GetEnv(v8::Local<v8::Context> context) {
745751
return result;
746752
}
747753

754+
napi_status Unwrap(napi_env env,
755+
napi_value js_object,
756+
void** result,
757+
v8::Local<v8::Object>* wrapper,
758+
v8::Local<v8::Object>* parent = nullptr) {
759+
CHECK_ARG(env, js_object);
760+
CHECK_ARG(env, result);
761+
762+
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
763+
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
764+
v8::Local<v8::Object> obj = value.As<v8::Object>();
765+
766+
RETURN_STATUS_IF_FALSE(
767+
env, v8impl::FindWrapper(obj, wrapper, parent), napi_invalid_arg);
768+
769+
v8::Local<v8::Value> unwrappedValue = (*wrapper)->GetInternalField(0);
770+
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);
771+
772+
*result = unwrappedValue.As<v8::External>()->Value();
773+
774+
return napi_ok;
775+
}
776+
748777
} // end of namespace v8impl
749778

750779
// Intercepts the Node-V8 module registration callback. Converts parameters
@@ -2266,62 +2295,78 @@ napi_status napi_wrap(napi_env env,
22662295
// Create a wrapper object with an internal field to hold the wrapped pointer
22672296
// and a second internal field to identify the owner as N-API.
22682297
v8::Local<v8::ObjectTemplate> wrapper_template;
2269-
ENV_OBJECT_TEMPLATE(env, wrap, wrapper_template, 2);
2298+
ENV_OBJECT_TEMPLATE(env, wrap, wrapper_template, v8impl::kWrapperFields);
22702299

22712300
auto maybe_object = wrapper_template->NewInstance(context);
22722301
CHECK_MAYBE_EMPTY(env, maybe_object, napi_generic_failure);
2273-
22742302
v8::Local<v8::Object> wrapper = maybe_object.ToLocalChecked();
2275-
wrapper->SetInternalField(1, v8::External::New(isolate,
2276-
reinterpret_cast<void*>(const_cast<char*>(v8impl::napi_wrap_name))));
22772303

22782304
// Store the pointer as an external in the wrapper.
22792305
wrapper->SetInternalField(0, v8::External::New(isolate, native_object));
2306+
wrapper->SetInternalField(1, v8::External::New(isolate,
2307+
reinterpret_cast<void*>(const_cast<char*>(v8impl::napi_wrap_name))));
22802308

22812309
// Insert the wrapper into the object's prototype chain.
22822310
v8::Local<v8::Value> proto = obj->GetPrototype();
22832311
CHECK(wrapper->SetPrototype(context, proto).FromJust());
22842312
CHECK(obj->SetPrototype(context, wrapper).FromJust());
22852313

2314+
v8impl::Reference* reference = nullptr;
22862315
if (result != nullptr) {
22872316
// The returned reference should be deleted via napi_delete_reference()
22882317
// ONLY in response to the finalize callback invocation. (If it is deleted
22892318
// before then, then the finalize callback will never be invoked.)
22902319
// Therefore a finalize callback is required when returning a reference.
22912320
CHECK_ARG(env, finalize_cb);
2292-
v8impl::Reference* reference = v8impl::Reference::New(
2321+
reference = v8impl::Reference::New(
22932322
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
22942323
*result = reinterpret_cast<napi_ref>(reference);
22952324
} else if (finalize_cb != nullptr) {
22962325
// Create a self-deleting reference just for the finalize callback.
2297-
v8impl::Reference::New(
2326+
reference = v8impl::Reference::New(
22982327
env, obj, 0, true, finalize_cb, native_object, finalize_hint);
22992328
}
23002329

2330+
if (reference != nullptr) {
2331+
wrapper->SetInternalField(2, v8::External::New(isolate, reference));
2332+
}
2333+
23012334
return GET_RETURN_STATUS(env);
23022335
}
23032336

2304-
napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) {
2337+
napi_status napi_unwrap(napi_env env, napi_value obj, void** result) {
23052338
// Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw
23062339
// JS exceptions.
23072340
CHECK_ENV(env);
2308-
CHECK_ARG(env, js_object);
2309-
CHECK_ARG(env, result);
2310-
2311-
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
2312-
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
2313-
v8::Local<v8::Object> obj = value.As<v8::Object>();
2341+
v8::Local<v8::Object> wrapper;
2342+
return napi_set_last_error(env, v8impl::Unwrap(env, obj, result, &wrapper));
2343+
}
23142344

2345+
napi_status napi_remove_wrap(napi_env env, napi_value obj, void** result) {
2346+
NAPI_PREAMBLE(env);
23152347
v8::Local<v8::Object> wrapper;
2316-
RETURN_STATUS_IF_FALSE(
2317-
env, v8impl::FindWrapper(obj, &wrapper), napi_invalid_arg);
2348+
v8::Local<v8::Object> parent;
2349+
napi_status status = v8impl::Unwrap(env, obj, result, &wrapper, &parent);
2350+
if (status != napi_ok) {
2351+
return napi_set_last_error(env, status);
2352+
}
23182353

2319-
v8::Local<v8::Value> unwrappedValue = wrapper->GetInternalField(0);
2320-
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);
2354+
v8::Local<v8::Value> external = wrapper->GetInternalField(2);
2355+
if (external->IsExternal()) {
2356+
v8impl::Reference::Delete(
2357+
static_cast<v8impl::Reference*>(external.As<v8::External>()->Value()));
2358+
}
23212359

2322-
*result = unwrappedValue.As<v8::External>()->Value();
2360+
if (!parent.IsEmpty()) {
2361+
v8::Maybe<bool> maybe = parent->SetPrototype(
2362+
env->isolate->GetCurrentContext(), wrapper->GetPrototype());
2363+
CHECK_MAYBE_NOTHING(env, maybe, napi_generic_failure);
2364+
if (!maybe.FromMaybe(false)) {
2365+
return napi_set_last_error(env, napi_generic_failure);
2366+
}
2367+
}
23232368

2324-
return napi_clear_last_error(env);
2369+
return GET_RETURN_STATUS(env);
23252370
}
23262371

23272372
napi_status napi_create_external(napi_env env,

src/node_api.h

+3
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,9 @@ NAPI_EXTERN napi_status napi_wrap(napi_env env,
362362
NAPI_EXTERN napi_status napi_unwrap(napi_env env,
363363
napi_value js_object,
364364
void** result);
365+
NAPI_EXTERN napi_status napi_remove_wrap(napi_env env,
366+
napi_value js_object,
367+
void** result);
365368
NAPI_EXTERN napi_status napi_create_external(napi_env env,
366369
void* data,
367370
napi_finalize finalize_cb,

test/addons-napi/test_general/test.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
// Flags: --expose-gc
23

34
const common = require('../../common');
45
const test_general = require(`./build/${common.buildType}/test_general`);
@@ -56,10 +57,37 @@ assert.strictEqual(release, process.release.name);
5657
// for null
5758
assert.strictEqual(test_general.testNapiTypeof(null), 'null');
5859

59-
const x = {};
60+
// Ensure that garbage collecting an object with a wrapped native item results
61+
// in the finalize callback being called.
62+
let w = {};
63+
test_general.wrap(w, []);
64+
w = null;
65+
global.gc();
66+
assert.strictEqual(test_general.derefItemWasCalled(), true,
67+
'deref_item() was called upon garbage collecting a ' +
68+
'wrapped object');
6069

6170
// Assert that wrapping twice fails.
71+
const x = {};
6272
test_general.wrap(x, 25);
6373
assert.throws(function() {
6474
test_general.wrap(x, 'Blah');
6575
}, Error);
76+
77+
// Ensure that wrapping, removing the wrap, and then wrapping again works.
78+
const y = {};
79+
test_general.wrap(y, -12);
80+
test_general.removeWrap(y);
81+
assert.doesNotThrow(function() {
82+
test_general.wrap(y, 're-wrap!');
83+
}, Error, 'Wrapping twice succeeds if a remove_wrap() separates the instances');
84+
85+
// Ensure that removing a wrap and garbage collecting does not fire the
86+
// finalize callback.
87+
let z = {};
88+
test_general.testFinalizeWrap(z);
89+
test_general.removeWrap(z);
90+
z = null;
91+
global.gc();
92+
assert.strictEqual(test_general.finalizeWasCalled(), false,
93+
'finalize callback was not called upon garbage collection');

test/addons-napi/test_general/test_general.c

+53
Original file line numberDiff line numberDiff line change
@@ -138,24 +138,73 @@ napi_value testNapiTypeof(napi_env env, napi_callback_info info) {
138138
return result;
139139
}
140140

141+
static bool deref_item_called = false;
141142
static void deref_item(napi_env env, void* data, void* hint) {
142143
(void) hint;
143144

145+
deref_item_called = true;
144146
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, (napi_ref)data));
145147
}
146148

149+
napi_value deref_item_was_called(napi_env env, napi_callback_info info) {
150+
napi_value it_was_called;
151+
152+
NAPI_CALL(env, napi_get_boolean(env, deref_item_called, &it_was_called));
153+
154+
return it_was_called;
155+
}
156+
147157
napi_value wrap(napi_env env, napi_callback_info info) {
148158
size_t argc = 2;
149159
napi_value argv[2];
150160
napi_ref payload;
151161

162+
deref_item_called = false;
163+
152164
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
153165
NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &payload));
154166
NAPI_CALL(env, napi_wrap(env, argv[0], payload, deref_item, NULL, NULL));
155167

156168
return NULL;
157169
}
158170

171+
napi_value remove_wrap(napi_env env, napi_callback_info info) {
172+
size_t argc = 1;
173+
napi_value wrapped;
174+
void* data;
175+
176+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &wrapped, NULL, NULL));
177+
NAPI_CALL(env, napi_remove_wrap(env, wrapped, &data));
178+
if (data != NULL) {
179+
NAPI_CALL(env, napi_delete_reference(env, (napi_ref)data));
180+
}
181+
182+
return NULL;
183+
}
184+
185+
static bool finalize_called = false;
186+
static void test_finalize(napi_env env, void* data, void* hint) {
187+
finalize_called = true;
188+
}
189+
190+
napi_value test_finalize_wrap(napi_env env, napi_callback_info info) {
191+
size_t argc = 1;
192+
napi_value js_object;
193+
194+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &js_object, NULL, NULL));
195+
NAPI_CALL(env, napi_wrap(env, js_object, NULL, test_finalize, NULL, NULL));
196+
197+
return NULL;
198+
}
199+
200+
napi_value finalize_was_called(napi_env env, napi_callback_info info) {
201+
napi_value it_was_called;
202+
203+
NAPI_CALL(env, napi_get_boolean(env, finalize_called, &it_was_called));
204+
205+
return it_was_called;
206+
}
207+
159208
void Init(napi_env env, napi_value exports, napi_value module, void* priv) {
160209
napi_property_descriptor descriptors[] = {
161210
DECLARE_NAPI_PROPERTY("testStrictEquals", testStrictEquals),
@@ -169,6 +218,10 @@ void Init(napi_env env, napi_value exports, napi_value module, void* priv) {
169218
DECLARE_NAPI_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup),
170219
DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof),
171220
DECLARE_NAPI_PROPERTY("wrap", wrap),
221+
DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap),
222+
DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap),
223+
DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called),
224+
DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called),
172225
};
173226

174227
NAPI_CALL_RETURN_VOID(env, napi_define_properties(

0 commit comments

Comments
 (0)