n-api: support type-tagging objects

`napi_instanceof()` is insufficient for reliably establishing the data
type to which a pointer stored with `napi_wrap()` or
`napi_create_external()` inside a JavaScript object points. Thus, we
need a way to "mark" an object with a value that, when later retrieved,
can unambiguously tell us whether it is safe to cast the pointer stored
inside it to a certain structure.

Such a check must survive loading/unloading/multiple instances of an
addon, so we use UUIDs chosen *a priori*.

Fixes: https://github.com/nodejs/node/issues/28164
Co-authored-by: Anna Henningsen <github@addaleax.net>
PR-URL: https://github.com/nodejs/node/pull/28237
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Signed-off-by: Gabriel Schulhof <gabriel.schulhof@intel.com>
This commit is contained in:
Gabriel Schulhof 2019-06-14 16:44:18 -07:00
parent 8b3ad75b03
commit cc7ec889e8
14 changed files with 545 additions and 0 deletions

View File

@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ '../type-tag/binding.c' ]
}
]
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.c' ]
}
]
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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

View File

@ -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") \

View File

@ -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

View File

@ -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_

View File

@ -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) {

View File

@ -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)

View File

@ -159,6 +159,24 @@ assert.strictEqual(newObject.test_string, 'test string');
assert(wrapper.protoB, true);
}
{
// 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();

View File

@ -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),
};