Skip to content

Commit a95f080

Browse files
jasonginjasnell
authored andcommitted
n-api: enable napi_wrap() to work with any object
Previously, napi_wrap() would only work with objects created from a constructor returned by napi_define_class(). While the N-API team was aware of this limitation, it was not clearly documented and is likely to cause confusion anyway. It's much simpler if addons are allowed to use any JS object. Also, the specific behavior of the limitation is difficult to reimplement on other VMs that work differently from V8. V8 requires object internal fields to be declared on the object prototype (which napi_define_class() used to do). Since it's too late to modify the object prototype by the time napi_wrap() is called, napi_wrap() now inserts a new object (with the internal field) into the supplied object's prototype chain. Then it can be retrieved from there later by napi_unwrap(). This change also includes improvements to the documentation for napi_create_external(), partly to explain how it is different from napi_wrap(). PR-URL: #13250 Reviewed-By: Michael Dawson <[email protected]>
1 parent b8b0bfb commit a95f080

File tree

4 files changed

+159
-65
lines changed

4 files changed

+159
-65
lines changed

doc/api/n-api.md

+36-21
Original file line numberDiff line numberDiff line change
@@ -1059,20 +1059,24 @@ napi_status napi_create_external(napi_env env,
10591059
```
10601060

10611061
- `[in] env`: The environment that the API is invoked under.
1062-
- `[in] data`: Raw pointer to the external data being wrapped.
1063-
- `[in] finalize_cb`: Optional callback to call when the wrapped object
1062+
- `[in] data`: Raw pointer to the external data.
1063+
- `[in] finalize_cb`: Optional callback to call when the external value
10641064
is being collected.
10651065
- `[in] finalize_hint`: Optional hint to pass to the finalize callback
10661066
during collection.
1067-
- `[out] result`: A `napi_value` representing an external object.
1067+
- `[out] result`: A `napi_value` representing an external value.
10681068

10691069
Returns `napi_ok` if the API succeeded.
10701070

1071-
This API allocates a JavaScript object with external data attached to it.
1072-
This is used to wrap native objects and project them into JavaScript.
1073-
The API allows the caller to pass in a finalize callback, in case the
1074-
underlying native resource needs to be cleaned up when the wrapper
1075-
JavaScript object gets collected.
1071+
This API allocates a JavaScript value with external data attached to it. This
1072+
is used to pass external data through JavaScript code, so it can be retrieved
1073+
later by native code. The API allows the caller to pass in a finalize callback,
1074+
in case the underlying native resource needs to be cleaned up when the external
1075+
JavaScript value gets collected.
1076+
1077+
*Note*: The created value is not an object, and therefore does not support
1078+
additional properties. It is considered a distinct value type: calling
1079+
`napi_typeof()` with an external value yields `napi_external`.
10761080

10771081
#### napi_create_external_arraybuffer
10781082
<!-- YAML
@@ -1364,7 +1368,8 @@ Returns `napi_ok` if the API succeeded.
13641368
13651369
This API is used to retrieve the underlying data buffer of an ArrayBuffer and
13661370
its length.
1367-
WARNING: Use caution while using this API. The lifetime of the underlying data
1371+
1372+
*WARNING*: Use caution while using this API. The lifetime of the underlying data
13681373
buffer is managed by the ArrayBuffer even after it's returned. A
13691374
possible safe way to use this API is in conjunction with [`napi_create_reference`][],
13701375
which can be used to guarantee control over the lifetime of the
@@ -1391,7 +1396,8 @@ Returns `napi_ok` if the API succeeded.
13911396

13921397
This API is used to retrieve the underlying data buffer of a `node::Buffer`
13931398
and it's length.
1394-
Warning: Use caution while using this API since the underlying data buffer's
1399+
1400+
*Warning*: Use caution while using this API since the underlying data buffer's
13951401
lifetime is not guaranteed if it's managed by the VM.
13961402

13971403
#### *napi_get_prototype*
@@ -1438,7 +1444,8 @@ to start projecting the TypedArray.
14381444
Returns `napi_ok` if the API succeeded.
14391445

14401446
This API returns various properties of a typed array.
1441-
Warning: Use caution while using this API since the underlying data buffer
1447+
1448+
*Warning*: Use caution while using this API since the underlying data buffer
14421449
is managed by the VM
14431450

14441451
#### *napi_get_value_bool*
@@ -1457,8 +1464,8 @@ Boolean.
14571464
Returns `napi_ok` if the API succeeded. If a non-boolean `napi_value` is
14581465
passed in it returns `napi_boolean_expected`.
14591466
1460-
This API returns C boolean primitive equivalent of the given JavaScript
1461-
Boolea
1467+
This API returns the C boolean primitive equivalent of the given JavaScript
1468+
Boolean.
14621469
14631470
#### *napi_get_value_double*
14641471
<!-- YAML
@@ -1493,14 +1500,14 @@ napi_status napi_get_value_external(napi_env env,
14931500
```
14941501
14951502
- `[in] env`: The environment that the API is invoked under.
1496-
- `[in] value`: `napi_value` representing JavaScript External value.
1497-
- `[out] result`: Pointer to the data wrapped by the JavaScript External value.
1503+
- `[in] value`: `napi_value` representing JavaScript external value.
1504+
- `[out] result`: Pointer to the data wrapped by the JavaScript external value.
14981505
14991506
Returns `napi_ok` if the API succeeded. If a non-external `napi_value` is
15001507
passed in it returns `napi_invalid_arg`.
15011508
1502-
This API returns the pointer to the data wrapped by the JavaScript
1503-
External value
1509+
This API retrieves the external data pointer that was previously passed to
1510+
`napi_create_external()`.
15041511
15051512
#### *napi_get_value_int32*
15061513
<!-- YAML
@@ -2770,6 +2777,7 @@ napi_status napi_wrap(napi_env env,
27702777
Returns `napi_ok` if the API succeeded.
27712778
27722779
Wraps a native instance in JavaScript object of the corresponding type.
2780+
The native instance can be retrieved later using `napi_unwrap()`.
27732781
27742782
When JavaScript code invokes a constructor for a class that was defined using
27752783
`napi_define_class()`, the `napi_callback` for the constructor is invoked.
@@ -2787,12 +2795,16 @@ The optional returned reference is initially a weak reference, meaning it
27872795
has a reference count of 0. Typically this reference count would be incremented
27882796
temporarily during async operations that require the instance to remain valid.
27892797
2790-
Caution: The optional returned reference (if obtained) should be deleted via
2791-
[`napi_delete_reference`][] ONLY in response to the finalize callback invocation.
2792-
(If it is deleted before then, then the finalize callback may never be
2793-
invoked.) Therefore when obtaining a reference a finalize callback is also
2798+
*Caution*: The optional returned reference (if obtained) should be deleted via
2799+
[`napi_delete_reference`][] ONLY in response to the finalize callback
2800+
invocation. (If it is deleted before then, then the finalize callback may never
2801+
be invoked.) Therefore, when obtaining a reference a finalize callback is also
27942802
required in order to enable correct proper of the reference.
27952803
2804+
*Note*: This API may modify the prototype chain of the wrapper object.
2805+
Afterward, additional manipulation of the wrapper's prototype chain may cause
2806+
`napi_unwrap()` to fail.
2807+
27962808
### *napi_unwrap*
27972809
<!-- YAML
27982810
added: v8.0.0
@@ -2809,6 +2821,9 @@ napi_status napi_unwrap(napi_env env,
28092821

28102822
Returns `napi_ok` if the API succeeded.
28112823

2824+
Retrieves a native instance that was previously wrapped in a JavaScript
2825+
object using `napi_wrap()`.
2826+
28122827
When JavaScript code invokes a method or property accessor on the class, the
28132828
corresponding `napi_callback` is invoked. If the callback is for an instance
28142829
method or accessor, then the `this` argument to the callback is the wrapper

src/node_api.cc

+28-14
Original file line numberDiff line numberDiff line change
@@ -819,9 +819,6 @@ napi_status napi_define_class(napi_env env,
819819
v8::Local<v8::FunctionTemplate> tpl = v8::FunctionTemplate::New(
820820
isolate, v8impl::FunctionCallbackWrapper::Invoke, cbdata);
821821

822-
// we need an internal field to stash the wrapped object
823-
tpl->InstanceTemplate()->SetInternalFieldCount(1);
824-
825822
v8::Local<v8::String> name_string;
826823
CHECK_NEW_FROM_UTF8(env, name_string, utf8name);
827824
tpl->SetClassName(name_string);
@@ -1950,14 +1947,24 @@ napi_status napi_wrap(napi_env env,
19501947
CHECK_ARG(env, js_object);
19511948

19521949
v8::Isolate* isolate = env->isolate;
1953-
v8::Local<v8::Object> obj =
1954-
v8impl::V8LocalValueFromJsValue(js_object).As<v8::Object>();
1950+
v8::Local<v8::Context> context = isolate->GetCurrentContext();
19551951

1956-
// Only objects that were created from a NAPI constructor's prototype
1957-
// via napi_define_class() can be (un)wrapped.
1958-
RETURN_STATUS_IF_FALSE(env, obj->InternalFieldCount() > 0, napi_invalid_arg);
1952+
v8::Local<v8::Value> value = v8impl::V8LocalValueFromJsValue(js_object);
1953+
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
1954+
v8::Local<v8::Object> obj = value.As<v8::Object>();
1955+
1956+
// Create a wrapper object with an internal field to hold the wrapped pointer.
1957+
v8::Local<v8::ObjectTemplate> wrapperTemplate =
1958+
v8::ObjectTemplate::New(isolate);
1959+
wrapperTemplate->SetInternalFieldCount(1);
1960+
v8::Local<v8::Object> wrapper =
1961+
wrapperTemplate->NewInstance(context).ToLocalChecked();
1962+
wrapper->SetInternalField(0, v8::External::New(isolate, native_object));
19591963

1960-
obj->SetInternalField(0, v8::External::New(isolate, native_object));
1964+
// Insert the wrapper into the object's prototype chain.
1965+
v8::Local<v8::Value> proto = obj->GetPrototype();
1966+
wrapper->SetPrototype(proto);
1967+
obj->SetPrototype(wrapper);
19611968

19621969
if (result != nullptr) {
19631970
// The returned reference should be deleted via napi_delete_reference()
@@ -1988,11 +1995,18 @@ napi_status napi_unwrap(napi_env env, napi_value js_object, void** result) {
19881995
RETURN_STATUS_IF_FALSE(env, value->IsObject(), napi_invalid_arg);
19891996
v8::Local<v8::Object> obj = value.As<v8::Object>();
19901997

1991-
// Only objects that were created from a NAPI constructor's prototype
1992-
// via napi_define_class() can be (un)wrapped.
1993-
RETURN_STATUS_IF_FALSE(env, obj->InternalFieldCount() > 0, napi_invalid_arg);
1994-
1995-
v8::Local<v8::Value> unwrappedValue = obj->GetInternalField(0);
1998+
// Search the object's prototype chain for the wrapper with an internal field.
1999+
// Usually the wrapper would be the first in the chain, but it is OK for
2000+
// other objects to be inserted in the prototype chain.
2001+
v8::Local<v8::Object> wrapper = obj;
2002+
do {
2003+
v8::Local<v8::Value> proto = wrapper->GetPrototype();
2004+
RETURN_STATUS_IF_FALSE(
2005+
env, !proto.IsEmpty() && proto->IsObject(), napi_invalid_arg);
2006+
wrapper = proto.As<v8::Object>();
2007+
} while (wrapper->InternalFieldCount() != 1);
2008+
2009+
v8::Local<v8::Value> unwrappedValue = wrapper->GetInternalField(0);
19962010
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);
19972011

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

test/addons-napi/test_object/test.js

+68-30
Original file line numberDiff line numberDiff line change
@@ -31,35 +31,73 @@ assert(test_object.Has(newObject, 'test_number'));
3131
assert.strictEqual(newObject.test_number, 987654321);
3232
assert.strictEqual(newObject.test_string, 'test string');
3333

34-
// test_object.Inflate increases all properties by 1
35-
const cube = {
36-
x: 10,
37-
y: 10,
38-
z: 10
39-
};
34+
{
35+
// test_object.Inflate increases all properties by 1
36+
const cube = {
37+
x: 10,
38+
y: 10,
39+
z: 10
40+
};
4041

41-
assert.deepStrictEqual(test_object.Inflate(cube), {x: 11, y: 11, z: 11});
42-
assert.deepStrictEqual(test_object.Inflate(cube), {x: 12, y: 12, z: 12});
43-
assert.deepStrictEqual(test_object.Inflate(cube), {x: 13, y: 13, z: 13});
44-
cube.t = 13;
45-
assert.deepStrictEqual(test_object.Inflate(cube), {x: 14, y: 14, z: 14, t: 14});
46-
47-
const sym1 = Symbol('1');
48-
const sym2 = Symbol('2');
49-
const sym3 = Symbol('3');
50-
const sym4 = Symbol('4');
51-
const object2 = {
52-
[sym1]: '@@iterator',
53-
[sym2]: sym3
54-
};
42+
assert.deepStrictEqual(test_object.Inflate(cube), {x: 11, y: 11, z: 11});
43+
assert.deepStrictEqual(test_object.Inflate(cube), {x: 12, y: 12, z: 12});
44+
assert.deepStrictEqual(test_object.Inflate(cube), {x: 13, y: 13, z: 13});
45+
cube.t = 13;
46+
assert.deepStrictEqual(
47+
test_object.Inflate(cube), {x: 14, y: 14, z: 14, t: 14});
48+
49+
const sym1 = Symbol('1');
50+
const sym2 = Symbol('2');
51+
const sym3 = Symbol('3');
52+
const sym4 = Symbol('4');
53+
const object2 = {
54+
[sym1]: '@@iterator',
55+
[sym2]: sym3
56+
};
57+
58+
assert(test_object.Has(object2, sym1));
59+
assert(test_object.Has(object2, sym2));
60+
assert.strictEqual(test_object.Get(object2, sym1), '@@iterator');
61+
assert.strictEqual(test_object.Get(object2, sym2), sym3);
62+
assert(test_object.Set(object2, 'string', 'value'));
63+
assert(test_object.Set(object2, sym4, 123));
64+
assert(test_object.Has(object2, 'string'));
65+
assert(test_object.Has(object2, sym4));
66+
assert.strictEqual(test_object.Get(object2, 'string'), 'value');
67+
assert.strictEqual(test_object.Get(object2, sym4), 123);
68+
}
69+
70+
{
71+
// Wrap a pointer in a JS object, then verify the pointer can be unwrapped.
72+
const wrapper = {};
73+
test_object.Wrap(wrapper);
74+
75+
assert(test_object.Unwrap(wrapper));
76+
}
77+
78+
{
79+
// Verify that wrapping doesn't break an object's prototype chain.
80+
const wrapper = {};
81+
const protoA = { protoA: true };
82+
Object.setPrototypeOf(wrapper, protoA);
83+
test_object.Wrap(wrapper);
84+
85+
assert(test_object.Unwrap(wrapper));
86+
assert(wrapper.protoA);
87+
}
88+
89+
{
90+
// Verify the pointer can be unwrapped after inserting in the prototype chain.
91+
const wrapper = {};
92+
const protoA = { protoA: true };
93+
Object.setPrototypeOf(wrapper, protoA);
94+
test_object.Wrap(wrapper);
95+
96+
const protoB = { protoB: true };
97+
Object.setPrototypeOf(protoB, Object.getPrototypeOf(wrapper));
98+
Object.setPrototypeOf(wrapper, protoB);
5599

56-
assert(test_object.Has(object2, sym1));
57-
assert(test_object.Has(object2, sym2));
58-
assert.strictEqual(test_object.Get(object2, sym1), '@@iterator');
59-
assert.strictEqual(test_object.Get(object2, sym2), sym3);
60-
assert(test_object.Set(object2, 'string', 'value'));
61-
assert(test_object.Set(object2, sym4, 123));
62-
assert(test_object.Has(object2, 'string'));
63-
assert(test_object.Has(object2, sym4));
64-
assert.strictEqual(test_object.Get(object2, 'string'), 'value');
65-
assert.strictEqual(test_object.Get(object2, sym4), 123);
100+
assert(test_object.Unwrap(wrapper));
101+
assert(wrapper.protoA, true);
102+
assert(wrapper.protoB, true);
103+
}

test/addons-napi/test_object/test_object.c

+27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <node_api.h>
22
#include "../common.h"
33
#include <string.h>
4+
#include <stdlib.h>
45

56
napi_value Get(napi_env env, napi_callback_info info) {
67
size_t argc = 2;
@@ -138,13 +139,39 @@ napi_value Inflate(napi_env env, napi_callback_info info) {
138139
return obj;
139140
}
140141

142+
napi_value Wrap(napi_env env, napi_callback_info info) {
143+
size_t argc = 1;
144+
napi_value arg;
145+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL));
146+
147+
int32_t* data = malloc(sizeof(int32_t));
148+
*data = 3;
149+
NAPI_CALL(env, napi_wrap(env, arg, data, NULL, NULL, NULL));
150+
return NULL;
151+
}
152+
153+
napi_value Unwrap(napi_env env, napi_callback_info info) {
154+
size_t argc = 1;
155+
napi_value arg;
156+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL));
157+
158+
int32_t* data;
159+
NAPI_CALL(env, napi_unwrap(env, arg, &data));
160+
161+
napi_value result;
162+
NAPI_CALL(env, napi_get_boolean(env, data != NULL && *data == 3, &result));
163+
return result;
164+
}
165+
141166
void Init(napi_env env, napi_value exports, napi_value module, void* priv) {
142167
napi_property_descriptor descriptors[] = {
143168
DECLARE_NAPI_PROPERTY("Get", Get),
144169
DECLARE_NAPI_PROPERTY("Set", Set),
145170
DECLARE_NAPI_PROPERTY("Has", Has),
146171
DECLARE_NAPI_PROPERTY("New", New),
147172
DECLARE_NAPI_PROPERTY("Inflate", Inflate),
173+
DECLARE_NAPI_PROPERTY("Wrap", Wrap),
174+
DECLARE_NAPI_PROPERTY("Unwrap", Unwrap),
148175
};
149176

150177
NAPI_CALL_RETURN_VOID(env, napi_define_properties(

0 commit comments

Comments
 (0)