diff --git a/PORTING.md b/PORTING.md index 4607b87..ed4f21d 100644 --- a/PORTING.md +++ b/PORTING.md @@ -63,7 +63,7 @@ Tests covering the engine-specific part of Node-API, defined in `js_native_api.h | `test_instance_data` | Not ported | Medium | | `test_new_target` | Ported ✅ | Easy | | `test_number` | Ported ✅ | Easy | -| `test_object` | Not ported | Hard | +| `test_object` | Partial | Hard | | `test_promise` | Ported ✅ | Easy | | `test_properties` | Ported ✅ | Easy | | `test_reference` | Ported ✅ | Medium | diff --git a/implementors/node/features.js b/implementors/node/features.js index fec2137..627e182 100644 --- a/implementors/node/features.js +++ b/implementors/node/features.js @@ -10,7 +10,11 @@ globalThis.experimentalFeatures = { // added in Node.js v24.9.0. Earlier versions do not export these symbols, // causing addons that reference them to fail at dlopen time. sharedArrayBuffer: major >= 25 || (major === 24 && minor >= 9), - createObjectWithProperties: true, + // node_api_create_object_with_properties was added in Node.js v25.2.0 and + // v24.12.0, and not backported to v20.x or v22.x. Earlier versions do not + // export the symbol, so an addon referencing it fails at dlopen time. + createObjectWithProperties: + major > 25 || (major === 25 && minor >= 2) || (major === 24 && minor >= 12), setPrototype: true, postFinalizer: true, }; diff --git a/tests/js-native-api/test_object/CMakeLists.txt b/tests/js-native-api/test_object/CMakeLists.txt new file mode 100644 index 0000000..ed2c27c --- /dev/null +++ b/tests/js-native-api/test_object/CMakeLists.txt @@ -0,0 +1,5 @@ +add_node_api_cts_addon(test_object test_object.c) + +# node_api_create_object_with_properties is gated behind NAPI_EXPERIMENTAL, so +# it builds as a separate addon that only runtimes exporting it need to load. +add_node_api_cts_experimental_addon(test_object_create_with_properties test_object_create_with_properties.c) diff --git a/tests/js-native-api/test_object/test.js b/tests/js-native-api/test_object/test.js new file mode 100644 index 0000000..5ae3627 --- /dev/null +++ b/tests/js-native-api/test_object/test.js @@ -0,0 +1,396 @@ +const test_object = loadAddon('test_object'); + +const object = { + hello: 'world', + array: [1, 94, 'str', 12.321, { test: 'obj in arr' }], + newObject: { + test: 'obj in obj', + }, +}; + +assert.strictEqual(test_object.Get(object, 'hello'), 'world'); +assert.strictEqual(test_object.GetNamed(object, 'hello'), 'world'); +assert.deepStrictEqual(test_object.Get(object, 'array'), [ + 1, 94, 'str', 12.321, { test: 'obj in arr' }, +]); +assert.deepStrictEqual(test_object.Get(object, 'newObject'), { + test: 'obj in obj', +}); + +assert.ok(test_object.Has(object, 'hello')); +assert.ok(test_object.HasNamed(object, 'hello')); +assert.ok(test_object.Has(object, 'array')); +assert.ok(test_object.Has(object, 'newObject')); + +const newObject = test_object.New(); +assert.ok(test_object.Has(newObject, 'test_number')); +assert.strictEqual(newObject.test_number, 987654321); +assert.strictEqual(newObject.test_string, 'test string'); + +{ + // napi_get_property walks the prototype chain. + function MyObject() { + this.foo = 42; + this.bar = 43; + } + + MyObject.prototype.bar = 44; + MyObject.prototype.baz = 45; + + const obj = new MyObject(); + + assert.strictEqual(test_object.Get(obj, 'foo'), 42); + assert.strictEqual(test_object.Get(obj, 'bar'), 43); + assert.strictEqual(test_object.Get(obj, 'baz'), 45); + assert.strictEqual( + test_object.Get(obj, 'toString'), + Object.prototype.toString, + ); +} + +{ + // napi_has_own_property fails if the key is not a name. + [true, false, null, undefined, {}, [], 0, 1, () => {}].forEach((value) => { + assert.throws( + () => test_object.HasOwn({}, value), + /^Error: A string or symbol was expected$/, + ); + }); +} + +{ + // napi_has_own_property does not walk the prototype chain. + const symbol1 = Symbol(); + const symbol2 = Symbol(); + + function MyObject() { + this.foo = 42; + this.bar = 43; + this[symbol1] = 44; + } + + MyObject.prototype.bar = 45; + MyObject.prototype.baz = 46; + MyObject.prototype[symbol2] = 47; + + const obj = new MyObject(); + + assert.strictEqual(test_object.HasOwn(obj, 'foo'), true); + assert.strictEqual(test_object.HasOwn(obj, 'bar'), true); + assert.strictEqual(test_object.HasOwn(obj, symbol1), true); + assert.strictEqual(test_object.HasOwn(obj, 'baz'), false); + assert.strictEqual(test_object.HasOwn(obj, 'toString'), false); + assert.strictEqual(test_object.HasOwn(obj, symbol2), false); +} + +{ + // Inflate increases every own enumerable property by 1. + const cube = { + x: 10, + y: 10, + z: 10, + }; + + assert.deepStrictEqual(test_object.Inflate(cube), { x: 11, y: 11, z: 11 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 12, y: 12, z: 12 }); + assert.deepStrictEqual(test_object.Inflate(cube), { x: 13, y: 13, z: 13 }); + cube.t = 13; + assert.deepStrictEqual(test_object.Inflate(cube), { + x: 14, y: 14, z: 14, t: 14, + }); + + const sym1 = Symbol('1'); + const sym2 = Symbol('2'); + const sym3 = Symbol('3'); + const sym4 = Symbol('4'); + const object2 = { + [sym1]: '@@iterator', + [sym2]: sym3, + }; + + assert.ok(test_object.Has(object2, sym1)); + assert.ok(test_object.Has(object2, sym2)); + assert.strictEqual(test_object.Get(object2, sym1), '@@iterator'); + assert.strictEqual(test_object.Get(object2, sym2), sym3); + assert.ok(test_object.Set(object2, 'string', 'value')); + assert.ok(test_object.SetNamed(object2, 'named_string', 'value')); + assert.ok(test_object.Set(object2, sym4, 123)); + assert.ok(test_object.Has(object2, 'string')); + assert.ok(test_object.HasNamed(object2, 'named_string')); + assert.ok(test_object.Has(object2, sym4)); + assert.strictEqual(test_object.Get(object2, 'string'), 'value'); + assert.strictEqual(test_object.Get(object2, sym4), 123); +} + +{ + // Wrap a pointer in a JS object, then verify the pointer can be unwrapped. + const wrapper = {}; + test_object.Wrap(wrapper); + + assert.ok(test_object.Unwrap(wrapper)); +} + +{ + // Wrapping does not break an object's prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + assert.ok(test_object.Unwrap(wrapper)); + assert.strictEqual(wrapper.protoA, true); +} + +{ + // The pointer can still be unwrapped after inserting a link in the + // prototype chain. + const wrapper = {}; + const protoA = { protoA: true }; + Object.setPrototypeOf(wrapper, protoA); + test_object.Wrap(wrapper); + + const protoB = { protoB: true }; + Object.setPrototypeOf(protoB, Object.getPrototypeOf(wrapper)); + Object.setPrototypeOf(wrapper, protoB); + + assert.ok(test_object.Unwrap(wrapper)); + assert.strictEqual(wrapper.protoA, true); + assert.strictEqual(wrapper.protoB, true); +} + +{ + // Objects can be type-tagged and type-tag-checked. + const obj1 = test_object.TypeTaggedInstance(0); + 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(); + + // Type tag indices greater than the largest available index are rejected. + assert.throws(() => test_object.TypeTaggedInstance(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + assert.throws(() => test_object.TypeTaggedExternal(39), { + name: 'RangeError', + message: 'Invalid type index', + }); + + // 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); + + // Wrongly tagged objects are rejected. + assert.strictEqual(test_object.CheckTypeTag(0, obj2), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj1), false); + assert.strictEqual(test_object.CheckTypeTag(0, obj3), false); + assert.strictEqual(test_object.CheckTypeTag(1, obj4), false); + 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); + + // 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); +} + +{ + // Normal and nonexistent properties can be deleted. + const sym = Symbol(); + const obj = { foo: 'bar', [sym]: 'baz' }; + + assert.strictEqual('foo' in obj, true); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, true); + assert.strictEqual('does_not_exist' in obj, false); + assert.strictEqual(test_object.Delete(obj, sym), true); + assert.strictEqual('foo' in obj, false); + assert.strictEqual(sym in obj, false); + assert.strictEqual('does_not_exist' in obj, false); +} + +{ + // Non-configurable properties are not deleted. + const obj = {}; + + Object.defineProperty(obj, 'foo', { configurable: false }); + assert.strictEqual(test_object.Delete(obj, 'foo'), false); + assert.strictEqual('foo' in obj, true); +} + +{ + // Prototype properties are not deleted. + function Foo() { + this.foo = 'bar'; + } + + Foo.prototype.foo = 'baz'; + + const obj = new Foo(); + + assert.strictEqual(obj.foo, 'bar'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); + assert.strictEqual(test_object.Delete(obj, 'foo'), true); + assert.strictEqual(obj.foo, 'baz'); +} + +{ + // napi_get_property_names gets the right set of property names: includes + // prototypes, only enumerable properties, skips symbols, and includes + // indices converted to strings. + const object = { + __proto__: { + inherited: 1, + }, + }; + + const fooSymbol = Symbol('foo'); + + object.normal = 2; + object[fooSymbol] = 3; + Object.defineProperty(object, 'unenumerable', { + value: 4, + enumerable: false, + writable: true, + configurable: true, + }); + Object.defineProperty(object, 'writable', { + value: 4, + enumerable: true, + writable: true, + configurable: false, + }); + Object.defineProperty(object, 'configurable', { + value: 4, + enumerable: true, + writable: false, + configurable: true, + }); + object[5] = 5; + + assert.deepStrictEqual(test_object.GetPropertyNames(object), [ + '5', + 'normal', + 'writable', + 'configurable', + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetSymbolNames(object), [fooSymbol]); + + assert.deepStrictEqual(test_object.GetEnumerableWritableNames(object), [ + '5', + 'normal', + 'writable', + fooSymbol, + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetOwnWritableNames(object), [ + '5', + 'normal', + 'unenumerable', + 'writable', + fooSymbol, + ]); + + assert.deepStrictEqual(test_object.GetEnumerableConfigurableNames(object), [ + '5', + 'normal', + 'configurable', + fooSymbol, + 'inherited', + ]); + + assert.deepStrictEqual(test_object.GetOwnConfigurableNames(object), [ + '5', + 'normal', + 'unenumerable', + 'configurable', + fooSymbol, + ]); +} + +// Passing NULL to napi_set_property reports the right error. +assert.deepStrictEqual(test_object.TestSetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + valueIsNull: 'Invalid argument', +}); + +// Passing NULL to napi_has_property reports the right error. +assert.deepStrictEqual(test_object.TestHasProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +// Passing NULL to napi_get_property reports the right error. +assert.deepStrictEqual(test_object.TestGetProperty(), { + envIsNull: 'Invalid argument', + objectIsNull: 'Invalid argument', + keyIsNull: 'Invalid argument', + resultIsNull: 'Invalid argument', +}); + +{ + const obj = { x: 'a', y: 'b', z: 'c' }; + + test_object.TestSeal(obj); + + assert.strictEqual(Object.isSealed(obj), true); + + // Test files are modules and therefore strict, so these violations throw + // rather than fail silently. The message text is engine-specific, so only + // the error type is asserted. + assert.throws(() => { + obj.w = 'd'; + }, TypeError); + + assert.throws(() => { + delete obj.x; + }, TypeError); + + // Sealing still allows updating existing properties. + obj.x = 'd'; + assert.strictEqual(obj.x, 'd'); +} + +{ + const obj = { x: 10, y: 10, z: 10 }; + + test_object.TestFreeze(obj); + + assert.strictEqual(Object.isFrozen(obj), true); + + assert.throws(() => { + obj.x = 10; + }, TypeError); + + assert.throws(() => { + obj.w = 15; + }, TypeError); + + assert.throws(() => { + delete obj.x; + }, TypeError); +} diff --git a/tests/js-native-api/test_object/testCreateObjectWithProperties.js b/tests/js-native-api/test_object/testCreateObjectWithProperties.js new file mode 100644 index 0000000..8843d42 --- /dev/null +++ b/tests/js-native-api/test_object/testCreateObjectWithProperties.js @@ -0,0 +1,41 @@ +// node_api_create_object_with_properties is gated behind NAPI_EXPERIMENTAL, so +// it lives in its own addon and the stable test_object addon stays loadable +// everywhere. +if (!experimentalFeatures.createObjectWithProperties) { + skipTest(); +} + +const addon = loadAddon('test_object_create_with_properties'); + +{ + // A null prototype plus three properties of differing types. + const objectWithProperties = addon.TestCreateObjectWithProperties(); + + assert.strictEqual(typeof objectWithProperties, 'object'); + assert.strictEqual(Object.getPrototypeOf(objectWithProperties), null); + assert.strictEqual(objectWithProperties.name, 'Foo'); + assert.strictEqual(objectWithProperties.age, 42); + assert.strictEqual(objectWithProperties.active, true); +} + +{ + // Zero properties and a NULL prototype argument. + const emptyObject = addon.TestCreateObjectWithPropertiesEmpty(); + + assert.strictEqual(typeof emptyObject, 'object'); + assert.strictEqual(Object.keys(emptyObject).length, 0); +} + +{ + // A supplied prototype contributes its members without becoming own + // properties. + const objectWithCustomPrototype = addon.TestCreateObjectWithCustomPrototype(); + + assert.strictEqual(typeof objectWithCustomPrototype, 'object'); + assert.deepStrictEqual( + Object.getOwnPropertyNames(objectWithCustomPrototype), + ['value'], + ); + assert.strictEqual(objectWithCustomPrototype.value, 42); + assert.strictEqual(typeof objectWithCustomPrototype.test, 'function'); +} diff --git a/tests/js-native-api/test_object/test_object.c b/tests/js-native-api/test_object/test_object.c new file mode 100644 index 0000000..dc2f0bb --- /dev/null +++ b/tests/js-native-api/test_object/test_object.c @@ -0,0 +1,665 @@ +#include +#include +#include "../common.h" +#include "../entry_point.h" + +static int test_value = 3; + +static napi_value Get(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + napi_value output; + NODE_API_CALL(env, napi_get_property(env, args[0], args[1], &output)); + + return output; +} + +static napi_value GetNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + napi_value output; + NODE_API_CALL(env, napi_get_named_property(env, args[0], key, &output)); + + return output; +} + +static napi_value GetPropertyNames(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value output; + NODE_API_CALL(env, napi_get_property_names(env, args[0], &output)); + + return output; +} + +// Returns the property names selected by the given filter/conversion, so the +// test can compare each napi_get_all_property_names mode against JS. +static napi_value GetAllNames(napi_env env, + napi_callback_info info, + napi_key_collection_mode mode, + napi_key_filter filter) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value output; + NODE_API_CALL(env, + napi_get_all_property_names(env, + args[0], + mode, + filter, + napi_key_numbers_to_strings, + &output)); + + return output; +} + +static napi_value GetSymbolNames(napi_env env, napi_callback_info info) { + return GetAllNames( + env, info, napi_key_include_prototypes, napi_key_skip_strings); +} + +static napi_value GetEnumerableWritableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, + info, + napi_key_include_prototypes, + napi_key_enumerable | napi_key_writable); +} + +static napi_value GetOwnWritableNames(napi_env env, napi_callback_info info) { + return GetAllNames(env, info, napi_key_own_only, napi_key_writable); +} + +static napi_value GetEnumerableConfigurableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, + info, + napi_key_include_prototypes, + napi_key_enumerable | napi_key_configurable); +} + +static napi_value GetOwnConfigurableNames(napi_env env, + napi_callback_info info) { + return GetAllNames(env, info, napi_key_own_only, napi_key_configurable); +} + +static napi_value Set(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + NODE_API_CALL(env, napi_set_property(env, args[0], args[1], args[2])); + + napi_value valuetrue; + NODE_API_CALL(env, napi_get_boolean(env, true, &valuetrue)); + + return valuetrue; +} + +static napi_value SetNamed(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 3, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + + NODE_API_CALL(env, napi_set_named_property(env, args[0], key, args[2])); + + napi_value value_true; + NODE_API_CALL(env, napi_get_boolean(env, true, &value_true)); + + return value_true; +} + +static napi_value Has(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + bool has_property; + NODE_API_CALL(env, napi_has_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasNamed(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + char key[256] = ""; + size_t key_length; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 2, "Wrong number of arguments"); + + napi_valuetype value_type0; + NODE_API_CALL(env, napi_typeof(env, args[0], &value_type0)); + + NODE_API_ASSERT(env, + value_type0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype value_type1; + NODE_API_CALL(env, napi_typeof(env, args[1], &value_type1)); + + NODE_API_ASSERT(env, + value_type1 == napi_string || value_type1 == napi_symbol, + "Wrong type of arguments. Expects a string as second."); + + NODE_API_CALL( + env, napi_get_value_string_utf8(env, args[1], key, 255, &key_length)); + key[255] = 0; + NODE_API_ASSERT( + env, key_length <= 255, "Cannot accommodate keys longer than 255 bytes"); + + bool has_property; + NODE_API_CALL(env, napi_has_named_property(env, args[0], key, &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value HasOwn(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + // The key is deliberately not type-checked here: the test asserts that + // napi_has_own_property itself rejects a key that is not a name. + bool has_property; + NODE_API_CALL(env, + napi_has_own_property(env, args[0], args[1], &has_property)); + + napi_value ret; + NODE_API_CALL(env, napi_get_boolean(env, has_property, &ret)); + + return ret; +} + +static napi_value Delete(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + NODE_API_ASSERT(env, argc == 2, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_valuetype valuetype1; + NODE_API_CALL(env, napi_typeof(env, args[1], &valuetype1)); + NODE_API_ASSERT(env, + valuetype1 == napi_string || valuetype1 == napi_symbol, + "Wrong type of arguments. Expects a string or symbol as " + "second."); + + bool result; + napi_value ret; + NODE_API_CALL(env, napi_delete_property(env, args[0], args[1], &result)); + NODE_API_CALL(env, napi_get_boolean(env, result, &ret)); + + return ret; +} + +static napi_value New(napi_env env, napi_callback_info info) { + napi_value ret; + NODE_API_CALL(env, napi_create_object(env, &ret)); + + napi_value num; + NODE_API_CALL(env, napi_create_int32(env, 987654321, &num)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_number", num)); + + napi_value str; + const char* str_val = "test string"; + size_t str_len = strlen(str_val); + NODE_API_CALL(env, napi_create_string_utf8(env, str_val, str_len, &str)); + + NODE_API_CALL(env, napi_set_named_property(env, ret, "test_string", str)); + + return ret; +} + +static napi_value Inflate(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_ASSERT(env, argc >= 1, "Wrong number of arguments"); + + napi_valuetype valuetype0; + NODE_API_CALL(env, napi_typeof(env, args[0], &valuetype0)); + + NODE_API_ASSERT(env, + valuetype0 == napi_object, + "Wrong type of arguments. Expects an object as first " + "argument."); + + napi_value obj = args[0]; + napi_value propertynames; + NODE_API_CALL(env, napi_get_property_names(env, obj, &propertynames)); + + uint32_t i, length; + NODE_API_CALL(env, napi_get_array_length(env, propertynames, &length)); + + for (i = 0; i < length; i++) { + napi_value property_str; + NODE_API_CALL(env, napi_get_element(env, propertynames, i, &property_str)); + + napi_value value; + NODE_API_CALL(env, napi_get_property(env, obj, property_str, &value)); + + double double_val; + NODE_API_CALL(env, napi_get_value_double(env, value, &double_val)); + NODE_API_CALL(env, napi_create_double(env, double_val + 1, &value)); + NODE_API_CALL(env, napi_set_property(env, obj, property_str, value)); + } + + return obj; +} + +static napi_value Wrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + NODE_API_CALL(env, napi_wrap(env, arg, &test_value, NULL, NULL, NULL)); + return NULL; +} + +static napi_value Unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value arg; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &arg, NULL, NULL)); + + void* data; + NODE_API_CALL(env, napi_unwrap(env, arg, &data)); + + bool is_expected = (data != NULL && *(int*)data == 3); + napi_value result; + NODE_API_CALL(env, napi_get_boolean(env, is_expected, &result)); + return result; +} + +static napi_value TestSetProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key, value; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(env, napi_create_object(env, &value)); + + status = napi_set_property(NULL, object, key, value); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_set_property(env, NULL, key, value); + add_last_status(env, "objectIsNull", object); + + napi_set_property(env, object, NULL, value); + add_last_status(env, "keyIsNull", object); + + napi_set_property(env, object, key, NULL); + add_last_status(env, "valueIsNull", object); + + return object; +} + +static napi_value TestHasProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key; + bool result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + + status = napi_has_property(NULL, object, key, &result); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_has_property(env, NULL, key, &result); + add_last_status(env, "objectIsNull", object); + + napi_has_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", object); + + napi_has_property(env, object, key, NULL); + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestGetProperty(napi_env env, napi_callback_info info) { + napi_status status; + napi_value object, key, result; + + NODE_API_CALL(env, napi_create_object(env, &object)); + NODE_API_CALL(env, napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &key)); + NODE_API_CALL(env, napi_create_object(env, &result)); + + status = napi_get_property(NULL, object, key, &result); + + add_returned_status( + env, "envIsNull", object, "Invalid argument", napi_invalid_arg, status); + + napi_get_property(env, NULL, key, &result); + add_last_status(env, "objectIsNull", object); + + napi_get_property(env, object, NULL, &result); + add_last_status(env, "keyIsNull", object); + + napi_get_property(env, object, key, NULL); + add_last_status(env, "resultIsNull", object); + + return object; +} + +static napi_value TestFreeze(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_CALL(env, napi_object_freeze(env, args[0])); + + return args[0]; +} + +static napi_value TestSeal(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, args, NULL, NULL)); + + NODE_API_CALL(env, napi_object_seal(env, args[0])); + + return args[0]; +} + +// Type tags are basically 128-bit UUIDs. The zero tag and the half-zero tags +// are included so an implementation cannot pass by treating "empty" as a +// wildcard. +#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) + +// V8 will not allow us to construct an external with a NULL data value. +#define IN_LIEU_OF_NULL ((void*)0x1) + +// Tags `instance`, then wipes the local copy of the tag. A tagging +// implementation that stored a pointer rather than the 128-bit value would be +// left pointing at cleared stack memory, so CheckTypeTag would fail. +static napi_status tag_and_clear(napi_env env, + napi_value instance, + uint32_t type_index) { + napi_type_tag tag = type_tags[type_index]; + NODE_API_CHECK_STATUS(napi_type_tag_object(env, instance, &tag)); + memset(&tag, 0, sizeof(tag)); + return napi_ok; +} + +static napi_value TypeTaggedInstance(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_object(env, &instance)); + NODE_API_CALL(env, tag_and_clear(env, instance, type_index)); + + return instance; +} + +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, tag_and_clear(env, instance, 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; + + 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], &result)); + NODE_API_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[] = { + 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("TestGetProperty", TestGetProperty), + 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("TestFreeze", TestFreeze), + DECLARE_NODE_API_PROPERTY("TestSeal", TestSeal), + }; + + NODE_API_CALL(env, + napi_define_properties(env, + exports, + sizeof(descriptors) / + sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END diff --git a/tests/js-native-api/test_object/test_object_create_with_properties.c b/tests/js-native-api/test_object/test_object_create_with_properties.c new file mode 100644 index 0000000..0412860 --- /dev/null +++ b/tests/js-native-api/test_object/test_object_create_with_properties.c @@ -0,0 +1,100 @@ +// node_api_create_object_with_properties is experimental, so this addon is +// built separately from test_object.c: a runtime that lacks the symbol can +// still load the stable test_object addon. +#include +#include "../common.h" +#include "../entry_point.h" + +static napi_value TestCreateObjectWithProperties(napi_env env, + napi_callback_info info) { + napi_value names[3]; + napi_value values[3]; + napi_value result; + + NODE_API_CALL( + env, napi_create_string_utf8(env, "name", NAPI_AUTO_LENGTH, &names[0])); + NODE_API_CALL( + env, napi_create_string_utf8(env, "Foo", NAPI_AUTO_LENGTH, &values[0])); + + NODE_API_CALL( + env, napi_create_string_utf8(env, "age", NAPI_AUTO_LENGTH, &names[1])); + NODE_API_CALL(env, napi_create_int32(env, 42, &values[1])); + + NODE_API_CALL( + env, napi_create_string_utf8(env, "active", NAPI_AUTO_LENGTH, &names[2])); + NODE_API_CALL(env, napi_get_boolean(env, true, &values[2])); + + napi_value null_prototype; + NODE_API_CALL(env, napi_get_null(env, &null_prototype)); + NODE_API_CALL(env, + node_api_create_object_with_properties( + env, null_prototype, names, values, 3, &result)); + + return result; +} + +static napi_value TestCreateObjectWithPropertiesEmpty(napi_env env, + napi_callback_info info) { + napi_value result; + + NODE_API_CALL( + env, + node_api_create_object_with_properties(env, NULL, NULL, NULL, 0, &result)); + + return result; +} + +static napi_value TestCreateObjectWithCustomPrototype(napi_env env, + napi_callback_info info) { + napi_value prototype; + napi_value method_name; + napi_value method_func; + napi_value names[1]; + napi_value values[1]; + napi_value result; + + NODE_API_CALL(env, napi_create_object(env, &prototype)); + NODE_API_CALL( + env, + napi_create_string_utf8(env, "test", NAPI_AUTO_LENGTH, &method_name)); + NODE_API_CALL(env, + napi_create_function(env, + "test", + NAPI_AUTO_LENGTH, + TestCreateObjectWithProperties, + NULL, + &method_func)); + NODE_API_CALL(env, napi_set_property(env, prototype, method_name, method_func)); + + NODE_API_CALL( + env, napi_create_string_utf8(env, "value", NAPI_AUTO_LENGTH, &names[0])); + NODE_API_CALL(env, napi_create_int32(env, 42, &values[0])); + + NODE_API_CALL(env, + node_api_create_object_with_properties( + env, prototype, names, values, 1, &result)); + + return result; +} + +EXTERN_C_START +napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor descriptors[] = { + DECLARE_NODE_API_PROPERTY("TestCreateObjectWithProperties", + TestCreateObjectWithProperties), + DECLARE_NODE_API_PROPERTY("TestCreateObjectWithPropertiesEmpty", + TestCreateObjectWithPropertiesEmpty), + DECLARE_NODE_API_PROPERTY("TestCreateObjectWithCustomPrototype", + TestCreateObjectWithCustomPrototype), + }; + + NODE_API_CALL(env, + napi_define_properties(env, + exports, + sizeof(descriptors) / + sizeof(*descriptors), + descriptors)); + + return exports; +} +EXTERN_C_END