Skip to content
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

src: add Object::TypeTag, Object::CheckTypeTag #1261

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions doc/external.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The `Napi::External` template class implements the ability to create a `Napi::Va

`Napi::External` objects can be created with an optional Finalizer function and optional Hint value. The Finalizer function, if specified, is called when your `Napi::External` object is released by Node's garbage collector. It gives your code the opportunity to free any dynamically created data. If you specify a Hint value, it is passed to your Finalizer function.

Note that `Napi::Value::IsExternal()` will return `true` for any external value.
It does not differentiate between the templated parameter `T` in
`Napi::External<T>`. It is up to the addon to ensure an `Napi::External<T>`
object holds the correct `T` when retrieving the data via
`Napi::External<T>::Data()`. One method to ensure an object is of a specific
type is through [type tags](./object.md#TypeTag).

## Methods

### New
Expand Down
27 changes: 27 additions & 0 deletions doc/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,33 @@ from being added to it and marking all existing properties as non-configurable.
Values of present properties can still be changed as long as they are
writable.

### TypeTag()

```cpp
void Napi::Object::TypeTag(const napi_type_tag* type_tag) const;
```

- `[in] type_tag`: The tag with which this object is to be marked.

The `Napi::Object::TypeTag()` method associates the value of the `type_tag`
pointer with this JavaScript object. `Napi::Object::CheckTypeTag()` can then be
used to compare the tag that was attached to this object with one owned by the
addon to ensure that this object has the right type.

### CheckTypeTag()

```cpp
bool Napi::Object::CheckTypeTag(const napi_type_tag* type_tag) const;
```

- `[in] type_tag`: The tag with which to compare any tag found on this object.

The `Napi::Object::CheckTypeTag()` method compares the pointer given as
`type_tag` with any that can be found on this JavaScript object. If no tag is
found on this object or, if a tag is found but it does not match `type_tag`,
then the return value is `false`. If a tag is found and it matches `type_tag`,
then the return value is `true`.

### operator\[\]()

```cpp
Expand Down
13 changes: 13 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,19 @@ inline MaybeOrValue<bool> Object::Seal() const {
napi_status status = napi_object_seal(_env, _value);
NAPI_RETURN_OR_THROW_IF_FAILED(_env, status, status == napi_ok, bool);
}

inline void Object::TypeTag(const napi_type_tag* type_tag) const {
napi_status status = napi_type_tag_object(_env, _value, type_tag);
NAPI_THROW_IF_FAILED_VOID(_env, status);
}

inline bool Object::CheckTypeTag(const napi_type_tag* type_tag) const {
bool result;
napi_status status =
napi_check_object_type_tag(_env, _value, type_tag, &result);
NAPI_THROW_IF_FAILED(_env, status, false);
return result;
}
#endif // NAPI_VERSION >= 8

////////////////////////////////////////////////////////////////////////////////
Expand Down
3 changes: 3 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,9 @@ class Object : public Value {
/// See
/// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-getprototypeof
MaybeOrValue<bool> Seal() const;

void TypeTag(const napi_type_tag* type_tag) const;
bool CheckTypeTag(const napi_type_tag* type_tag) const;
#endif // NAPI_VERSION >= 8
};

Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Object InitVersionManagement(Env env);
Object InitThunkingManual(Env env);
#if (NAPI_VERSION > 7)
Object InitObjectFreezeSeal(Env env);
Object InitObjectTypeTag(Env env);
#endif

#if defined(NODE_ADDON_API_ENABLE_MAYBE)
Expand Down Expand Up @@ -164,6 +165,7 @@ Object Init(Env env, Object exports) {
exports.Set("thunking_manual", InitThunkingManual(env));
#if (NAPI_VERSION > 7)
exports.Set("object_freeze_seal", InitObjectFreezeSeal(env));
exports.Set("object_type_tag", InitObjectTypeTag(env));
#endif

#if defined(NODE_ADDON_API_ENABLE_MAYBE)
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'object/has_property.cc',
'object/object.cc',
'object/object_freeze_seal.cc',
'object/object_type_tag.cc',
'object/set_property.cc',
'object/subscript_operator.cc',
'promise.cc',
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ if (majorNodeVersion < 12 && !filterConditionsProvided) {

if (napiVersion < 8 && !filterConditionsProvided) {
testModules.splice(testModules.indexOf('object/object_freeze_seal'), 1);
testModules.splice(testModules.indexOf('object/object_type_tag'), 1);
}

(async function () {
Expand Down
39 changes: 39 additions & 0 deletions test/object/object_type_tag.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include "napi.h"

#if (NAPI_VERSION > 7)

using namespace Napi;

static const napi_type_tag type_tags[5] = {
{0xdaf987b3cc62481a, 0xb745b0497f299531},
{0xbb7936c374084d9b, 0xa9548d0762eeedb9},
{0xa5ed9ce2e4c00c38, 0},
{0, 0},
{0xa5ed9ce2e4c00c38, 0xdaf987b3cc62481a},
};

Value TypeTaggedInstance(const CallbackInfo& info) {
Object instance = Object::New(info.Env());
uint32_t type_index = info[0].As<Number>().Int32Value();

instance.TypeTag(&type_tags[type_index]);

return instance;
}

Value CheckTypeTag(const CallbackInfo& info) {
uint32_t type_index = info[0].As<Number>().Int32Value();
Object instance = info[1].As<Object>();

return Boolean::New(info.Env(),
instance.CheckTypeTag(&type_tags[type_index]));
}

Object InitObjectTypeTag(Env env) {
Object exports = Object::New(env);
exports["checkTypeTag"] = Function::New(env, CheckTypeTag);
exports["typedTaggedInstance"] = Function::New(env, TypeTaggedInstance);
return exports;
}

#endif
55 changes: 55 additions & 0 deletions test/object/object_type_tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const assert = require('assert');

module.exports = require('../common').runTest(test);

// eslint-disable-next-line camelcase
function test ({ object_type_tag }) {
const obj1 = object_type_tag.typedTaggedInstance(0);
const obj2 = object_type_tag.typedTaggedInstance(1);

// Verify that type tags are correctly accepted.
assert.strictEqual(object_type_tag.checkTypeTag(0, obj1), true);
assert.strictEqual(object_type_tag.checkTypeTag(1, obj2), true);

// Verify that wrongly tagged objects are rejected.
assert.strictEqual(object_type_tag.checkTypeTag(0, obj2), false);
assert.strictEqual(object_type_tag.checkTypeTag(1, obj1), false);

// Verify that untagged objects are rejected.
assert.strictEqual(object_type_tag.checkTypeTag(0, {}), false);
assert.strictEqual(object_type_tag.checkTypeTag(1, {}), false);

// Node v14 and v16 have an issue checking type tags if the `upper` in
// `napi_type_tag` is 0, so these tests can only be performed on Node version
// >=18. See:
// - https://github.com/nodejs/node/issues/43786
// - https://github.com/nodejs/node/pull/43788
const nodeVersion = parseInt(process.versions.node.split('.')[0]);
if (nodeVersion < 18) {
return;
}

const obj3 = object_type_tag.typedTaggedInstance(2);
const obj4 = object_type_tag.typedTaggedInstance(3);

// Verify that untagged objects are rejected.
assert.strictEqual(object_type_tag.checkTypeTag(0, {}), false);
assert.strictEqual(object_type_tag.checkTypeTag(1, {}), false);

// Verify that type tags are correctly accepted.
assert.strictEqual(object_type_tag.checkTypeTag(0, obj1), true);
assert.strictEqual(object_type_tag.checkTypeTag(1, obj2), true);
assert.strictEqual(object_type_tag.checkTypeTag(2, obj3), true);
assert.strictEqual(object_type_tag.checkTypeTag(3, obj4), true);

// Verify that wrongly tagged objects are rejected.
assert.strictEqual(object_type_tag.checkTypeTag(0, obj2), false);
assert.strictEqual(object_type_tag.checkTypeTag(1, obj1), false);
assert.strictEqual(object_type_tag.checkTypeTag(0, obj3), false);
assert.strictEqual(object_type_tag.checkTypeTag(1, obj4), false);
assert.strictEqual(object_type_tag.checkTypeTag(2, obj4), false);
assert.strictEqual(object_type_tag.checkTypeTag(3, obj3), false);
assert.strictEqual(object_type_tag.checkTypeTag(4, obj3), false);
}