Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

n-api: add ability to remove a wrapping #14658

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3152,7 +3152,9 @@ Afterward, additional manipulation of the wrapper's prototype chain may cause

*Note*: Calling `napi_wrap()` a second time on an object that already has a
native instance associated with it by virtue of a previous call to
`napi_wrap()` will cause an error to be returned.
`napi_wrap()` will cause an error to be returned. If you wish to associate
another native instance with the given object, call `napi_remove_wrap()` on it
first.

### *napi_unwrap*
<!-- YAML
Expand All @@ -3165,8 +3167,8 @@ napi_status napi_unwrap(napi_env env,
```

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

Returns `napi_ok` if the API succeeded.

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

### *napi_remove_wrap*
<!-- YAML
added: REPLACEME
-->
```C
napi_status napi_remove_wrap(napi_env env,
napi_value js_object,
void** result);
```

- `[in] env`: The environment that the API is invoked under.
- `[in] js_object`: The object associated with the native instance.
- `[out] result`: Pointer to the wrapped native instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why also return the pointer here? That is already available via napi_wrap(). Or, maybe these two APIs could be combined, with a boolean parameter that indicates whether to remove it at the same time. (I'm not sure I actually prefer that though.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's to avoid having to make two API calls. In nodejs/abi-stable-node#266 @sampsongao is trying to do a napi_unwrap() followed unconditionally by a napi_wrap(). Knowing that an unconditional napi_wrap() will follow would allow him to drop in napi_remove_wrap() instead of napi_unwrap().

Copy link
Member

@jasongin jasongin Aug 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're concerned about performance, why not make it work in one napi_wrap() call? Maybe a boolean to allow overwriting a previous wrap? Or return a previous wrap value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mhdawson wanted an explicit napi_remove_wrap(), but, I guess an explicit true/false parameter is also, well, explicit.

If we do this within napi_unwrap(), though, we're treading into semver-major territory - which I OK while in experimental, but still, to be discussed, right?


Returns `napi_ok` if the API succeeded.

Retrieves a native instance that was previously wrapped in the JavaScript
object `js_object` using `napi_wrap()` and removes the wrapping, thereby
restoring the JavaScript object's prototype chain. If a finalize callback was
associated with the wrapping, it will no longer be called when the JavaScript
object becomes garbage-collected.

## Asynchronous Operations

Addon modules often need to leverage async helpers from libuv as part of their
Expand Down
87 changes: 66 additions & 21 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,8 @@ v8::Local<v8::Object> CreateAccessorCallbackData(napi_env env,
return cbdata;
}

int kWrapperFields = 3;

// Pointer used to identify items wrapped by N-API. Used by FindWrapper and
// napi_wrap().
const char napi_wrap_name[] = "N-API Wrapper";
Expand All @@ -682,16 +684,20 @@ const char napi_wrap_name[] = "N-API Wrapper";
// wrapper would be the first in the chain, but it is OK for other objects to
// be inserted in the prototype chain.
bool FindWrapper(v8::Local<v8::Object> obj,
v8::Local<v8::Object>* result = nullptr) {
v8::Local<v8::Object>* result = nullptr,
v8::Local<v8::Object>* parent = nullptr) {
v8::Local<v8::Object> wrapper = obj;

do {
v8::Local<v8::Value> proto = wrapper->GetPrototype();
if (proto.IsEmpty() || !proto->IsObject()) {
return false;
}
if (parent != nullptr) {
*parent = wrapper;
}
wrapper = proto.As<v8::Object>();
if (wrapper->InternalFieldCount() == 2) {
if (wrapper->InternalFieldCount() == kWrapperFields) {
v8::Local<v8::Value> external = wrapper->GetInternalField(1);
if (external->IsExternal() &&
external.As<v8::External>()->Value() == v8impl::napi_wrap_name) {
Expand Down Expand Up @@ -745,6 +751,29 @@ napi_env GetEnv(v8::Local<v8::Context> context) {
return result;
}

napi_status Unwrap(napi_env env,
napi_value js_object,
void** result,
v8::Local<v8::Object>* wrapper,
v8::Local<v8::Object>* parent = nullptr) {
CHECK_ARG(env, js_object);
CHECK_ARG(env, 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>();

RETURN_STATUS_IF_FALSE(
env, v8impl::FindWrapper(obj, wrapper, parent), napi_invalid_arg);

v8::Local<v8::Value> unwrappedValue = (*wrapper)->GetInternalField(0);
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);

*result = unwrappedValue.As<v8::External>()->Value();

return napi_ok;
}

} // end of namespace v8impl

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

auto maybe_object = wrapper_template->NewInstance(context);
CHECK_MAYBE_EMPTY(env, maybe_object, napi_generic_failure);

v8::Local<v8::Object> wrapper = maybe_object.ToLocalChecked();
wrapper->SetInternalField(1, v8::External::New(isolate,
reinterpret_cast<void*>(const_cast<char*>(v8impl::napi_wrap_name))));

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

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

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);
v8impl::Reference* reference = v8impl::Reference::New(
reference = v8impl::Reference::New(
env, obj, 0, false, finalize_cb, native_object, finalize_hint);
*result = reinterpret_cast<napi_ref>(reference);
} else if (finalize_cb != nullptr) {
// Create a self-deleting reference just for the finalize callback.
v8impl::Reference::New(
reference = v8impl::Reference::New(
env, obj, 0, true, finalize_cb, native_object, finalize_hint);
}

if (reference != nullptr) {
wrapper->SetInternalField(2, v8::External::New(isolate, reference));
}

return GET_RETURN_STATUS(env);
}

napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) {
napi_status napi_unwrap(napi_env env, napi_value obj, void** result) {
// Omit NAPI_PREAMBLE and GET_RETURN_STATUS because V8 calls here cannot throw
// JS exceptions.
CHECK_ENV(env);
CHECK_ARG(env, js_object);
CHECK_ARG(env, 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>();
v8::Local<v8::Object> wrapper;
return napi_set_last_error(env, v8impl::Unwrap(env, obj, result, &wrapper));
}

napi_status napi_remove_wrap(napi_env env, napi_value obj, void** result) {
NAPI_PREAMBLE(env);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a try-catch needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took it as a hint from V8 that an exception may be thrown in ->SetPrototype() because it returns a v8::Maybe<bool> - much like .

In fact, we should re-examine how we deal with v8::Maybe(Local)? return values as we concurrently deal with exceptions. I get the impression that an empty/nothing return value happens iff there was an exception. If that's the case then we should perhaps not return napi_generic_failure if the v8::Maybe(Local)? is empty, but we should rather check for an exception first and return napi_exception_pending if we find one, and only return napi_generic_failure if we don't.

Copy link
Member

@jasongin jasongin Aug 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a v8::Maybe return type always means there is a possibility of an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, then we need to check whether there is such a possibility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the Maybe errors can be triggered through Proxies; for example, SetPrototype() fails when a setPrototype() trap throws. It can also throw e.g. when you try to set up a circular prototype chain or something like that, but I don’t think that would happen here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax out of curiosity and AFAUK, does V8 have any APIs which return v8::Maybes or v8::MaybeLocals even though there is no chance that an exception will be thrown in their execution?

The reason I ask is that the only ones I've seen are property get/set/has/delete and SetPrototype, all of which can throw exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, NM. v8::String::NewFromUtf8.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I was also thinking of the number APIs like v8::Value::Int32Value(context), that returns a maybe but doesn't throw.

Copy link
Member

@addaleax addaleax Aug 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasongin But that returns a Maybe because it can throw, Symbol.toPrimitive says hi. ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Good to know. :)

v8::Local<v8::Object> wrapper;
RETURN_STATUS_IF_FALSE(
env, v8impl::FindWrapper(obj, &wrapper), napi_invalid_arg);
v8::Local<v8::Object> parent;
napi_status status = v8impl::Unwrap(env, obj, result, &wrapper, &parent);
if (status != napi_ok) {
return napi_set_last_error(env, status);
}

v8::Local<v8::Value> unwrappedValue = wrapper->GetInternalField(0);
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);
v8::Local<v8::Value> external = wrapper->GetInternalField(2);
if (external->IsExternal()) {
v8impl::Reference::Delete(
static_cast<v8impl::Reference*>(external.As<v8::External>()->Value()));
}

*result = unwrappedValue.As<v8::External>()->Value();
if (!parent.IsEmpty()) {
v8::Maybe<bool> maybe = parent->SetPrototype(
env->isolate->GetCurrentContext(), wrapper->GetPrototype());
CHECK_MAYBE_NOTHING(env, maybe, napi_generic_failure);
if (!maybe.FromMaybe(false)) {
return napi_set_last_error(env, napi_generic_failure);
}
}

return napi_clear_last_error(env);
return GET_RETURN_STATUS(env);
}

napi_status napi_create_external(napi_env env,
Expand Down
3 changes: 3 additions & 0 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,9 @@ NAPI_EXTERN napi_status napi_wrap(napi_env env,
NAPI_EXTERN napi_status napi_unwrap(napi_env env,
napi_value js_object,
void** result);
NAPI_EXTERN napi_status napi_remove_wrap(napi_env env,
napi_value js_object,
void** result);
NAPI_EXTERN napi_status napi_create_external(napi_env env,
void* data,
napi_finalize finalize_cb,
Expand Down
30 changes: 29 additions & 1 deletion test/addons-napi/test_general/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
// Flags: --expose-gc

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

const x = {};
// Ensure that garbage collecting an object with a wrapped native item results
// in the finalize callback being called.
let w = {};
test_general.wrap(w, []);
w = null;
global.gc();
assert.strictEqual(test_general.derefItemWasCalled(), true,
'deref_item() was called upon garbage collecting a ' +
'wrapped object');

// Assert that wrapping twice fails.
const x = {};
test_general.wrap(x, 25);
assert.throws(function() {
test_general.wrap(x, 'Blah');
}, Error);

// Ensure that wrapping, removing the wrap, and then wrapping again works.
const y = {};
test_general.wrap(y, -12);
test_general.removeWrap(y);
assert.doesNotThrow(function() {
test_general.wrap(y, 're-wrap!');
}, Error, 'Wrapping twice succeeds if a remove_wrap() separates the instances');

// Ensure that removing a wrap and garbage collecting does not fire the
// finalize callback.
let z = {};
test_general.testFinalizeWrap(z);
test_general.removeWrap(z);
z = null;
global.gc();
assert.strictEqual(test_general.finalizeWasCalled(), false,
'finalize callback was not called upon garbage collection');
53 changes: 53 additions & 0 deletions test/addons-napi/test_general/test_general.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,73 @@ napi_value testNapiTypeof(napi_env env, napi_callback_info info) {
return result;
}

static bool deref_item_called = false;
static void deref_item(napi_env env, void* data, void* hint) {
(void) hint;

deref_item_called = true;
NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, (napi_ref)data));
}

napi_value deref_item_was_called(napi_env env, napi_callback_info info) {
napi_value it_was_called;

NAPI_CALL(env, napi_get_boolean(env, deref_item_called, &it_was_called));

return it_was_called;
}

napi_value wrap(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value argv[2];
napi_ref payload;

deref_item_called = false;

NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &payload));
NAPI_CALL(env, napi_wrap(env, argv[0], payload, deref_item, NULL, NULL));

return NULL;
}

napi_value remove_wrap(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_remove_wrap(env, wrapped, &data));
if (data != NULL) {
NAPI_CALL(env, napi_delete_reference(env, (napi_ref)data));
}

return NULL;
}

static bool finalize_called = false;
static void test_finalize(napi_env env, void* data, void* hint) {
finalize_called = true;
}

napi_value test_finalize_wrap(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value js_object;

NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &js_object, NULL, NULL));
NAPI_CALL(env, napi_wrap(env, js_object, NULL, test_finalize, NULL, NULL));

return NULL;
}

napi_value finalize_was_called(napi_env env, napi_callback_info info) {
napi_value it_was_called;

NAPI_CALL(env, napi_get_boolean(env, finalize_called, &it_was_called));

return it_was_called;
}

void Init(napi_env env, napi_value exports, napi_value module, void* priv) {
napi_property_descriptor descriptors[] = {
DECLARE_NAPI_PROPERTY("testStrictEquals", testStrictEquals),
Expand All @@ -169,6 +218,10 @@ void Init(napi_env env, napi_value exports, napi_value module, void* priv) {
DECLARE_NAPI_PROPERTY("testNapiErrorCleanup", testNapiErrorCleanup),
DECLARE_NAPI_PROPERTY("testNapiTypeof", testNapiTypeof),
DECLARE_NAPI_PROPERTY("wrap", wrap),
DECLARE_NAPI_PROPERTY("removeWrap", remove_wrap),
DECLARE_NAPI_PROPERTY("testFinalizeWrap", test_finalize_wrap),
DECLARE_NAPI_PROPERTY("finalizeWasCalled", finalize_was_called),
DECLARE_NAPI_PROPERTY("derefItemWasCalled", deref_item_was_called),
};

NAPI_CALL_RETURN_VOID(env, napi_define_properties(
Expand Down