Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
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.

Fixes: nodejs/abi-stable-node#266
  • Loading branch information
Gabriel Schulhof
Gabriel Schulhof committed Aug 17, 2017
commit 0eb969b13ee49122380b73f2e37525834ea6ca67
28 changes: 25 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,26 @@ 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
Copy Markdown
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
Copy Markdown
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().

@jasongin jasongin Aug 7, 2017 •

Copy link
Copy Markdown
Member

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
Copy Markdown
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this invoke the finalizer callback of the previous wrapping? Should it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My intention was that it not call the finalizer callback. I need to test this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why shouldn't it?

Either way, the behavior needs to be documented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If it calls the finalizer callback it's kinda useless to return the pointer, since it's already freed. I guess my concept was that by removing the wrapping, you resume responsibility for freeing the pointer, having previously assigned such responsibility to the VM.

... and I need to update the documentation ...


## 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
Copy Markdown
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
Copy Markdown
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.

@jasongin jasongin Aug 7, 2017 •

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

@addaleax addaleax Aug 7, 2017 •

Copy link
Copy Markdown
Member

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
Copy Markdown
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
11 changes: 9 additions & 2 deletions test/addons-napi/test_general/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,17 @@ assert.strictEqual(release, process.release.name);
// for null
assert.strictEqual(test_general.testNapiTypeof(null), 'null');

const x = {};

// 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');
13 changes: 13 additions & 0 deletions test/addons-napi/test_general/test_general.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ napi_value wrap(napi_env env, napi_callback_info info) {
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));
NAPI_CALL(env, napi_delete_reference(env, (napi_ref)data));

return NULL;
}

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 +181,7 @@ 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),
};

NAPI_CALL_RETURN_VOID(env, napi_define_properties(
Expand Down