From 0a8d5238d44c553f59b97558a232ac2fe52a79c3 Mon Sep 17 00:00:00 2001 From: Vladimir Morozov Date: Fri, 16 Dec 2022 07:57:55 -0800 Subject: [PATCH] update API and add tests --- src/js_native_api_v8.cc | 2 +- src/js_native_api_v8.h | 8 +- src/node_api.cc | 29 ++- src/node_api.h | 105 +++++++++-- src/node_api_internals.h | 2 +- src/node_binding.cc | 20 +- src/node_binding.h | 13 +- test/cctest/test_node_api.cc | 2 +- test/node-api/test_null_init/binding.gyp | 8 + test/node-api/test_null_init/test.js | 7 + test/node-api/test_null_init/test_null_init.c | 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 ++++++++++++++++++ 17 files changed, 703 insertions(+), 60 deletions(-) create mode 100644 test/node-api/test_null_init/binding.gyp create mode 100644 test/node-api/test_null_init/test.js create mode 100644 test/node-api/test_null_init/test_null_init.c 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/js_native_api_v8.cc b/src/js_native_api_v8.cc index 8100b00f564d87..4186ec02f5231c 100644 --- a/src/js_native_api_v8.cc +++ b/src/js_native_api_v8.cc @@ -2419,7 +2419,7 @@ napi_status NAPI_CDECL napi_create_reference(napi_env env, CHECK_ARG(env, result); v8::Local v8_value = v8impl::V8LocalValueFromJsValue(value); - if (env->node_api_version <= 8) { + 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); diff --git a/src/js_native_api_v8.h b/src/js_native_api_v8.h index a6c9da40e732c7..0355acba33686b 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -52,10 +52,12 @@ class Finalizer; struct napi_env__ { explicit napi_env__(v8::Local context, - int32_t node_api_version) + int32_t module_api_version) : isolate(context->GetIsolate()), context_persistent(isolate, context), - node_api_version(node_api_version) { + module_api_version(module_api_version != 0 + ? module_api_version + : NAPI_DEFAULT_MODULE_API_VERSION) { napi_clear_last_error(this); } @@ -151,7 +153,7 @@ struct napi_env__ { int open_callback_scopes = 0; int refs = 1; void* instance_data = nullptr; - int node_api_version = NAPI_DEFAULT_MODULE_API_VERSION; + 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 4720f52fdc01e4..3d208ac7d668b9 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -21,8 +21,8 @@ node_napi_env__::node_napi_env__(v8::Local context, const std::string& module_filename, - int32_t node_api_version) - : napi_env__(context, node_api_version), filename(module_filename) { + int32_t module_api_version) + : napi_env__(context, module_api_version), filename(module_filename) { CHECK_NOT_NULL(node_env()); } @@ -161,10 +161,10 @@ class BufferFinalizer : private Finalizer { inline napi_env NewEnv(v8::Local context, const std::string& module_filename, - int32_t node_api_version) { + int32_t module_api_version) { node_napi_env result; - result = new node_napi_env__(context, module_filename, node_api_version); + 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, @@ -630,15 +630,15 @@ static void napi_module_register_cb(v8::Local exports, module, context, static_cast(priv)->nm_register_func, - static_cast(priv)->nm_api_version_func); + static_cast( + static_cast(priv)->nm_api_version)); } -void napi_module_register_by_symbol( - v8::Local exports, - v8::Local module, - v8::Local context, - napi_addon_register_func init, - napi_addon_api_version_func get_api_version) { +void napi_module_register_by_symbol(v8::Local exports, + v8::Local module, + v8::Local context, + 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) { @@ -647,11 +647,6 @@ void napi_module_register_by_symbol( return; } - int32_t node_api_version = NAPI_DEFAULT_MODULE_API_VERSION; - if (get_api_version != nullptr) { - node_api_version = get_api_version(); - } - // We set `env->filename` from `module.filename` here, but we could just as // easily add a private property to `exports` in `process.dlopen`, which // receives the file name from JS, and retrieve *that* here. Thus, we are not @@ -671,7 +666,7 @@ void napi_module_register_by_symbol( } // Create a new napi_env for this specific module. - napi_env env = v8impl::NewEnv(context, module_filename, node_api_version); + 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 567b68ae1e07bf..2512cd0c5a4e3b 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -31,7 +31,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_api_version_func)(); +#ifndef NAPI_EXPERIMENTAL typedef struct napi_module { int nm_version; @@ -40,10 +40,26 @@ typedef struct napi_module { napi_addon_register_func nm_register_func; const char* nm_modname; void* nm_priv; - napi_addon_api_version_func nm_api_version_func; + void* reserved[4]; +} napi_module; + +#else + +typedef int32_t(NAPI_CDECL* napi_addon_get_api_version_func)(); + +typedef struct napi_module { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + uintptr_t nm_api_version; void* reserved[3]; } napi_module; +#endif // NAPI_EXPERIMENTAL + #define NAPI_MODULE_VERSION 1 #if defined(_MSC_VER) @@ -76,7 +92,57 @@ typedef struct napi_module { static void fn(void) #endif -#define NAPI_MODULE_XV(modname, regfunc, apiversionfunc, priv, flags) \ +#ifndef NAPI_EXPERIMENTAL + +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + EXTERN_C_START \ + static napi_module _module = { \ + NAPI_MODULE_VERSION, \ + flags, \ + __FILE__, \ + regfunc, \ + #modname, \ + priv, \ + {0}, \ + }; \ + NAPI_C_CTOR(_register_##modname) { napi_module_register(&_module); } \ + EXTERN_C_END + +#define NAPI_MODULE_INITIALIZER_X(base, version) \ + NAPI_MODULE_INITIALIZER_X_HELPER(base, version) +#define NAPI_MODULE_INITIALIZER_X_HELPER(base, version) base##version + +#ifdef __wasm32__ +#define NAPI_WASM_INITIALIZER \ + NAPI_MODULE_INITIALIZER_X(napi_register_wasm_v, NAPI_MODULE_VERSION) +#define NAPI_MODULE(modname, regfunc) \ + EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value NAPI_WASM_INITIALIZER(napi_env env, \ + napi_value exports) { \ + return regfunc(env, exports); \ + } \ + EXTERN_C_END +#else +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_X(modname, regfunc, NULL, 0) // NOLINT (readability/null_usage) +#endif + +#define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v + +#define NAPI_MODULE_INITIALIZER \ + NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) + +#define NAPI_MODULE_INIT() \ + EXTERN_C_START \ + NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ + napi_value exports); \ + EXTERN_C_END \ + NAPI_MODULE(NODE_GYP_MODULE_NAME, NAPI_MODULE_INITIALIZER) \ + napi_value NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports) + +#else + +#define NAPI_MODULE_XV(modname, regfunc, api_version, priv, flags) \ EXTERN_C_START \ static napi_module _module = { \ NAPI_MODULE_VERSION, \ @@ -85,64 +151,63 @@ typedef struct napi_module { regfunc, \ #modname, \ priv, \ - apiversionfunc, \ + (uintptr_t)api_version, \ {0}, \ }; \ NAPI_C_CTOR(_register_##modname) { napi_module_register(&_module); } \ EXTERN_C_END +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + NAPI_MODULE_XV(modname, regfunc, NAPI_VERSION, priv, flags) + #define NAPI_CONCAT_HELPER(text1, text2) text1##text2 #define NAPI_CONCAT(text1, text2) NAPI_CONCAT_HELPER(text1, text2) #define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v -#define NAPI_MODULE_API_VERSION_BASE napi_module_api_version_v +#define NAPI_MODULE_GET_API_VERSION_BASE napi_module_get_api_version_v #define NAPI_MODULE_INITIALIZER \ NAPI_CONCAT(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) -#define NAPI_MODULE_API_VERSION \ - NAPI_CONCAT(NAPI_MODULE_API_VERSION_BASE, NAPI_MODULE_VERSION) - -#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ - EXTERN_C_START \ - NAPI_MODULE_EXPORT int32_t NAPI_MODULE_API_VERSION() { \ - return NAPI_VERSION; \ - } \ - EXTERN_C_END \ - NAPI_MODULE_XV(modname, regfunc, NAPI_MODULE_API_VERSION, priv, flags) +#define NAPI_MODULE_GET_API_VERSION \ + NAPI_CONCAT(NAPI_MODULE_GET_API_VERSION_BASE, NAPI_MODULE_VERSION) #define NAPI_MODULE_INIT() \ EXTERN_C_START \ NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \ napi_value exports); \ - NAPI_MODULE_EXPORT int32_t NAPI_MODULE_API_VERSION() { \ + NAPI_MODULE_EXPORT int32_t NAPI_MODULE_GET_API_VERSION() { \ return NAPI_VERSION; \ } \ EXTERN_C_END \ napi_value NAPI_MODULE_INITIALIZER(napi_env env, napi_value exports) #ifdef __wasm32__ + #define NAPI_WASM_INITIALIZER \ NAPI_CONCAT(napi_register_wasm_v, NAPI_MODULE_VERSION) -#define NAPI_WASM_API_VERSION \ - NAPI_CONCAT(napi_wasm_api_version_v, NAPI_MODULE_VERSION) +#define NAPI_WASM_GET_API_VERSION \ + NAPI_CONCAT(napi_wasm_get_api_version_v, NAPI_MODULE_VERSION) #define NAPI_MODULE(modname, regfunc) \ EXTERN_C_START \ NAPI_MODULE_EXPORT napi_value NAPI_WASM_INITIALIZER(napi_env env, \ napi_value exports) { \ return regfunc(env, exports); \ } \ - NAPI_MODULE_EXPORT int32_t NAPI_WASM_API_VERSION() { \ + NAPI_MODULE_EXPORT int32_t NAPI_WASM_GET_API_VERSION() { \ return NAPI_VERSION; \ } \ EXTERN_C_END #else + #define NAPI_MODULE(modname, regfunc) \ NAPI_MODULE_INIT() { \ return regfunc(env, exports); \ } -#endif +#endif // __wasm32__ + +#endif // NAPI_EXPERIMENTAL EXTERN_C_START diff --git a/src/node_api_internals.h b/src/node_api_internals.h index b3a3d20c85a3ea..6ac80a8b2b5893 100644 --- a/src/node_api_internals.h +++ b/src/node_api_internals.h @@ -11,7 +11,7 @@ struct node_napi_env__ : public napi_env__ { node_napi_env__(v8::Local context, const std::string& module_filename, - int32_t node_api_version); + 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 72568f19eb55b2..bf1dbbd205af64 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -411,11 +411,10 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) { dlib->GetSymbolAddress(name)); } -inline napi_addon_api_version_func GetNapiAddonApiVersionCallback(DLib* dlib) { - const char* name = - STRINGIFY(NAPI_MODULE_API_VERSION_BASE) STRINGIFY(NAPI_MODULE_VERSION); - return reinterpret_cast( - 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). @@ -494,11 +493,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, - GetNapiAddonApiVersionCallback(dlib)); + 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 7621310c1849fc..d60a1f9ffaf7c2 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"); #define NODE_BINDINGS_WITH_PER_ISOLATE_INIT(V) \ @@ -41,12 +41,11 @@ static_assert(static_cast(NM_F_LINKED) == nullptr}; \ void _register_##modname() { node_module_register(&_module); } -void napi_module_register_by_symbol( - v8::Local exports, - v8::Local module, - v8::Local context, - napi_addon_register_func init, - napi_addon_api_version_func get_api_version); +void napi_module_register_by_symbol(v8::Local exports, + v8::Local module, + v8::Local context, + napi_addon_register_func init, + int32_t module_api_version); namespace node { diff --git a/test/cctest/test_node_api.cc b/test/cctest/test_node_api.cc index 2b1ec8eabef837..261e3e368c1012 100644 --- a/test/cctest/test_node_api.cc +++ b/test/cctest/test_node_api.cc @@ -35,7 +35,7 @@ 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, nullptr); + 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_null_init/binding.gyp b/test/node-api/test_null_init/binding.gyp new file mode 100644 index 00000000000000..27701616e338db --- /dev/null +++ b/test/node-api/test_null_init/binding.gyp @@ -0,0 +1,8 @@ +{ + 'targets': [ + { + 'target_name': 'test_null_init', + 'sources': [ 'test_null_init.c' ] + } + ] +} diff --git a/test/node-api/test_null_init/test.js b/test/node-api/test_null_init/test.js new file mode 100644 index 00000000000000..6e6bf51839bed0 --- /dev/null +++ b/test/node-api/test_null_init/test.js @@ -0,0 +1,7 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +assert.throws( + () => require(`./build/${common.buildType}/test_null_init`), + /Module has no declared entry point[.]/); diff --git a/test/node-api/test_null_init/test_null_init.c b/test/node-api/test_null_init/test_null_init.c new file mode 100644 index 00000000000000..d9d2200488ce41 --- /dev/null +++ b/test/node-api/test_null_init/test_null_init.c @@ -0,0 +1,3 @@ +#include + +NAPI_MODULE(NODE_GYP_MODULE_NAME, NULL) 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..bcfc301680211d --- /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