From f489c6710c31e83fec6f57db5c9feaec45d605f4 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 PR-URL: https://github.com/nodejs/node/pull/45715 Reviewed-By: Gabriel Schulhof Reviewed-By: Michael Dawson Reviewed-By: Chengzhong Wu --- doc/api/n-api.md | 61 +++--- src/api/environment.cc | 30 ++- src/js_native_api_v8.cc | 31 ++- src/js_native_api_v8.h | 9 +- src/node.h | 8 +- src/node_api.cc | 76 +++++++- src/node_api.h | 10 + src/node_api_internals.h | 3 +- src/node_binding.cc | 13 +- src/node_binding.h | 17 +- src/node_version.h | 6 +- test/cctest/test_linked_binding.cc | 94 ++++++++- test/cctest/test_node_api.cc | 3 +- test/js-native-api/test_reference/test.js | 14 ++ .../binding.gyp | 14 ++ .../test.js | 124 ++++++++++++ .../test_reference_by_node_api_version.c | 180 ++++++++++++++++++ 17 files changed, 620 insertions(+), 73 deletions(-) create mode 100644 test/node-api/test_reference_by_node_api_version/binding.gyp create mode 100644 test/node-api/test_reference_by_node_api_version/test.js create mode 100644 test/node-api/test_reference_by_node_api_version/test_reference_by_node_api_version.c diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 42598c6fdbb5ef..77cc49946c4231 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -1639,25 +1639,36 @@ If it is called more than once an error will be returned. This API can be called even if there is a pending JavaScript exception. -### References to objects with a lifespan longer than that of the native method +### References to values with a lifespan longer than that of the native method -In some cases an addon will need to be able to create and reference objects +In some cases, an addon will need to be able to create and reference values with a lifespan longer than that of a single native method invocation. For example, to create a constructor and later use that constructor -in a request to creates instances, it must be possible to reference +in a request to create instances, it must be possible to reference the constructor object across many different instance creation requests. This would not be possible with a normal handle returned as a `napi_value` as described in the earlier section. The lifespan of a normal handle is managed by scopes and all scopes must be closed before the end of a native method. -Node-API provides methods to create persistent references to an object. -Each persistent reference has an associated count with a value of 0 -or higher. The count determines if the reference will keep -the corresponding object live. References with a count of 0 do not -prevent the object from being collected and are often called 'weak' -references. Any count greater than 0 will prevent the object -from being collected. +Node-API provides methods for creating persistent references to values. +Each reference has an associated count with a value of 0 or higher, +which determines whether the reference will keep the corresponding value alive. +References with a count of 0 do not prevent values from being collected. +Values of object (object, function, external) and symbol types are becoming +'weak' references and can still be accessed while they are not collected. +Values of other types are released when the count becomes 0 +and cannot be accessed from the reference any more. +Any count greater than 0 will prevent the values from being collected. + +Symbol values have different flavors. The true weak reference behavior is +only supported by local symbols created with the `Symbol()` constructor call. +Globally registered symbols created with the `Symbol.for()` call remain +always strong references because the garbage collector does not collect them. +The same is true for well-known symbols such as `Symbol.iterator`. They are +also never collected by the garbage collector. JavaScript's `WeakRef` and +`WeakMap` types return an error when registered symbols are used, +but they succeed for local and well-known symbols. References can be created with an initial reference count. The count can then be modified through [`napi_reference_ref`][] and @@ -1668,6 +1679,11 @@ will return `NULL` for the returned `napi_value`. An attempt to call [`napi_reference_ref`][] for a reference whose object has been collected results in an error. +Node-API versions 8 and earlier only allow references to be created for a +limited set of value types, including object, external, function, and symbol. +However, in newer Node-API versions, references can be created for any +value type. + References must be deleted once they are no longer required by the addon. When a reference is deleted, it will no longer prevent the corresponding object from being collected. Failure to delete a persistent reference results in @@ -1700,15 +1716,18 @@ NAPI_EXTERN napi_status napi_create_reference(napi_env env, ``` * `[in] env`: The environment that the API is invoked under. -* `[in] value`: `napi_value` representing the `Object` to which we want a - reference. +* `[in] value`: The `napi_value` for which a reference is being created. * `[in] initial_refcount`: Initial reference count for the new reference. * `[out] result`: `napi_ref` pointing to the new reference. Returns `napi_ok` if the API succeeded. This API creates a new reference with the specified reference count -to the `Object` passed in. +to the value passed in. + +In Node-API version 8 and earlier, a reference could only be created for +object, function, external, and symbol value types. However, in newer Node-API +versions, a reference can be created for any value type. #### `napi_delete_reference` @@ -1787,18 +1806,15 @@ NAPI_EXTERN napi_status napi_get_reference_value(napi_env env, napi_value* result); ``` -the `napi_value passed` in or out of these methods is a handle to the -object to which the reference is related. - * `[in] env`: The environment that the API is invoked under. -* `[in] ref`: `napi_ref` for which we requesting the corresponding `Object`. -* `[out] result`: The `napi_value` for the `Object` referenced by the - `napi_ref`. +* `[in] ref`: The `napi_ref` for which the corresponding value is + being requested. +* `[out] result`: The `napi_value` referenced by the `napi_ref`. Returns `napi_ok` if the API succeeded. If still valid, this API returns the `napi_value` representing the -JavaScript `Object` associated with the `napi_ref`. Otherwise, result +JavaScript value associated with the `napi_ref`. Otherwise, result will be `NULL`. ### Cleanup on exit of the current Node.js environment @@ -5069,9 +5085,8 @@ napi_status napi_define_class(napi_env env, ``` * `[in] env`: The environment that the API is invoked under. -* `[in] utf8name`: Name of the JavaScript constructor function; When wrapping a - C++ class, we recommend for clarity that this name be the same as that of - the C++ class. +* `[in] utf8name`: Name of the JavaScript constructor function. For clarity, + it is recommended to use the C++ class name when wrapping a C++ class. * `[in] length`: The length of the `utf8name` in bytes, or `NAPI_AUTO_LENGTH` if it is null-terminated. * `[in] constructor`: Callback function that handles constructing instances diff --git a/src/api/environment.cc b/src/api/environment.cc index cfc3516d61f78a..2128ca6c4ebb75 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -872,26 +872,18 @@ void AddLinkedBinding(Environment* env, void AddLinkedBinding(Environment* env, const char* name, - napi_addon_register_func fn) { + napi_addon_register_func fn, + int32_t module_api_version) { node_module mod = { - -1, - NM_F_LINKED, - nullptr, // nm_dso_handle - nullptr, // nm_filename - nullptr, // nm_register_func - [](v8::Local exports, - v8::Local module, - v8::Local context, - void* priv) { - napi_module_register_by_symbol( - exports, - module, - context, - reinterpret_cast(priv)); - }, - name, - reinterpret_cast(fn), - nullptr // nm_link + -1, // nm_version for Node-API + NM_F_LINKED, // nm_flags + nullptr, // nm_dso_handle + nullptr, // nm_filename + nullptr, // nm_register_func + get_node_api_context_register_func(env, name, module_api_version), + name, // nm_modname + reinterpret_cast(fn), // nm_priv + nullptr // nm_link }; AddLinkedBinding(env, mod); } diff --git a/src/js_native_api_v8.cc b/src/js_native_api_v8.cc index f4df74299781c8..9cbcd2445d1980 100644 --- a/src/js_native_api_v8.cc +++ b/src/js_native_api_v8.cc @@ -457,6 +457,18 @@ inline napi_status Wrap(napi_env env, return GET_RETURN_STATUS(env); } +// In JavaScript, weak references can be created for object types (Object, +// Function, and external Object) and for local symbols that are created with +// the `Symbol` function call. Global symbols created with the `Symbol.for` +// method cannot be weak references because they are never collected. +// +// Currently, V8 has no API to detect if a symbol is local or global. +// Until we have a V8 API for it, we consider that all symbols can be weak. +// This matches the current Node-API behavior. +inline bool CanBeHeldWeakly(v8::Local value) { + return value->IsObject() || value->IsSymbol(); +} + } // end of anonymous namespace void Finalizer::ResetFinalizer() { @@ -551,7 +563,8 @@ void RefBase::Finalize() { template Reference::Reference(napi_env env, v8::Local value, Args&&... args) : RefBase(env, std::forward(args)...), - persistent_(env->isolate, value) { + persistent_(env->isolate, value), + can_be_weak_(CanBeHeldWeakly(value)) { if (RefCount() == 0) { SetWeak(); } @@ -585,7 +598,7 @@ uint32_t Reference::Ref() { return 0; } uint32_t refcount = RefBase::Ref(); - if (refcount == 1) { + if (refcount == 1 && can_be_weak_) { persistent_.ClearWeak(); } return refcount; @@ -625,7 +638,11 @@ void Reference::Finalize() { // Mark the reference as weak and eligible for collection // by the gc. void Reference::SetWeak() { - persistent_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); + if (can_be_weak_) { + persistent_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); + } else { + persistent_.Reset(); + } } // The N-API finalizer callback may make calls into the engine. V8's heap is @@ -2419,9 +2436,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 6378a055208ef9..2f4705dce67713 100644 --- a/src/js_native_api_v8.h +++ b/src/js_native_api_v8.h @@ -51,8 +51,11 @@ 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) { napi_clear_last_error(this); } @@ -144,6 +147,7 @@ struct napi_env__ { int open_callback_scopes = 0; int refs = 1; void* instance_data = nullptr; + int32_t module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION; protected: // Should not be deleted directly. Delete with `napi_env__::DeleteMe()` @@ -419,6 +423,7 @@ class Reference : public RefBase { void SetWeak(); v8impl::Persistent persistent_; + bool can_be_weak_; }; } // end of namespace v8impl diff --git a/src/node.h b/src/node.h index b484df642db1d8..c71a4a7b76039d 100644 --- a/src/node.h +++ b/src/node.h @@ -1237,9 +1237,11 @@ NODE_EXTERN void AddLinkedBinding(Environment* env, const char* name, addon_context_register_func fn, void* priv); -NODE_EXTERN void AddLinkedBinding(Environment* env, - const char* name, - napi_addon_register_func fn); +NODE_EXTERN void AddLinkedBinding( + Environment* env, + const char* name, + napi_addon_register_func fn, + int32_t module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION); /* Registers a callback with the passed-in Environment instance. The callback * is called after the event loop exits, but before the VM is disposed. diff --git a/src/node_api.cc b/src/node_api.cc index 096a6e580fb9e8..efe1d09ed8ed0c 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()); } @@ -151,11 +152,36 @@ class BufferFinalizer : private Finalizer { ~BufferFinalizer() { env_->Unref(); } }; +void ThrowNodeApiVersionError(node::Environment* node_env, + const char* module_name, + int32_t module_api_version) { + std::string error_message; + error_message += module_name; + error_message += " requires Node-API version "; + error_message += std::to_string(module_api_version); + error_message += ", but this version of Node.js only supports version "; + error_message += NODE_STRINGIFY(NAPI_VERSION) " add-ons."; + node_env->ThrowError(error_message.c_str()); +} + 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); + // Validate module_api_version. + if (module_api_version < NODE_API_DEFAULT_MODULE_API_VERSION) { + module_api_version = NODE_API_DEFAULT_MODULE_API_VERSION; + } else if (module_api_version > NAPI_VERSION && + module_api_version != NAPI_VERSION_EXPERIMENTAL) { + node::Environment* node_env = node::Environment::GetCurrent(context); + CHECK_NOT_NULL(node_env); + ThrowNodeApiVersionError( + node_env, module_filename.c_str(), module_api_version); + return nullptr; + } + + 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,10 +649,48 @@ static void napi_module_register_cb(v8::Local exports, static_cast(priv)->nm_register_func); } +template +static void node_api_context_register_func(v8::Local exports, + v8::Local module, + v8::Local context, + void* priv) { + napi_module_register_by_symbol( + exports, + module, + context, + reinterpret_cast(priv), + module_api_version); +} + +// This function must be augmented for each new Node API version. +// The key role of this function is to encode module_api_version in the function +// pointer. We are not going to have many Node API versions and having one +// function per version is relatively cheap. It avoids dynamic memory +// allocations or implementing more expensive changes to module registration. +// Currently AddLinkedBinding is the only user of this function. +node::addon_context_register_func get_node_api_context_register_func( + node::Environment* node_env, + const char* module_name, + int32_t module_api_version) { + static_assert( + NAPI_VERSION == 8, + "New version of Node-API requires adding another else-if statement below " + "for the new version and updating this assert condition."); + if (module_api_version <= NODE_API_DEFAULT_MODULE_API_VERSION) { + return node_api_context_register_func; + } else if (module_api_version == NAPI_VERSION_EXPERIMENTAL) { + return node_api_context_register_func; + } else { + v8impl::ThrowNodeApiVersionError(node_env, module_name, module_api_version); + return nullptr; + } +} + 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) { @@ -654,7 +718,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..4c0356eaccb2fc 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* node_api_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 NODE_API_MODULE_GET_API_VERSION_BASE node_api_module_get_api_version_v + #define NAPI_MODULE_INITIALIZER \ NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION) +#define NODE_API_MODULE_GET_API_VERSION \ + NAPI_MODULE_INITIALIZER_X(NODE_API_MODULE_GET_API_VERSION_BASE, \ + NAPI_MODULE_VERSION) + #define NAPI_MODULE_INIT() \ EXTERN_C_START \ + NAPI_MODULE_EXPORT int32_t NODE_API_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 5201966b779508..25f6b291902024 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; void CallFinalizer(napi_finalize cb, void* data, void* hint) override; diff --git a/src/node_binding.cc b/src/node_binding.cc index 90855aada5dab9..cb27a497a3b6d1 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 node_api_addon_get_api_version_func GetNapiAddonGetApiVersionCallback( + DLib* dlib) { + return reinterpret_cast( + dlib->GetSymbolAddress(STRINGIFY(NODE_API_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 = NODE_API_DEFAULT_MODULE_API_VERSION; + 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 d6e59d19ea4652..f04be60c3890f0 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 @@ -57,10 +57,17 @@ 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); +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_API_DEFAULT_MODULE_API_VERSION); + +node::addon_context_register_func get_node_api_context_register_func( + node::Environment* node_env, + const char* module_name, + int32_t module_api_version); namespace node { diff --git a/src/node_version.h b/src/node_version.h index 65bcabd9f5f99d..75f5ea1a85255d 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 + +// Node API modules use NAPI_VERSION 8 by default if it is not explicitly +// specified. It must be always 8. +#define NODE_API_DEFAULT_MODULE_API_VERSION 8 #endif // SRC_NODE_VERSION_H_ diff --git a/test/cctest/test_linked_binding.cc b/test/cctest/test_linked_binding.cc index 1a4e6a838b5d72..6f2efda77268c5 100644 --- a/test/cctest/test_linked_binding.cc +++ b/test/cctest/test_linked_binding.cc @@ -132,11 +132,15 @@ TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiCallbackTest) { const Argv argv; Env test_env{handle_scope, argv}; - AddLinkedBinding(*test_env, "local_linked_napi", InitializeLocalNapiBinding); + AddLinkedBinding(*test_env, + "local_linked_napi_cb", + InitializeLocalNapiBinding, + NAPI_VERSION); v8::Local context = isolate_->GetCurrentContext(); - const char* run_script = "process._linkedBinding('local_linked_napi').hello"; + const char* run_script = + "process._linkedBinding('local_linked_napi_cb').hello"; v8::Local script = v8::Script::Compile( context, @@ -150,6 +154,84 @@ TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiCallbackTest) { CHECK_EQ(strcmp(*utf8val, "world"), 0); } +static int32_t node_api_version = NODE_API_DEFAULT_MODULE_API_VERSION; + +napi_value InitializeLocalNapiRefBinding(napi_env env, napi_value exports) { + napi_value key, value; + CHECK_EQ( + napi_create_string_utf8(env, "napi_ref_created", NAPI_AUTO_LENGTH, &key), + napi_ok); + + // In experimental Node-API version we can create napi_ref to any value type. + // Here we are trying to create a reference to the key string. + napi_ref ref{}; + if (node_api_version == NAPI_VERSION_EXPERIMENTAL) { + CHECK_EQ(napi_create_reference(env, key, 1, &ref), napi_ok); + CHECK_EQ(napi_delete_reference(env, ref), napi_ok); + } else { + CHECK_EQ(napi_create_reference(env, key, 1, &ref), napi_invalid_arg); + } + CHECK_EQ(napi_get_boolean(env, ref != nullptr, &value), napi_ok); + CHECK_EQ(napi_set_property(env, exports, key, value), napi_ok); + return nullptr; +} + +// napi_ref in Node-API version 8 cannot accept strings. +TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiRefVersion8Test) { + node_api_version = NODE_API_DEFAULT_MODULE_API_VERSION; + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + + AddLinkedBinding(*test_env, + "local_linked_napi_ref_v8", + InitializeLocalNapiRefBinding, + node_api_version); + + v8::Local context = isolate_->GetCurrentContext(); + + const char* run_script = + "process._linkedBinding('local_linked_napi_ref_v8').napi_ref_created"; + v8::Local script = + v8::Script::Compile( + context, + v8::String::NewFromOneByte( + isolate_, reinterpret_cast(run_script)) + .ToLocalChecked()) + .ToLocalChecked(); + v8::Local completion_value = script->Run(context).ToLocalChecked(); + CHECK(completion_value->IsBoolean()); + CHECK(!completion_value.As()->Value()); +} + +// Experimental version of napi_ref in Node-API can accept strings. +TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiRefExperimentalTest) { + node_api_version = NAPI_VERSION_EXPERIMENTAL; + const v8::HandleScope handle_scope(isolate_); + const Argv argv; + Env test_env{handle_scope, argv}; + + AddLinkedBinding(*test_env, + "local_linked_napi_ref_experimental", + InitializeLocalNapiRefBinding, + node_api_version); + + v8::Local context = isolate_->GetCurrentContext(); + + const char* run_script = "process._linkedBinding('local_linked_napi_ref_" + "experimental').napi_ref_created"; + v8::Local script = + v8::Script::Compile( + context, + v8::String::NewFromOneByte( + isolate_, reinterpret_cast(run_script)) + .ToLocalChecked()) + .ToLocalChecked(); + v8::Local completion_value = script->Run(context).ToLocalChecked(); + CHECK(completion_value->IsBoolean()); + CHECK(completion_value.As()->Value()); +} + napi_value NapiLinkedWithInstanceData(napi_env env, napi_value exports) { int* instance_data = new int(0); CHECK_EQ(napi_set_instance_data( @@ -223,13 +305,15 @@ TEST_F(LinkedBindingTest, const Argv argv; Env test_env{handle_scope, argv}; - AddLinkedBinding( - *test_env, "local_linked_napi_id", NapiLinkedWithInstanceData); + AddLinkedBinding(*test_env, + "local_linked_napi_id_cb", + NapiLinkedWithInstanceData, + NAPI_VERSION); v8::Local context = isolate_->GetCurrentContext(); const char* run_script = - "process._linkedBinding('local_linked_napi_id').hello"; + "process._linkedBinding('local_linked_napi_id_cb').hello"; v8::Local script = v8::Script::Compile( context, diff --git a/test/cctest/test_node_api.cc b/test/cctest/test_node_api.cc index ad5be52fc8ffcc..8921b9d8d373db 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, NAPI_VERSION); 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/js-native-api/test_reference/test.js b/test/js-native-api/test_reference/test.js index 6f128b788706cd..e65847fbdb0596 100644 --- a/test/js-native-api/test_reference/test.js +++ b/test/js-native-api/test_reference/test.js @@ -25,6 +25,13 @@ async function runTests() { const symbol = test_reference.createSymbolFor('testSymFor'); test_reference.createReference(symbol, 0); assert.strictEqual(test_reference.referenceValue, symbol); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolFor('testSymFor'); + test_reference.createReference(symbol, 1); + assert.strictEqual(test_reference.referenceValue, symbol); assert.strictEqual(test_reference.referenceValue, Symbol.for('testSymFor')); })(); test_reference.deleteReference(); @@ -32,6 +39,13 @@ async function runTests() { (() => { const symbol = test_reference.createSymbolForEmptyString(); test_reference.createReference(symbol, 0); + assert.strictEqual(test_reference.referenceValue, Symbol.for('')); + })(); + test_reference.deleteReference(); + + (() => { + const symbol = test_reference.createSymbolForEmptyString(); + test_reference.createReference(symbol, 1); assert.strictEqual(test_reference.referenceValue, symbol); assert.strictEqual(test_reference.referenceValue, Symbol.for('')); })(); diff --git a/test/node-api/test_reference_by_node_api_version/binding.gyp b/test/node-api/test_reference_by_node_api_version/binding.gyp new file mode 100644 index 00000000000000..2ee1d24763b0b3 --- /dev/null +++ b/test/node-api/test_reference_by_node_api_version/binding.gyp @@ -0,0 +1,14 @@ +{ + "targets": [ + { + "target_name": "test_reference_all_types", + "sources": [ "test_reference_by_node_api_version.c" ], + "defines": [ "NAPI_EXPERIMENTAL" ], + }, + { + "target_name": "test_reference_obj_only", + "sources": [ "test_reference_by_node_api_version.c" ], + "defines": [ "NAPI_VERSION=8" ], + } + ] +} diff --git a/test/node-api/test_reference_by_node_api_version/test.js b/test/node-api/test_reference_by_node_api_version/test.js new file mode 100644 index 00000000000000..32530f681508c8 --- /dev/null +++ b/test/node-api/test_reference_by_node_api_version/test.js @@ -0,0 +1,124 @@ +'use strict'; +// Flags: --expose-gc +// +// Testing API calls for Node-API references. +// We compare their behavior between Node-API version 8 and later. +// In version 8 references can be created only for object, function, +// and symbol types, while in newer versions they can be created for +// any value type. +// +const { gcUntil, buildType } = require('../../common'); +const assert = require('assert'); +const addon_v8 = require(`./build/${buildType}/test_reference_obj_only`); +const addon_new = require(`./build/${buildType}/test_reference_all_types`); + +async function runTests(addon, isVersion8, isLocalSymbol) { + 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 globalSymbolValue = Symbol.for('test_symbol_global'); + const localSymbolValue = Symbol('test_symbol_local'); + const symbolValue = isLocalSymbol ? localSymbolValue : globalSymbolValue; + const objectValue = { x: 1, y: 2 }; + const functionValue = (x, y) => x + y; + const externalValue = addon.createExternal(); + const bigintValue = 9007199254740991n; + + // The position of entries in the allEntries array corresponds to the + // napi_valuetype enum value. See the CreateRef function for the + // implementation details. + allEntries = [ + { value: undefinedValue, canBeWeak: false, canBeRefV8: false }, + { value: nullValue, canBeWeak: false, canBeRefV8: false }, + { value: booleanValue, canBeWeak: false, canBeRefV8: false }, + { value: numberValue, canBeWeak: false, canBeRefV8: false }, + { value: stringValue, canBeWeak: false, canBeRefV8: false }, + { value: symbolValue, canBeWeak: isLocalSymbol, canBeRefV8: true, + isAlwaysStrong: !isLocalSymbol }, + { value: objectValue, canBeWeak: true, canBeRefV8: true }, + { value: functionValue, canBeWeak: true, canBeRefV8: true }, + { value: externalValue, canBeWeak: true, canBeRefV8: true }, + { value: bigintValue, canBeWeak: false, canBeRefV8: 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 (!isVersion8 || entry.canBeRefV8) { + 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', + }); + } + } + + // When the reference count is zero, then object types become weak pointers + // and other types are released. + // Here we know that the GC is not run yet because the values are + // still in the allEntries array. + allEntries.forEach((entry, index) => { + if (!isVersion8 || entry.canBeRefV8) { + if (entry.canBeWeak || entry.isAlwaysStrong) { + assert.strictEqual(addon.getRefValue(index), entry.value); + } else { + assert.strictEqual(addon.getRefValue(index), undefined); + } + } + // Set to undefined to allow GC collect the value. + entry.value = undefined; + }); + + // To check that GC pass is done. + const objWithFinalizer = {}; + addon.addFinalizer(objWithFinalizer); + })(); + + addon.initFinalizeCount(); + 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. + allEntries.forEach((entry, index) => { + if (!isVersion8 || entry.canBeRefV8) { + if (!entry.isAlwaysStrong) { + assert.strictEqual(addon.getRefValue(index), undefined); + } else { + assert.notStrictEqual(addon.getRefValue(index), undefined); + } + addon.deleteRef(index); + } + }); +} + +async function runAllTests() { + await runTests(addon_v8, /* isVersion8 */ true, /* isLocalSymbol */ true); + await runTests(addon_v8, /* isVersion8 */ true, /* isLocalSymbol */ false); + await runTests(addon_new, /* isVersion8 */ false, /* isLocalSymbol */ true); + await runTests(addon_new, /* isVersion8 */ false, /* isLocalSymbol */ false); +} + +runAllTests(); diff --git a/test/node-api/test_reference_by_node_api_version/test_reference_by_node_api_version.c b/test/node-api/test_reference_by_node_api_version/test_reference_by_node_api_version.c new file mode 100644 index 00000000000000..23e5b1988b441c --- /dev/null +++ b/test/node-api/test_reference_by_node_api_version/test_reference_by_node_api_version.c @@ -0,0 +1,180 @@ +#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); +} + +static napi_value InitFinalizeCount(napi_env env, napi_callback_info info) { + finalizeCount = 0; + return NULL; +} + +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), + DECLARE_NODE_API_PROPERTY("initFinalizeCount", InitFinalizeCount), + }; + + NODE_API_CALL( + env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +EXTERN_C_END