Repository navigation
n-api: add ability to remove a wrapping #14658
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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. | ||
|
|
||
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this invoke the finalizer callback of the previous wrapping? Should it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why shouldn't it? Either way, the behavior needs to be documented.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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) { | ||
|
|
@@ -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 | ||
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is a try-catch needed here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 In fact, we should re-examine how we deal with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, then we need to check whether there is such a possibility.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 The reason I ask is that the only ones I've seen are property get/set/has/delete and
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, NM.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. I was also thinking of the number APIs like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jasongin But that returns a Maybe because it can throw,
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
||
There was a problem hiding this comment.
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.)There was a problem hiding this comment.
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 anapi_wrap(). Knowing that an unconditionalnapi_wrap()will follow would allow him to drop innapi_remove_wrap()instead ofnapi_unwrap().Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 explicittrue/falseparameter 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?