diff options
-rw-r--r-- | benchmark/napi/type-tag-check/binding.gyp | 8 | ||||
-rw-r--r-- | benchmark/napi/type-tag-check/index.js | 18 | ||||
-rw-r--r-- | benchmark/napi/type-tag/binding.c | 84 | ||||
-rw-r--r-- | benchmark/napi/type-tag/binding.gyp | 8 | ||||
-rw-r--r-- | benchmark/napi/type-tag/check-object-tag.js | 18 | ||||
-rw-r--r-- | benchmark/napi/type-tag/index.js | 18 | ||||
-rw-r--r-- | doc/api/n-api.md | 213 | ||||
-rw-r--r-- | src/env.h | 1 | ||||
-rw-r--r-- | src/js_native_api.h | 10 | ||||
-rw-r--r-- | src/js_native_api_types.h | 7 | ||||
-rw-r--r-- | src/js_native_api_v8.cc | 69 | ||||
-rw-r--r-- | src/js_native_api_v8.h | 32 | ||||
-rw-r--r-- | test/js-native-api/test_object/test.js | 18 | ||||
-rw-r--r-- | test/js-native-api/test_object/test_object.c | 41 |
14 files changed, 545 insertions, 0 deletions
diff --git a/benchmark/napi/type-tag-check/binding.gyp b/benchmark/napi/type-tag-check/binding.gyp new file mode 100644 index 00000000000..595ab325233 --- /dev/null +++ b/benchmark/napi/type-tag-check/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ '../type-tag/binding.c' ] + } + ] +} diff --git a/benchmark/napi/type-tag-check/index.js b/benchmark/napi/type-tag-check/index.js new file mode 100644 index 00000000000..346dfb7812d --- /dev/null +++ b/benchmark/napi/type-tag-check/index.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common.js'); + +let binding; +try { + binding = require(`./build/${common.buildType}/binding`); +} catch { + console.error(`${__filename}: Binding failed to load`); + process.exit(0); +} + +const bench = common.createBenchmark(main, { + n: [1e5, 1e6, 1e7], +}); + +function main({ n }) { + binding.checkObjectTag(n, bench, bench.start, bench.end); +} diff --git a/benchmark/napi/type-tag/binding.c b/benchmark/napi/type-tag/binding.c new file mode 100644 index 00000000000..7bc8b5d7502 --- /dev/null +++ b/benchmark/napi/type-tag/binding.c @@ -0,0 +1,84 @@ +#include <assert.h> +#define NAPI_EXPERIMENTAL +#include <node_api.h> + +#define NAPI_CALL(call) \ + do { \ + napi_status status = call; \ + assert(status == napi_ok && #call " failed"); \ + } while (0); + +#define EXPORT_FUNC(env, exports, name, func) \ + do { \ + napi_value js_func; \ + NAPI_CALL(napi_create_function((env), \ + (name), \ + NAPI_AUTO_LENGTH, \ + (func), \ + NULL, \ + &js_func)); \ + NAPI_CALL(napi_set_named_property((env), \ + (exports), \ + (name), \ + js_func)); \ + } while (0); + +static const napi_type_tag tag = { + 0xe7ecbcd5954842f6, 0x9e75161c9bf27282 +}; + +static napi_value TagObject(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value argv[4]; + uint32_t n; + uint32_t index; + napi_handle_scope scope; + + NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NAPI_CALL(napi_get_value_uint32(env, argv[0], &n)); + NAPI_CALL(napi_open_handle_scope(env, &scope)); + napi_value objects[n]; + for (index = 0; index < n; index++) { + NAPI_CALL(napi_create_object(env, &objects[index])); + } + + // Time the object tag creation. + NAPI_CALL(napi_call_function(env, argv[1], argv[2], 0, NULL, NULL)); + for (index = 0; index < n; index++) { + NAPI_CALL(napi_type_tag_object(env, objects[index], &tag)); + } + NAPI_CALL(napi_call_function(env, argv[1], argv[3], 1, &argv[0], NULL)); + + NAPI_CALL(napi_close_handle_scope(env, scope)); + return NULL; +} + +static napi_value CheckObjectTag(napi_env env, napi_callback_info info) { + size_t argc = 4; + napi_value argv[4]; + uint32_t n; + uint32_t index; + bool is_of_type; + + NAPI_CALL(napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NAPI_CALL(napi_get_value_uint32(env, argv[0], &n)); + napi_value object; + NAPI_CALL(napi_create_object(env, &object)); + NAPI_CALL(napi_type_tag_object(env, object, &tag)); + + // Time the object tag checking. + NAPI_CALL(napi_call_function(env, argv[1], argv[2], 0, NULL, NULL)); + for (index = 0; index < n; index++) { + NAPI_CALL(napi_check_object_type_tag(env, object, &tag, &is_of_type)); + assert(is_of_type && " type mismatch"); + } + NAPI_CALL(napi_call_function(env, argv[1], argv[3], 1, &argv[0], NULL)); + + return NULL; +} + +NAPI_MODULE_INIT() { + EXPORT_FUNC(env, exports, "tagObject", TagObject); + EXPORT_FUNC(env, exports, "checkObjectTag", CheckObjectTag); + return exports; +} diff --git a/benchmark/napi/type-tag/binding.gyp b/benchmark/napi/type-tag/binding.gyp new file mode 100644 index 00000000000..413621ade33 --- /dev/null +++ b/benchmark/napi/type-tag/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'binding', + 'sources': [ 'binding.c' ] + } + ] +} diff --git a/benchmark/napi/type-tag/check-object-tag.js b/benchmark/napi/type-tag/check-object-tag.js new file mode 100644 index 00000000000..346dfb7812d --- /dev/null +++ b/benchmark/napi/type-tag/check-object-tag.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common.js'); + +let binding; +try { + binding = require(`./build/${common.buildType}/binding`); +} catch { + console.error(`${__filename}: Binding failed to load`); + process.exit(0); +} + +const bench = common.createBenchmark(main, { + n: [1e5, 1e6, 1e7], +}); + +function main({ n }) { + binding.checkObjectTag(n, bench, bench.start, bench.end); +} diff --git a/benchmark/napi/type-tag/index.js b/benchmark/napi/type-tag/index.js new file mode 100644 index 00000000000..3f85b9af8e7 --- /dev/null +++ b/benchmark/napi/type-tag/index.js @@ -0,0 +1,18 @@ +'use strict'; +const common = require('../../common.js'); + +let binding; +try { + binding = require(`./build/${common.buildType}/binding`); +} catch { + console.error(`${__filename}: Binding failed to load`); + process.exit(0); +} + +const bench = common.createBenchmark(main, { + n: [1e3, 1e4, 1e5], +}); + +function main({ n }) { + binding.tagObject(n, bench, bench.start, bench.end); +} diff --git a/doc/api/n-api.md b/doc/api/n-api.md index f45dd2dd4c6..8dff1e0e15f 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -602,6 +602,27 @@ minimum lifetimes explicitly. For more details, review the [Object lifetime management][]. +#### napi_type_tag +<!-- YAML +added: REPLACEME +--> + +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. + +```c +typedef struct { + uint64_t lower; + uint64_t upper; +} napi_type_tag; +``` + ### N-API callback types #### napi_callback_info @@ -4288,6 +4309,143 @@ if (is_instance) { The reference must be freed once it is no longer needed. +There are occasions where `napi_instanceof()` is insufficient for ensuring that +a JavaScript object is a wrapper for a certain native type. This is the case +especially when wrapped JavaScript objects are passed back into the addon via +static methods rather than as the `this` value of prototype methods. In such +cases there is a chance that they may be unwrapped incorrectly. + +```js +const myAddon = require('./build/Release/my_addon.node'); + +// `openDatabase()` returns a JavaScript object that wraps a native database +// handle. +const dbHandle = myAddon.openDatabase(); + +// `query()` returns a JavaScript object that wraps a native query handle. +const queryHandle = myAddon.query(dbHandle, 'Gimme ALL the things!'); + +// There is an accidental error in the line below. The first parameter to +// `myAddon.queryHasRecords()` should be the database handle (`dbHandle`), not +// the query handle (`query`), so the correct condition for the while-loop +// should be +// +// myAddon.queryHasRecords(dbHandle, queryHandle) +// +while (myAddon.queryHasRecords(queryHandle, dbHandle)) { + // retrieve records +} +``` + +In the above example `myAddon.queryHasRecords()` is a method that accepts two +arguments. The first is a database handle and the second is a query handle. +Internally, it unwraps the first argument and casts the resulting pointer to a +native database handle. It then unwraps the second argument and casts the +resulting pointer to a query handle. If the arguments are passed in the wrong +order, the casts will work, however, there is a good chance that the underlying +database operation will fail, or will even cause an invalid memory access. + +To ensure that the pointer retrieved from the first argument is indeed a pointer +to a database handle and, similarly, that the pointer retrieved from the second +argument is indeed a pointer to a query handle, the implementation of +`queryHasRecords()` has to perform a type validation. Retaining the JavaScript +class constructor from which the database handle was instantiated and the +constructor from which the query handle was instantiated in `napi_ref`s can +help, because `napi_instanceof()` can then be used to ensure that the instances +passed into `queryHashRecords()` are indeed of the correct type. + +Unfortunately, `napi_instanceof()` does not protect against prototype +manipulation. For example, the prototype of the database handle instance can be +set to the prototype of the constructor for query handle instances. In this +case, the database handle instance can appear as a query handle instance, and it +will pass the `napi_instanceof()` test for a query handle instance, while still +containing a pointer to a database handle. + +To this end, N-API provides type-tagging capabilities. + +A type tag is a 128-bit integer unique to the addon. N-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 +`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 +along with the original type tag to determine whether the JavaScript object was +previously "marked" with the type tag. This creates a type-checking capability +of a higher fidelity than `napi_instanceof()` can provide, because such type- +tagging survives prototype manipulation and addon unloading/reloading. + +Continuing the above example, the following skeleton addon implementation +illustrates the use of `napi_type_tag_object()` and +`napi_check_object_type_tag()`. + +```c +// This value is the type tag for a database handle. The command +// +// uuidgen | sed -r -e 's/-//g' -e 's/(.{16})(.*)/0x\1, 0x\2/' +// +// can be used to obtain the two values with which to initialize the structure. +static const napi_type_tag DatabaseHandleTypeTag = { + 0x1edf75a38336451d, 0xa5ed9ce2e4c00c38 +}; + +// This value is the type tag for a query handle. +static const napi_type_tag QueryHandleTypeTag = { + 0x9c73317f9fad44a3, 0x93c3920bf3b0ad6a +}; + +static napi_value +openDatabase(napi_env env, napi_callback_info info) { + napi_status status; + napi_value result; + + // Perform the underlying action which results in a database handle. + DatabaseHandle* dbHandle = open_database(); + + // Create a new, empty JS object. + status = napi_create_object(env, &result); + if (status != napi_ok) return NULL; + + // Tag the object to indicate that it holds a pointer to a `DatabaseHandle`. + status = napi_type_tag_object(env, result, &DatabaseHandleTypeTag); + if (status != napi_ok) return NULL; + + // Store the pointer to the `DatabaseHandle` structure inside the JS object. + status = napi_wrap(env, result, dbHandle, NULL, NULL, NULL); + if (status != napi_ok) return NULL; + + return result; +} + +// Later when we receive a JavaScript object purporting to be a database handle +// we can use `napi_check_object_type_tag()` to ensure that it is indeed such a +// handle. + +static napi_value +query(napi_env env, napi_callback_info info) { + napi_status status; + size_t argc = 2; + napi_value argv[2]; + bool is_db_handle; + + status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + if (status != napi_ok) return NULL; + + // Check that the object passed as the first parameter has the previously + // applied tag. + status = napi_check_object_type_tag(env, + argv[0], + &DatabaseHandleTypeTag, + &is_db_handle); + if (status != napi_ok) return NULL; + + // Throw a `TypeError` if it doesn't. + if (!is_db_handle) { + // Throw a TypeError. + return NULL; + } +} +``` + ### napi_define_class <!-- YAML added: v8.0.0 @@ -4461,6 +4619,60 @@ object `js_object` using `napi_wrap()` and removes the wrapping. If a finalize callback was associated with the wrapping, it will no longer be called when the JavaScript object becomes garbage-collected. +### napi_type_tag_object +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +```c +napi_status napi_type_tag_object(napi_env env, + napi_value js_object, + const napi_type_tag* type_tag); +``` + +* `[in] env`: The environment that the API is invoked under. +* `[in] js_object`: The JavaScript object 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. + +If the object already has an associated type tag, this API will return +`napi_invalid_arg`. + +### napi_check_object_type_tag +<!-- YAML +added: REPLACEME +--> + +> Stability: 1 - Experimental + +```c +napi_status napi_check_object_type_tag(napi_env env, + napi_value js_object, + const napi_type_tag* type_tag, + bool* result); +``` + +* `[in] env`: The environment that the API is invoked under. +* `[in] js_object`: The JavaScript object 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. + +Returns `napi_ok` if the API succeeded. + +Compares the pointer given as `type_tag` with any that can be found on +`js_object`. If no tag is found on `js_object` or, if a tag is found but it does +not match `type_tag`, then `result` is set to `false`. If a tag is found and it +matches `type_tag`, then `result` is set to `true`. + ### napi_add_finalizer <!-- YAML @@ -5523,6 +5735,7 @@ This API may only be called from the main thread. [`napi_get_reference_value`]: #n_api_napi_get_reference_value [`napi_get_value_external`]: #n_api_napi_get_value_external [`napi_has_property`]: #n_api_napi_has_property +[`napi_instanceof`]: #n_api_napi_instanceof [`napi_is_error`]: #n_api_napi_is_error [`napi_is_exception_pending`]: #n_api_napi_is_exception_pending [`napi_make_callback`]: #n_api_napi_make_callback diff --git a/src/env.h b/src/env.h index d475fc78fb9..d99bb63db6f 100644 --- a/src/env.h +++ b/src/env.h @@ -152,6 +152,7 @@ constexpr size_t kFsStatsBufferLength = V(contextify_context_private_symbol, "node:contextify:context") \ V(contextify_global_private_symbol, "node:contextify:global") \ V(decorated_private_symbol, "node:decorated") \ + V(napi_type_tag, "node:napi:type_tag") \ V(napi_wrapper, "node:napi:wrapper") \ V(untransferable_object_private_symbol, "node:untransferableObject") \ diff --git a/src/js_native_api.h b/src/js_native_api.h index 00dedfbfc71..d0d975c2b45 100644 --- a/src/js_native_api.h +++ b/src/js_native_api.h @@ -537,6 +537,16 @@ NAPI_EXTERN napi_status napi_detach_arraybuffer(napi_env env, NAPI_EXTERN napi_status napi_is_detached_arraybuffer(napi_env env, napi_value value, bool* result); +// Type tagging +NAPI_EXTERN napi_status napi_type_tag_object(napi_env env, + napi_value value, + const napi_type_tag* type_tag); + +NAPI_EXTERN napi_status +napi_check_object_type_tag(napi_env env, + napi_value value, + const napi_type_tag* type_tag, + bool* result); #endif // NAPI_EXPERIMENTAL EXTERN_C_END diff --git a/src/js_native_api_types.h b/src/js_native_api_types.h index 7254019dd78..115ccebf261 100644 --- a/src/js_native_api_types.h +++ b/src/js_native_api_types.h @@ -140,4 +140,11 @@ typedef enum { } napi_key_conversion; #endif // NAPI_VERSION >= 6 +#ifdef NAPI_EXPERIMENTAL +typedef struct { + uint64_t lower; + uint64_t upper; +} napi_type_tag; +#endif // NAPI_EXPERIMENTAL + #endif // SRC_JS_NATIVE_API_TYPES_H_ diff --git a/src/js_native_api_v8.cc b/src/js_native_api_v8.cc index 37cd1a1ab22..b8455eb3a69 100644 --- a/src/js_native_api_v8.cc +++ b/src/js_native_api_v8.cc @@ -10,6 +10,9 @@ #define CHECK_MAYBE_NOTHING(env, maybe, status) \ RETURN_STATUS_IF_FALSE((env), !((maybe).IsNothing()), (status)) +#define CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe, status) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsNothing()), (status)) + #define CHECK_TO_NUMBER(env, context, result, src) \ CHECK_TO_TYPE((env), Number, (context), (result), (src), napi_number_expected) @@ -2356,6 +2359,72 @@ napi_status napi_create_external(napi_env env, return napi_clear_last_error(env); } +NAPI_EXTERN napi_status napi_type_tag_object(napi_env env, + napi_value object, + const napi_type_tag* type_tag) { + NAPI_PREAMBLE(env); + v8::Local<v8::Context> context = env->context(); + v8::Local<v8::Object> obj; + CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object); + CHECK_ARG_WITH_PREAMBLE(env, type_tag); + + auto key = NAPI_PRIVATE_KEY(context, type_tag); + auto maybe_has = obj->HasPrivate(context, key); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_has, napi_generic_failure); + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(env, + !maybe_has.FromJust(), + napi_invalid_arg); + + auto tag = v8::BigInt::NewFromWords(context, + 0, + 2, + reinterpret_cast<const uint64_t*>(type_tag)); + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, tag, napi_generic_failure); + + auto maybe_set = obj->SetPrivate(context, key, tag.ToLocalChecked()); + CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_set, napi_generic_failure); + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(env, + maybe_set.FromJust(), + napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + +NAPI_EXTERN napi_status +napi_check_object_type_tag(napi_env env, + napi_value object, + const napi_type_tag* type_tag, + bool* result) { + NAPI_PREAMBLE(env); + v8::Local<v8::Context> context = env->context(); + v8::Local<v8::Object> obj; + CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object); + CHECK_ARG_WITH_PREAMBLE(env, type_tag); + CHECK_ARG_WITH_PREAMBLE(env, result); + + auto maybe_value = obj->GetPrivate(context, + NAPI_PRIVATE_KEY(context, type_tag)); + CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe_value, napi_generic_failure); + v8::Local<v8::Value> val = maybe_value.ToLocalChecked(); + + // We consider the type check to have failed unless we reach the line below + // where we set whether the type check succeeded or not based on the + // comparison of the two type tags. + *result = false; + if (val->IsBigInt()) { + int sign; + int size = 2; + napi_type_tag tag; + val.As<v8::BigInt>()->ToWordsArray(&sign, + &size, + reinterpret_cast<uint64_t*>(&tag)); + if (size == 2 && sign == 0) + *result = (tag.lower == type_tag->lower && tag.upper == type_tag->upper); + } + + return GET_RETURN_STATUS(env); +} + napi_status napi_get_value_external(napi_env env, napi_value value, void** result) { diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index 83e6a0bd02e..06b8049ec46 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -148,6 +148,14 @@ napi_status napi_set_last_error(napi_env env, napi_status error_code, } \ } while (0) +#define RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(env, condition, status) \ + do { \ + if (!(condition)) { \ + return napi_set_last_error( \ + (env), try_catch.HasCaught() ? napi_pending_exception : (status)); \ + } \ + } while (0) + #define CHECK_ENV(env) \ do { \ if ((env) == nullptr) { \ @@ -158,9 +166,17 @@ napi_status napi_set_last_error(napi_env env, napi_status error_code, #define CHECK_ARG(env, arg) \ RETURN_STATUS_IF_FALSE((env), ((arg) != nullptr), napi_invalid_arg) +#define CHECK_ARG_WITH_PREAMBLE(env, arg) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), \ + ((arg) != nullptr), \ + napi_invalid_arg) + #define CHECK_MAYBE_EMPTY(env, maybe, status) \ RETURN_STATUS_IF_FALSE((env), !((maybe).IsEmpty()), (status)) +#define CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe, status) \ + RETURN_STATUS_IF_FALSE_WITH_PREAMBLE((env), !((maybe).IsEmpty()), (status)) + // NAPI_PREAMBLE is not wrapped in do..while: try_catch must have function scope #define NAPI_PREAMBLE(env) \ CHECK_ENV((env)); \ @@ -178,6 +194,14 @@ napi_status napi_set_last_error(napi_env env, napi_status error_code, (result) = maybe.ToLocalChecked(); \ } while (0) +#define CHECK_TO_TYPE_WITH_PREAMBLE(env, type, context, result, src, status) \ + do { \ + CHECK_ARG_WITH_PREAMBLE((env), (src)); \ + auto maybe = v8impl::V8LocalValueFromJsValue((src))->To##type((context)); \ + CHECK_MAYBE_EMPTY_WITH_PREAMBLE((env), maybe, (status)); \ + (result) = maybe.ToLocalChecked(); \ + } while (0) + #define CHECK_TO_FUNCTION(env, result, src) \ do { \ CHECK_ARG((env), (src)); \ @@ -189,6 +213,14 @@ napi_status napi_set_last_error(napi_env env, napi_status error_code, #define CHECK_TO_OBJECT(env, context, result, src) \ CHECK_TO_TYPE((env), Object, (context), (result), (src), napi_object_expected) +#define CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, result, src) \ + CHECK_TO_TYPE_WITH_PREAMBLE((env), \ + Object, \ + (context), \ + (result), \ + (src), \ + napi_object_expected) + #define CHECK_TO_STRING(env, context, result, src) \ CHECK_TO_TYPE((env), String, (context), (result), (src), napi_string_expected) diff --git a/test/js-native-api/test_object/test.js b/test/js-native-api/test_object/test.js index 2cd65af6b34..b7866699527 100644 --- a/test/js-native-api/test_object/test.js +++ b/test/js-native-api/test_object/test.js @@ -160,6 +160,24 @@ assert.strictEqual(newObject.test_string, 'test string'); } { + // Verify that objects can be type-tagged and type-tag-checked. + const obj1 = test_object.TypeTaggedInstance(0); + const obj2 = test_object.TypeTaggedInstance(1); + + // Verify that type tags are correctly accepted. + assert.strictEqual(test_object.CheckTypeTag(0, obj1), true); + assert.strictEqual(test_object.CheckTypeTag(1, obj2), true); + + // Verify that wrongly tagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj1), false); + + // Verify that untagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, {}), false); + assert.strictEqual(test_object.CheckTypeTag(1, {}), false); +} + +{ // Verify that normal and nonexistent properties can be deleted. const sym = Symbol(); const obj = { foo: 'bar', [sym]: 'baz' }; diff --git a/test/js-native-api/test_object/test_object.c b/test/js-native-api/test_object/test_object.c index 08f619bf7ff..f2ea89d6c60 100644 --- a/test/js-native-api/test_object/test_object.c +++ b/test/js-native-api/test_object/test_object.c @@ -1,3 +1,4 @@ +#define NAPI_EXPERIMENTAL #include <js_native_api.h> #include "../common.h" #include <string.h> @@ -471,6 +472,44 @@ static napi_value TestGetProperty(napi_env env, return object; } +// We create two type tags. They are basically 128-bit UUIDs. +static const napi_type_tag type_tags[2] = { + { 0xdaf987b3cc62481a, 0xb745b0497f299531 }, + { 0xbb7936c374084d9b, 0xa9548d0762eeedb9 } +}; + +static napi_value +TypeTaggedInstance(napi_env env, napi_callback_info info) { + size_t argc = 1; + uint32_t type_index; + napi_value instance, which_type; + + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &which_type, NULL, NULL)); + NAPI_CALL(env, napi_get_value_uint32(env, which_type, &type_index)); + NAPI_CALL(env, napi_create_object(env, &instance)); + NAPI_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; + bool result; + napi_value argv[2], js_result; + uint32_t type_index; + + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NAPI_CALL(env, napi_get_value_uint32(env, argv[0], &type_index)); + NAPI_CALL(env, napi_check_object_type_tag(env, + argv[1], + &type_tags[type_index], + &result)); + NAPI_CALL(env, napi_get_boolean(env, result, &js_result)); + + return js_result; +} + EXTERN_C_START napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor descriptors[] = { @@ -490,6 +529,8 @@ napi_value Init(napi_env env, napi_value exports) { DECLARE_NAPI_PROPERTY("Unwrap", Unwrap), DECLARE_NAPI_PROPERTY("TestSetProperty", TestSetProperty), DECLARE_NAPI_PROPERTY("TestHasProperty", TestHasProperty), + DECLARE_NAPI_PROPERTY("TypeTaggedInstance", TypeTaggedInstance), + DECLARE_NAPI_PROPERTY("CheckTypeTag", CheckTypeTag), DECLARE_NAPI_PROPERTY("TestGetProperty", TestGetProperty), }; |