From 722dbc505e9332070cc9b7dc1155981a1819ad81 Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Fri, 7 Apr 2023 07:40:16 -0700 Subject: [PATCH] node-api: get Node API version used by addon --- src/api/environment.cc | 3 +- src/js_native_api_v8.cc | 8 +- src/js_native_api_v8.h | 10 +- src/node_api.cc | 19 +- src/node_api.h | 10 + src/node_api_internals.h | 3 +- src/node_binding.cc | 13 +- src/node_binding.h | 5 +- src/node_version.h | 6 +- test/cctest/test_node_api.cc | 3 +- .../test_reference_all_types/binding.gyp | 9 + .../node-api/test_reference_all_types/test.js | 90 +++++++++ .../test_reference_all_types.c | 175 ++++++++++++++++++ .../test_reference_obj_only/binding.gyp | 9 + test/node-api/test_reference_obj_only/test.js | 106 +++++++++++ .../test_reference_obj_only.c | 175 ++++++++++++++++++ 16 files changed, 625 insertions(+), 19 deletions(-) create mode 100644 test/node-api/test_reference_all_types/binding.gyp create mode 100644 test/node-api/test_reference_all_types/test.js create mode 100644 test/node-api/test_reference_all_types/test_reference_all_types.c create mode 100644 test/node-api/test_reference_obj_only/binding.gyp create mode 100644 test/node-api/test_reference_obj_only/test.js create mode 100644 test/node-api/test_reference_obj_only/test_reference_obj_only.c diff --git a/src/api/environment.cc b/src/api/environment.cc index c9bb4bdf1645e2..e8e609709d0b59 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -884,7 +884,8 @@ void AddLinkedBinding(Environment* env, exports, module, context, - reinterpret_cast(priv)); + reinterpret_cast(priv), + NAPI_DEFAULT_MODULE_API_VERSION); }, name, reinterpret_cast(fn), diff --git a/src/js_native_api_v8.cc b/src/js_native_api_v8.cc index f4df74299781c8..4186ec02f5231c 100644 --- a/src/js_native_api_v8.cc +++ b/src/js_native_api_v8.cc @@ -2419,9 +2419,11 @@ napi_status NAPI_CDECL napi_create_reference(napi_env env, CHECK_ARG(env, result); v8::Local v8_value = v8impl::V8LocalValueFromJsValue(value); - if (!(v8_value->IsObject() || v8_value->IsFunction() || - v8_value->IsSymbol())) { - return napi_set_last_error(env, napi_invalid_arg); + if (env->module_api_version <= 8) { + if (!(v8_value->IsObject() || v8_value->IsFunction() || + v8_value->IsSymbol())) { + return napi_set_last_error(env, napi_invalid_arg); + } } v8impl::Reference* reference = v8impl::Reference::New( diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index 12b3ab68a8d979..0355acba33686b 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -51,8 +51,13 @@ class Finalizer; } // end of namespace v8impl struct napi_env__ { - explicit napi_env__(v8::Local context) - : isolate(context->GetIsolate()), context_persistent(isolate, context) { + explicit napi_env__(v8::Local context, + int32_t module_api_version) + : isolate(context->GetIsolate()), + context_persistent(isolate, context), + module_api_version(module_api_version != 0 + ? module_api_version + : NAPI_DEFAULT_MODULE_API_VERSION) { napi_clear_last_error(this); } @@ -148,6 +153,7 @@ struct napi_env__ { int open_callback_scopes = 0; int refs = 1; void* instance_data = nullptr; + int32_t module_api_version = NAPI_DEFAULT_MODULE_API_VERSION; protected: // Should not be deleted directly. Delete with `napi_env__::DeleteMe()` diff --git a/src/node_api.cc b/src/node_api.cc index 4d95d518286b87..178fa9b48becc3 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -20,8 +20,9 @@ #include node_napi_env__::node_napi_env__(v8::Local context, - const std::string& module_filename) - : napi_env__(context), filename(module_filename) { + const std::string& module_filename, + int32_t module_api_version) + : napi_env__(context, module_api_version), filename(module_filename) { CHECK_NOT_NULL(node_env()); } @@ -159,10 +160,11 @@ class BufferFinalizer : private Finalizer { }; inline napi_env NewEnv(v8::Local context, - const std::string& module_filename) { + const std::string& module_filename, + int32_t module_api_version) { node_napi_env result; - result = new node_napi_env__(context, module_filename); + result = new node_napi_env__(context, module_filename, module_api_version); // TODO(addaleax): There was previously code that tried to delete the // napi_env when its v8::Context was garbage collected; // However, as long as N-API addons using this napi_env are in place, @@ -623,17 +625,20 @@ static void napi_module_register_cb(v8::Local exports, v8::Local module, v8::Local context, void* priv) { + const napi_module* napi_mod = static_cast(priv); napi_module_register_by_symbol( exports, module, context, - static_cast(priv)->nm_register_func); + napi_mod->nm_register_func, + NAPI_DEFAULT_MODULE_API_VERSION); } void napi_module_register_by_symbol(v8::Local exports, v8::Local module, v8::Local context, - napi_addon_register_func init) { + napi_addon_register_func init, + int32_t module_api_version) { node::Environment* node_env = node::Environment::GetCurrent(context); std::string module_filename = ""; if (init == nullptr) { @@ -661,7 +666,7 @@ void napi_module_register_by_symbol(v8::Local exports, } // Create a new napi_env for this specific module. - napi_env env = v8impl::NewEnv(context, module_filename); + napi_env env = v8impl::NewEnv(context, module_filename, module_api_version); napi_value _exports = nullptr; env->CallIntoModule([&](napi_env env) { diff --git a/src/node_api.h b/src/node_api.h index 4d1b50414e2e02..b5355f457dae75 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -30,6 +30,7 @@ struct uv_loop_s; // Forward declaration. typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env, napi_value exports); +typedef int32_t(NAPI_CDECL* napi_addon_get_api_version_func)(); // Used by deprecated registration method napi_module_register. typedef struct napi_module { @@ -54,11 +55,20 @@ typedef struct napi_module { #define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v #endif +#define NAPI_MODULE_GET_API_VERSION_BASE napi_module_get_api_version_v + #define NAPI_MODULE_INITIALIZER \ NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) +#define NAPI_MODULE_GET_API_VERSION \ + NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_GET_API_VERSION_BASE, \ + NAPI_MODULE_VERSION) + #define NAPI_MODULE_INIT() \ EXTERN_C_START \ + NAPI_MODULE_EXPORT int32_t NAPI_MODULE_GET_API_VERSION() { \ + return NAPI_VERSION; \ + } \ NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ napi_value exports); \ EXTERN_C_END \ diff --git a/src/node_api_internals.h b/src/node_api_internals.h index 8b4db661e65fde..6ac80a8b2b5893 100644 --- a/src/node_api_internals.h +++ b/src/node_api_internals.h @@ -10,7 +10,8 @@ struct node_napi_env__ : public napi_env__ { node_napi_env__(v8::Local context, - const std::string& module_filename); + const std::string& module_filename, + int32_t module_api_version); bool can_call_into_js() const override; v8::Maybe mark_arraybuffer_as_untransferable( diff --git a/src/node_binding.cc b/src/node_binding.cc index 90855aada5dab9..dfcf59736e33fe 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -408,6 +408,12 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) { dlib->GetSymbolAddress(name)); } +inline napi_addon_get_api_version_func GetNapiAddonGetApiVersionCallback( + DLib* dlib) { + return reinterpret_cast( + dlib->GetSymbolAddress(STRINGIFY(NAPI_MODULE_GET_API_VERSION))); +} + // DLOpen is process.dlopen(module, filename, flags). // Used to load 'module.node' dynamically shared objects. // @@ -484,7 +490,12 @@ void DLOpen(const FunctionCallbackInfo& args) { callback(exports, module, context); return true; } else if (auto napi_callback = GetNapiInitializerCallback(dlib)) { - napi_module_register_by_symbol(exports, module, context, napi_callback); + int32_t module_api_version = 0; + if (auto get_version = GetNapiAddonGetApiVersionCallback(dlib)) { + module_api_version = get_version(); + } + napi_module_register_by_symbol( + exports, module, context, napi_callback, module_api_version); return true; } else { mp = dlib->GetSavedModuleFromGlobalHandleMap(); diff --git a/src/node_binding.h b/src/node_binding.h index 1b024774e120a9..805064b36ebd4b 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -21,7 +21,7 @@ enum { // Make sure our internal values match the public API's values. static_assert(static_cast(NM_F_LINKED) == - static_cast(node::ModuleFlags::kLinked), + static_cast(node::ModuleFlags::kLinked), "NM_F_LINKED != node::ModuleFlags::kLinked"); #if NODE_HAVE_I18N_SUPPORT @@ -52,7 +52,8 @@ static_assert(static_cast(NM_F_LINKED) == void napi_module_register_by_symbol(v8::Local exports, v8::Local module, v8::Local context, - napi_addon_register_func init); + napi_addon_register_func init, + int32_t module_api_version); namespace node { diff --git a/src/node_version.h b/src/node_version.h index 6d1728734cb166..76cf4c3b764585 100644 --- a/src/node_version.h +++ b/src/node_version.h @@ -93,6 +93,10 @@ // The NAPI_VERSION provided by this version of the runtime. This is the version // which the Node binary being built supports. -#define NAPI_VERSION 8 +#define NAPI_VERSION 8 + +// The NAPI_VERSION used by default by Node API modules if it is not explicitly +// specified. It must be always 8. +#define NAPI_DEFAULT_MODULE_API_VERSION 8 #endif // SRC_NODE_VERSION_H_ diff --git a/test/cctest/test_node_api.cc b/test/cctest/test_node_api.cc index ad5be52fc8ffcc..261e3e368c1012 100644 --- a/test/cctest/test_node_api.cc +++ b/test/cctest/test_node_api.cc @@ -34,7 +34,8 @@ TEST_F(NodeApiTest, CreateNodeApiEnv) { }; Local module_obj = Object::New(isolate_); Local exports_obj = Object::New(isolate_); - napi_module_register_by_symbol(exports_obj, module_obj, env->context(), init); + napi_module_register_by_symbol( + exports_obj, module_obj, env->context(), init, 0); ASSERT_NE(addon_env, nullptr); node_napi_env internal_env = reinterpret_cast(addon_env); EXPECT_EQ(internal_env->node_env(), env); diff --git a/test/node-api/test_reference_all_types/binding.gyp b/test/node-api/test_reference_all_types/binding.gyp new file mode 100644 index 00000000000000..f58807b0e5baf1 --- /dev/null +++ b/test/node-api/test_reference_all_types/binding.gyp @@ -0,0 +1,9 @@ +{ + "targets": [ + { + "target_name": "test_reference_all_types", + "sources": [ "test_reference_all_types.c" ], + "defines": [ 'NAPI_EXPERIMENTAL' ] + } + ] +} diff --git a/test/node-api/test_reference_all_types/test.js b/test/node-api/test_reference_all_types/test.js new file mode 100644 index 00000000000000..caee3f4d803125 --- /dev/null +++ b/test/node-api/test_reference_all_types/test.js @@ -0,0 +1,90 @@ +'use strict'; +// Flags: --expose-gc +// +// Testing API calls for references to all value types. +// This test uses NAPI_MODULE_INIT macro to initialize module. +// +const { gcUntil, buildType } = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${buildType}/test_reference_all_types`); + +async function runTests() { + let allEntries = []; + + (() => { + // Create values of all napi_valuetype types. + const undefinedValue = undefined; + const nullValue = null; + const booleanValue = false; + const numberValue = 42; + const stringValue = 'test_string'; + const symbolValue = Symbol.for('test_symbol'); + const objectValue = { x: 1, y: 2 }; + const functionValue = (x, y) => x + y; + const externalValue = addon.createExternal(); + const bigintValue = 9007199254740991n; + + allEntries = [ + { value: undefinedValue, canBeWeak: false }, + { value: nullValue, canBeWeak: false }, + { value: booleanValue, canBeWeak: false }, + { value: numberValue, canBeWeak: false }, + { value: stringValue, canBeWeak: false }, + { value: symbolValue, canBeWeak: false }, + { value: objectValue, canBeWeak: true }, + { value: functionValue, canBeWeak: true }, + { value: externalValue, canBeWeak: true }, + { value: bigintValue, canBeWeak: false }, + ]; + + // Go over all values of different types, create strong ref values for + // them, read the stored values, and check how the ref count works. + for (const entry of allEntries) { + const index = addon.createRef(entry.value); + const refValue = addon.getRefValue(index); + assert.strictEqual(entry.value, refValue); + assert.strictEqual(addon.ref(index), 2); + assert.strictEqual(addon.unref(index), 1); + assert.strictEqual(addon.unref(index), 0); + } + + // The references become weak pointers when the ref count is 0. + // Here we know that the GC is not run yet because the values are + // still in the allEntries array. + allEntries.forEach((entry, index) => { + assert.strictEqual(addon.getRefValue(index), entry.value); + // Set to undefined to allow GC collect the value. + entry.value = undefined; + }); + + // To check that GC pass is done. + const objWithFinalizer = {}; + addon.addFinalizer(objWithFinalizer); + })(); + + assert.strictEqual(addon.getFinalizeCount(), 0); + await gcUntil('Wait until a finalizer is called', + () => (addon.getFinalizeCount() === 1)); + + // Create and call finalizer again to make sure that we had another GC pass. + (() => { + const objWithFinalizer = {}; + addon.addFinalizer(objWithFinalizer); + })(); + await gcUntil('Wait until a finalizer is called again', + () => (addon.getFinalizeCount() === 2)); + + // After GC and finalizers run, all values that support weak reference + // semantic must return undefined value. + // It also includes the value at index 0 because it is the undefined value. + // Other value types are not collected by GC. + allEntries.forEach((entry, index) => { + if (entry.canBeWeak || index === 0) { + assert.strictEqual(addon.getRefValue(index), undefined); + } else { + assert.notStrictEqual(addon.getRefValue(index), undefined); + } + addon.deleteRef(index); + }); +} +runTests(); diff --git a/test/node-api/test_reference_all_types/test_reference_all_types.c b/test/node-api/test_reference_all_types/test_reference_all_types.c new file mode 100644 index 00000000000000..332baa7e8433d2 --- /dev/null +++ b/test/node-api/test_reference_all_types/test_reference_all_types.c @@ -0,0 +1,175 @@ +#define NAPI_EXPERIMENTAL +#include +#include "../../js-native-api/common.h" +#include "stdlib.h" + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define NODE_API_CHECK_STATUS(env, the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +static uint32_t finalizeCount = 0; + +static void FreeData(napi_env env, void* data, void* hint) { + NODE_API_ASSERT_RETURN_VOID(env, data != NULL, "Expects non-NULL data."); + free(data); +} + +static void Finalize(napi_env env, void* data, void* hint) { + ++finalizeCount; +} + +static napi_status GetArgValue(napi_env env, + napi_callback_info info, + napi_value* argValue) { + size_t argc = 1; + NODE_API_CHECK_STATUS( + env, napi_get_cb_info(env, info, &argc, argValue, NULL, NULL)); + + NODE_API_ASSERT_STATUS(env, argc == 1, "Expects one arg."); + return napi_ok; +} + +static napi_status GetArgValueAsIndex(napi_env env, + napi_callback_info info, + uint32_t* index) { + napi_value argValue; + NODE_API_CHECK_STATUS(env, GetArgValue(env, info, &argValue)); + + napi_valuetype valueType; + NODE_API_CHECK_STATUS(env, napi_typeof(env, argValue, &valueType)); + NODE_API_ASSERT_STATUS( + env, valueType == napi_number, "Argument must be a number."); + + return napi_get_value_uint32(env, argValue, index); +} + +static napi_status GetRef(napi_env env, + napi_callback_info info, + napi_ref* ref) { + uint32_t index; + NODE_API_CHECK_STATUS(env, GetArgValueAsIndex(env, info, &index)); + + napi_ref* refValues; + NODE_API_CHECK_STATUS(env, napi_get_instance_data(env, (void**)&refValues)); + NODE_API_ASSERT_STATUS(env, refValues != NULL, "Cannot get instance data."); + + *ref = refValues[index]; + return napi_ok; +} + +static napi_value ToUInt32Value(napi_env env, uint32_t value) { + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, value, &result)); + return result; +} + +static napi_status InitRefArray(napi_env env) { + // valueRefs array has one entry per napi_valuetype + napi_ref* valueRefs = malloc(sizeof(napi_ref) * ((int)napi_bigint + 1)); + return napi_set_instance_data(env, valueRefs, &FreeData, NULL); +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + napi_value result; + int* data = (int*)malloc(sizeof(int)); + *data = 42; + NODE_API_CALL(env, napi_create_external(env, data, &FreeData, NULL, &result)); + return result; +} + +static napi_value CreateRef(napi_env env, napi_callback_info info) { + napi_value argValue; + NODE_API_CALL(env, GetArgValue(env, info, &argValue)); + + napi_valuetype valueType; + NODE_API_CALL(env, napi_typeof(env, argValue, &valueType)); + uint32_t index = (uint32_t)valueType; + + napi_ref* valueRefs; + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&valueRefs)); + NODE_API_CALL(env, + napi_create_reference(env, argValue, 1, valueRefs + index)); + + return ToUInt32Value(env, index); +} + +static napi_value GetRefValue(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + napi_value value; + NODE_API_CALL(env, napi_get_reference_value(env, refValue, &value)); + return value; +} + +static napi_value Ref(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + uint32_t refCount; + NODE_API_CALL(env, napi_reference_ref(env, refValue, &refCount)); + return ToUInt32Value(env, refCount); +} + +static napi_value Unref(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + uint32_t refCount; + NODE_API_CALL(env, napi_reference_unref(env, refValue, &refCount)); + return ToUInt32Value(env, refCount); +} + +static napi_value DeleteRef(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + NODE_API_CALL(env, napi_delete_reference(env, refValue)); + return NULL; +} + +static napi_value AddFinalizer(napi_env env, napi_callback_info info) { + napi_value obj; + NODE_API_CALL(env, GetArgValue(env, info, &obj)); + + napi_valuetype valueType; + NODE_API_CALL(env, napi_typeof(env, obj, &valueType)); + NODE_API_ASSERT(env, valueType == napi_object, "Argument must be an object."); + + NODE_API_CALL(env, napi_add_finalizer(env, obj, NULL, &Finalize, NULL, NULL)); + return NULL; +} + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + return ToUInt32Value(env, finalizeCount); +} + +EXTERN_C_START + +NAPI_MODULE_INIT() { + finalizeCount = 0; + NODE_API_CALL(env, InitRefArray(env)); + + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createRef", CreateRef), + DECLARE_NODE_API_PROPERTY("getRefValue", GetRefValue), + DECLARE_NODE_API_PROPERTY("ref", Ref), + DECLARE_NODE_API_PROPERTY("unref", Unref), + DECLARE_NODE_API_PROPERTY("deleteRef", DeleteRef), + DECLARE_NODE_API_PROPERTY("addFinalizer", AddFinalizer), + DECLARE_NODE_API_PROPERTY("getFinalizeCount", GetFinalizeCount), + }; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +EXTERN_C_END diff --git a/test/node-api/test_reference_obj_only/binding.gyp b/test/node-api/test_reference_obj_only/binding.gyp new file mode 100644 index 00000000000000..8165ac9092d594 --- /dev/null +++ b/test/node-api/test_reference_obj_only/binding.gyp @@ -0,0 +1,9 @@ +{ + "targets": [ + { + "target_name": "test_reference_obj_only", + "sources": [ "test_reference_obj_only.c" ], + "defines": [ "NAPI_VERSION=8" ] + } + ] +} diff --git a/test/node-api/test_reference_obj_only/test.js b/test/node-api/test_reference_obj_only/test.js new file mode 100644 index 00000000000000..a1f34c8ad0a7df --- /dev/null +++ b/test/node-api/test_reference_obj_only/test.js @@ -0,0 +1,106 @@ +'use strict'; +// Flags: --expose-gc +// +// Testing API calls for references to only object, function, and symbol types. +// This is the reference behavior without the napi_feature_reference_all_types +// feature enabled. +// This test uses NAPI_MODULE_INIT macro to initialize module. +// +const { gcUntil, buildType } = require('../../common'); +const assert = require('assert'); +const addon = require(`./build/${buildType}/test_reference_obj_only`); + +async function runTests() { + let allEntries = []; + + (() => { + // Create values of all napi_valuetype types. + const undefinedValue = undefined; + const nullValue = null; + const booleanValue = false; + const numberValue = 42; + const stringValue = 'test_string'; + const symbolValue = Symbol.for('test_symbol'); + const objectValue = { x: 1, y: 2 }; + const functionValue = (x, y) => x + y; + const externalValue = addon.createExternal(); + const bigintValue = 9007199254740991n; + + allEntries = [ + { value: undefinedValue, canBeWeak: false, canBeRef: false }, + { value: nullValue, canBeWeak: false, canBeRef: false }, + { value: booleanValue, canBeWeak: false, canBeRef: false }, + { value: numberValue, canBeWeak: false, canBeRef: false }, + { value: stringValue, canBeWeak: false, canBeRef: false }, + { value: symbolValue, canBeWeak: false, canBeRef: true }, + { value: objectValue, canBeWeak: true, canBeRef: true }, + { value: functionValue, canBeWeak: true, canBeRef: true }, + { value: externalValue, canBeWeak: true, canBeRef: true }, + { value: bigintValue, canBeWeak: false, canBeRef: false }, + ]; + + // Go over all values of different types, create strong ref values for + // them, read the stored values, and check how the ref count works. + for (const entry of allEntries) { + if (entry.canBeRef) { + const index = addon.createRef(entry.value); + const refValue = addon.getRefValue(index); + assert.strictEqual(entry.value, refValue); + assert.strictEqual(addon.ref(index), 2); + assert.strictEqual(addon.unref(index), 1); + assert.strictEqual(addon.unref(index), 0); + } else { + assert.throws(() => { addon.createRef(entry.value); }, + { + name: 'Error', + message: 'Invalid argument', + }); + } + } + + // The references become weak pointers when the ref count is 0. + // The old reference were supported for objects, external objects, + // functions, and symbols. + // Here we know that the GC is not run yet because the values are + // still in the allEntries array. + allEntries.forEach((entry, index) => { + if (entry.canBeRef) { + assert.strictEqual(addon.getRefValue(index), entry.value); + } + // Set to undefined to allow GC collect the value. + entry.value = undefined; + }); + + // To check that GC pass is done. + const objWithFinalizer = {}; + addon.addFinalizer(objWithFinalizer); + })(); + + assert.strictEqual(addon.getFinalizeCount(), 0); + await gcUntil('Wait until a finalizer is called', + () => (addon.getFinalizeCount() === 1)); + + // Create and call finalizer again to make sure that we had another GC pass. + (() => { + const objWithFinalizer = {}; + addon.addFinalizer(objWithFinalizer); + })(); + await gcUntil('Wait until a finalizer is called again', + () => (addon.getFinalizeCount() === 2)); + + // After GC and finalizers run, all values that support weak reference + // semantic must return undefined value. + // It also includes the value at index 0 because it is the undefined value. + // Other value types are not collected by GC. + allEntries.forEach((entry, index) => { + if (entry.canBeRef) { + if (entry.canBeWeak || index === 0) { + assert.strictEqual(addon.getRefValue(index), undefined); + } else { + assert.notStrictEqual(addon.getRefValue(index), undefined); + } + addon.deleteRef(index); + } + }); +} +runTests(); diff --git a/test/node-api/test_reference_obj_only/test_reference_obj_only.c b/test/node-api/test_reference_obj_only/test_reference_obj_only.c new file mode 100644 index 00000000000000..332baa7e8433d2 --- /dev/null +++ b/test/node-api/test_reference_obj_only/test_reference_obj_only.c @@ -0,0 +1,175 @@ +#define NAPI_EXPERIMENTAL +#include +#include "../../js-native-api/common.h" +#include "stdlib.h" + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define NODE_API_CHECK_STATUS(env, the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +static uint32_t finalizeCount = 0; + +static void FreeData(napi_env env, void* data, void* hint) { + NODE_API_ASSERT_RETURN_VOID(env, data != NULL, "Expects non-NULL data."); + free(data); +} + +static void Finalize(napi_env env, void* data, void* hint) { + ++finalizeCount; +} + +static napi_status GetArgValue(napi_env env, + napi_callback_info info, + napi_value* argValue) { + size_t argc = 1; + NODE_API_CHECK_STATUS( + env, napi_get_cb_info(env, info, &argc, argValue, NULL, NULL)); + + NODE_API_ASSERT_STATUS(env, argc == 1, "Expects one arg."); + return napi_ok; +} + +static napi_status GetArgValueAsIndex(napi_env env, + napi_callback_info info, + uint32_t* index) { + napi_value argValue; + NODE_API_CHECK_STATUS(env, GetArgValue(env, info, &argValue)); + + napi_valuetype valueType; + NODE_API_CHECK_STATUS(env, napi_typeof(env, argValue, &valueType)); + NODE_API_ASSERT_STATUS( + env, valueType == napi_number, "Argument must be a number."); + + return napi_get_value_uint32(env, argValue, index); +} + +static napi_status GetRef(napi_env env, + napi_callback_info info, + napi_ref* ref) { + uint32_t index; + NODE_API_CHECK_STATUS(env, GetArgValueAsIndex(env, info, &index)); + + napi_ref* refValues; + NODE_API_CHECK_STATUS(env, napi_get_instance_data(env, (void**)&refValues)); + NODE_API_ASSERT_STATUS(env, refValues != NULL, "Cannot get instance data."); + + *ref = refValues[index]; + return napi_ok; +} + +static napi_value ToUInt32Value(napi_env env, uint32_t value) { + napi_value result; + NODE_API_CALL(env, napi_create_uint32(env, value, &result)); + return result; +} + +static napi_status InitRefArray(napi_env env) { + // valueRefs array has one entry per napi_valuetype + napi_ref* valueRefs = malloc(sizeof(napi_ref) * ((int)napi_bigint + 1)); + return napi_set_instance_data(env, valueRefs, &FreeData, NULL); +} + +static napi_value CreateExternal(napi_env env, napi_callback_info info) { + napi_value result; + int* data = (int*)malloc(sizeof(int)); + *data = 42; + NODE_API_CALL(env, napi_create_external(env, data, &FreeData, NULL, &result)); + return result; +} + +static napi_value CreateRef(napi_env env, napi_callback_info info) { + napi_value argValue; + NODE_API_CALL(env, GetArgValue(env, info, &argValue)); + + napi_valuetype valueType; + NODE_API_CALL(env, napi_typeof(env, argValue, &valueType)); + uint32_t index = (uint32_t)valueType; + + napi_ref* valueRefs; + NODE_API_CALL(env, napi_get_instance_data(env, (void**)&valueRefs)); + NODE_API_CALL(env, + napi_create_reference(env, argValue, 1, valueRefs + index)); + + return ToUInt32Value(env, index); +} + +static napi_value GetRefValue(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + napi_value value; + NODE_API_CALL(env, napi_get_reference_value(env, refValue, &value)); + return value; +} + +static napi_value Ref(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + uint32_t refCount; + NODE_API_CALL(env, napi_reference_ref(env, refValue, &refCount)); + return ToUInt32Value(env, refCount); +} + +static napi_value Unref(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + uint32_t refCount; + NODE_API_CALL(env, napi_reference_unref(env, refValue, &refCount)); + return ToUInt32Value(env, refCount); +} + +static napi_value DeleteRef(napi_env env, napi_callback_info info) { + napi_ref refValue; + NODE_API_CALL(env, GetRef(env, info, &refValue)); + NODE_API_CALL(env, napi_delete_reference(env, refValue)); + return NULL; +} + +static napi_value AddFinalizer(napi_env env, napi_callback_info info) { + napi_value obj; + NODE_API_CALL(env, GetArgValue(env, info, &obj)); + + napi_valuetype valueType; + NODE_API_CALL(env, napi_typeof(env, obj, &valueType)); + NODE_API_ASSERT(env, valueType == napi_object, "Argument must be an object."); + + NODE_API_CALL(env, napi_add_finalizer(env, obj, NULL, &Finalize, NULL, NULL)); + return NULL; +} + +static napi_value GetFinalizeCount(napi_env env, napi_callback_info info) { + return ToUInt32Value(env, finalizeCount); +} + +EXTERN_C_START + +NAPI_MODULE_INIT() { + finalizeCount = 0; + NODE_API_CALL(env, InitRefArray(env)); + + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("createExternal", CreateExternal), + DECLARE_NODE_API_PROPERTY("createRef", CreateRef), + DECLARE_NODE_API_PROPERTY("getRefValue", GetRefValue), + DECLARE_NODE_API_PROPERTY("ref", Ref), + DECLARE_NODE_API_PROPERTY("unref", Unref), + DECLARE_NODE_API_PROPERTY("deleteRef", DeleteRef), + DECLARE_NODE_API_PROPERTY("addFinalizer", AddFinalizer), + DECLARE_NODE_API_PROPERTY("getFinalizeCount", GetFinalizeCount), + }; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +EXTERN_C_END