From 77f7200cce91b02b7c193b2e9b7c12c0ae037b24 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Tue, 21 Mar 2023 21:47:08 -0700 Subject: [PATCH] node-api: extend type-tagging to externals Since externals behave as JavaScript objects on the JavaScript side, allow them to be type-tagged. Signed-off-by: Gabriel Schulhof PR-URL: https://github.com/nodejs/node/pull/47141 Reviewed-By: Michael Dawson Reviewed-By: Chengzhong Wu --- doc/api/n-api.md | 31 +++--- test/js-native-api/test_object/test.js | 23 ++++ test/js-native-api/test_object/test_object.c | 108 +++++++++++++------ 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 8b39e0fe158d58..b1e65e989b39d2 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -733,13 +733,13 @@ napiVersion: 8 --> A 128-bit value stored as two unsigned 64-bit integers. It serves as a UUID -with which JavaScript objects can be "tagged" in order to ensure that they are -of a certain type. This is a stronger check than [`napi_instanceof`][], because -the latter can report a false positive if the object's prototype has been -manipulated. Type-tagging is most useful in conjunction with [`napi_wrap`][] -because it ensures that the pointer retrieved from a wrapped object can be -safely cast to the native type corresponding to the type tag that had been -previously applied to the JavaScript object. +with which JavaScript objects or [externals][] can be "tagged" in order to +ensure that they are of a certain type. This is a stronger check than +[`napi_instanceof`][], because the latter can report a false positive if the +object's prototype has been manipulated. Type-tagging is most useful in +conjunction with [`napi_wrap`][] because it ensures that the pointer retrieved +from a wrapped object can be safely cast to the native type corresponding to the +type tag that had been previously applied to the JavaScript object. ```c typedef struct { @@ -4967,7 +4967,7 @@ To this end, Node-API provides type-tagging capabilities. A type tag is a 128-bit integer unique to the addon. Node-API provides the `napi_type_tag` structure for storing a type tag. When such a value is passed -along with a JavaScript object stored in a `napi_value` to +along with a JavaScript object or [external][] stored in a `napi_value` to `napi_type_tag_object()`, the JavaScript object will be "marked" with the type tag. The "mark" is invisible on the JavaScript side. When a JavaScript object arrives into a native binding, `napi_check_object_type_tag()` can be used @@ -5253,15 +5253,15 @@ napi_status napi_type_tag_object(napi_env env, ``` * `[in] env`: The environment that the API is invoked under. -* `[in] js_object`: The JavaScript object to be marked. +* `[in] js_object`: The JavaScript object or [external][] to be marked. * `[in] type_tag`: The tag with which the object is to be marked. Returns `napi_ok` if the API succeeded. -Associates the value of the `type_tag` pointer with the JavaScript object. -`napi_check_object_type_tag()` can then be used to compare the tag that was -attached to the object with one owned by the addon to ensure that the object -has the right type. +Associates the value of the `type_tag` pointer with the JavaScript object or +[external][]. `napi_check_object_type_tag()` can then be used to compare the tag +that was attached to the object with one owned by the addon to ensure that the +object has the right type. If the object already has an associated type tag, this API will return `napi_invalid_arg`. @@ -5283,7 +5283,8 @@ napi_status napi_check_object_type_tag(napi_env env, ``` * `[in] env`: The environment that the API is invoked under. -* `[in] js_object`: The JavaScript object whose type tag to examine. +* `[in] js_object`: The JavaScript object or [external][] whose type tag to + examine. * `[in] type_tag`: The tag with which to compare any tag found on the object. * `[out] result`: Whether the type tag given matched the type tag on the object. `false` is also returned if no type tag was found on the object. @@ -6453,6 +6454,8 @@ the add-on's file name during loading. [async_hooks `type`]: async_hooks.md#type [context-aware addons]: addons.md#context-aware-addons [docs]: https://github.com/nodejs/node-addon-api#api-documentation +[external]: #napi_create_external +[externals]: #napi_create_external [global scope]: globals.md [gyp-next]: https://github.com/nodejs/gyp-next [module scope]: modules.md#the-module-scope diff --git a/test/js-native-api/test_object/test.js b/test/js-native-api/test_object/test.js index 012f737ba74b43..670ae200a35bec 100644 --- a/test/js-native-api/test_object/test.js +++ b/test/js-native-api/test_object/test.js @@ -165,12 +165,26 @@ assert.strictEqual(newObject.test_string, 'test string'); const obj2 = test_object.TypeTaggedInstance(1); const obj3 = test_object.TypeTaggedInstance(2); const obj4 = test_object.TypeTaggedInstance(3); + const external = test_object.TypeTaggedExternal(2); + const plainExternal = test_object.PlainExternal(); + + // Verify that we do not allow type tag indices greater than the largest + // available index. + assert.throws(() => test_object.TypeTaggedInstance(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + assert.throws(() => test_object.TypeTaggedExternal(39), { + name: 'RangeError', + message: 'Invalid type index', + }); // Verify that type tags are correctly accepted. assert.strictEqual(test_object.CheckTypeTag(0, obj1), true); assert.strictEqual(test_object.CheckTypeTag(1, obj2), true); assert.strictEqual(test_object.CheckTypeTag(2, obj3), true); assert.strictEqual(test_object.CheckTypeTag(3, obj4), true); + assert.strictEqual(test_object.CheckTypeTag(2, external), true); // Verify that wrongly tagged objects are rejected. assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); @@ -180,10 +194,19 @@ assert.strictEqual(newObject.test_string, 'test string'); assert.strictEqual(test_object.CheckTypeTag(2, obj4), false); assert.strictEqual(test_object.CheckTypeTag(3, obj3), false); assert.strictEqual(test_object.CheckTypeTag(4, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(0, external), false); + assert.strictEqual(test_object.CheckTypeTag(1, external), false); + assert.strictEqual(test_object.CheckTypeTag(3, external), false); + assert.strictEqual(test_object.CheckTypeTag(4, external), false); // Verify that untagged objects are rejected. assert.strictEqual(test_object.CheckTypeTag(0, {}), false); assert.strictEqual(test_object.CheckTypeTag(1, {}), false); + assert.strictEqual(test_object.CheckTypeTag(0, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(1, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(2, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(3, plainExternal), false); + assert.strictEqual(test_object.CheckTypeTag(4, plainExternal), false); } { diff --git a/test/js-native-api/test_object/test_object.c b/test/js-native-api/test_object/test_object.c index 3ae54a9b7cd65c..eb5aa2071e30a3 100644 --- a/test/js-native-api/test_object/test_object.c +++ b/test/js-native-api/test_object/test_object.c @@ -605,13 +605,23 @@ static napi_value TestSeal(napi_env env, } // We create two type tags. They are basically 128-bit UUIDs. -static const napi_type_tag type_tags[5] = { - { 0xdaf987b3cc62481a, 0xb745b0497f299531 }, - { 0xbb7936c374084d9b, 0xa9548d0762eeedb9 }, - { 0xa5ed9ce2e4c00c38, 0 }, - { 0, 0 }, - { 0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a }, +#define TYPE_TAG_COUNT 5 +static const napi_type_tag type_tags[TYPE_TAG_COUNT] = { + {0xdaf987b3cc62481a, 0xb745b0497f299531}, + {0xbb7936c374084d9b, 0xa9548d0762eeedb9}, + {0xa5ed9ce2e4c00c38, 0}, + {0, 0}, + {0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a}, }; +#define VALIDATE_TYPE_INDEX(env, type_index) \ + do { \ + if ((type_index) >= TYPE_TAG_COUNT) { \ + NODE_API_CALL((env), \ + napi_throw_range_error((env), \ + "NODE_API_TEST_INVALID_TYPE_INDEX", \ + "Invalid type index")); \ + } \ + } while (0) static napi_value TypeTaggedInstance(napi_env env, napi_callback_info info) { @@ -621,12 +631,42 @@ TypeTaggedInstance(napi_env env, napi_callback_info info) { NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); NODE_API_CALL(env, napi_create_object(env, &instance)); NODE_API_CALL(env, napi_type_tag_object(env, instance, &type_tags[type_index])); return instance; } +// V8 will not allowe us to construct an external with a NULL data value. +#define IN_LIEU_OF_NULL ((void*)0x1) + +static napi_value PlainExternal(napi_env env, napi_callback_info info) { + napi_value instance; + + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + + return instance; +} + +static napi_value TypeTaggedExternal(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NODE_API_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); + NODE_API_CALL( + env, napi_create_external(env, IN_LIEU_OF_NULL, NULL, NULL, &instance)); + NODE_API_CALL(env, + napi_type_tag_object(env, instance, &type_tags[type_index])); + + return instance; +} + static napi_value CheckTypeTag(napi_env env, napi_callback_info info) { size_t argc = 2; @@ -636,6 +676,7 @@ CheckTypeTag(napi_env env, napi_callback_info info) { NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); NODE_API_CALL(env, napi_get_value_uint32(env, argv[0], &type_index)); + VALIDATE_TYPE_INDEX(env, type_index); NODE_API_CALL(env, napi_check_object_type_tag(env, argv[1], &type_tags[type_index], @@ -648,31 +689,36 @@ CheckTypeTag(napi_env env, napi_callback_info info) { EXTERN_C_START napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor descriptors[] = { - DECLARE_NODE_API_PROPERTY("Get", Get), - DECLARE_NODE_API_PROPERTY("GetNamed", GetNamed), - DECLARE_NODE_API_PROPERTY("GetPropertyNames", GetPropertyNames), - DECLARE_NODE_API_PROPERTY("GetSymbolNames", GetSymbolNames), - DECLARE_NODE_API_PROPERTY("GetEnumerableWritableNames", GetEnumerableWritableNames), - DECLARE_NODE_API_PROPERTY("GetOwnWritableNames", GetOwnWritableNames), - DECLARE_NODE_API_PROPERTY("GetEnumerableConfigurableNames", GetEnumerableConfigurableNames), - DECLARE_NODE_API_PROPERTY("GetOwnConfigurableNames", GetOwnConfigurableNames), - DECLARE_NODE_API_PROPERTY("Set", Set), - DECLARE_NODE_API_PROPERTY("SetNamed", SetNamed), - DECLARE_NODE_API_PROPERTY("Has", Has), - DECLARE_NODE_API_PROPERTY("HasNamed", HasNamed), - DECLARE_NODE_API_PROPERTY("HasOwn", HasOwn), - DECLARE_NODE_API_PROPERTY("Delete", Delete), - DECLARE_NODE_API_PROPERTY("New", New), - DECLARE_NODE_API_PROPERTY("Inflate", Inflate), - DECLARE_NODE_API_PROPERTY("Wrap", Wrap), - DECLARE_NODE_API_PROPERTY("Unwrap", Unwrap), - DECLARE_NODE_API_PROPERTY("TestSetProperty", TestSetProperty), - DECLARE_NODE_API_PROPERTY("TestHasProperty", TestHasProperty), - DECLARE_NODE_API_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), - DECLARE_NODE_API_PROPERTY("CheckTypeTag", CheckTypeTag), - DECLARE_NODE_API_PROPERTY("TestGetProperty", TestGetProperty), - DECLARE_NODE_API_PROPERTY("TestFreeze", TestFreeze), - DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), + DECLARE_NODE_API_PROPERTY("Get", Get), + DECLARE_NODE_API_PROPERTY("GetNamed", GetNamed), + DECLARE_NODE_API_PROPERTY("GetPropertyNames", GetPropertyNames), + DECLARE_NODE_API_PROPERTY("GetSymbolNames", GetSymbolNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableWritableNames", + GetEnumerableWritableNames), + DECLARE_NODE_API_PROPERTY("GetOwnWritableNames", GetOwnWritableNames), + DECLARE_NODE_API_PROPERTY("GetEnumerableConfigurableNames", + GetEnumerableConfigurableNames), + DECLARE_NODE_API_PROPERTY("GetOwnConfigurableNames", + GetOwnConfigurableNames), + DECLARE_NODE_API_PROPERTY("Set", Set), + DECLARE_NODE_API_PROPERTY("SetNamed", SetNamed), + DECLARE_NODE_API_PROPERTY("Has", Has), + DECLARE_NODE_API_PROPERTY("HasNamed", HasNamed), + DECLARE_NODE_API_PROPERTY("HasOwn", HasOwn), + DECLARE_NODE_API_PROPERTY("Delete", Delete), + DECLARE_NODE_API_PROPERTY("New", New), + DECLARE_NODE_API_PROPERTY("Inflate", Inflate), + DECLARE_NODE_API_PROPERTY("Wrap", Wrap), + DECLARE_NODE_API_PROPERTY("Unwrap", Unwrap), + DECLARE_NODE_API_PROPERTY("TestSetProperty", TestSetProperty), + DECLARE_NODE_API_PROPERTY("TestHasProperty", TestHasProperty), + DECLARE_NODE_API_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), + DECLARE_NODE_API_PROPERTY("TypeTaggedExternal", TypeTaggedExternal), + DECLARE_NODE_API_PROPERTY("PlainExternal", PlainExternal), + DECLARE_NODE_API_PROPERTY("CheckTypeTag", CheckTypeTag), + DECLARE_NODE_API_PROPERTY("TestGetProperty", TestGetProperty), + DECLARE_NODE_API_PROPERTY("TestFreeze", TestFreeze), + DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), }; init_test_null(env, exports);