Sitelet https://github.com/nodejs/node/pull/42557/commits/579d44cfbe994499f72308c8893829cea5b9b720
Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
node-api: enable napi_ref for all value types
  • Loading branch information
vmoroz committed Oct 28, 2022
commit 579d44cfbe994499f72308c8893829cea5b9b720
68 changes: 68 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,48 @@ the `napi_value` in question is of the JavaScript type expected by the API.

### Enum types

#### `napi_features`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

```c
typedef enum {
napi_feature_none = 0,
napi_feature_reference_all_types = 1 << 0,

napi_default_experimental_features = napi_feature_reference_all_types,

napi_default_features = napi_default_experimental_features, // version specific
} napi_features;
```

The `napi_features` allow changing internal behavior of existing Node-API
functions.

We pass a `napi_features` pointer to the `napi_module` struct in the
`NAPI_MODULE_X` macro. This macro is used for the module registration.
If the module is initialized without using this macro, then there will be
no features selected and the module will use the `napi_feature_none`.

Each Node-API version defines its own default set of features.
For the current version it can be accessed using `napi_default_features`.
A module can override the set of its enabled features by adding
`NAPI_CUSTOM_FEATURES` definition to the `.gyp` file and then defining the
value of the global `napi_module_features` variable.
To check enabled features use the `napi_is_feature_enabled` function.

For example, to disables `napi_feature_reference_all_types` feature we can
exclude its bit from the `napi_default_features` set:

```c
napi_features napi_module_features =
napi_default_features & ~napi_feature_reference_all_types;
```

#### `napi_key_collection_mode`

<!-- YAML
Expand Down Expand Up @@ -5700,6 +5742,32 @@ support it:
* If the function is not available, provide an alternate implementation
that does not use the function.

#### `napi_is_feature_enabled`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

```c
NAPI_EXTERN napi_status napi_is_feature_enabled(napi_env env,
napi_features feature,
bool* result);
```

* `[in] env`: The environment that the API is invoked under.
* `[in] feature`: The feature that we want to test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `[in] feature`: The feature that we want to test.
* `[in] feature`: The features that we want to test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed. Though I am not sure why we need to use plural form, while the parameter is singular. Should I also rename the parameter?

* `[out] result`: Whether the feature or a set of features are enabled.

Returns `napi_ok` if the API succeeded.

The function checks enabled features for the module.
If `feature` parameter has multiple `napi_features` bit flags, then the
function returns `true` only when all the requested fatures are enabled.

See [`napi_features`][] for more details about Node-API features.

## Memory management

### `napi_adjust_external_memory`
Expand Down
5 changes: 5 additions & 0 deletions src/js_native_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,11 @@ NAPI_EXTERN napi_status NAPI_CDECL napi_object_seal(napi_env env,
napi_value object);
#endif // NAPI_VERSION >= 8

#ifdef NAPI_EXPERIMENTAL
NAPI_EXTERN napi_status NAPI_CDECL
napi_is_feature_enabled(napi_env env, napi_features feature, bool* result);
#endif // NAPI_EXPERIMENTAL

EXTERN_C_END

#endif // SRC_JS_NATIVE_API_H_
31 changes: 31 additions & 0 deletions src/js_native_api_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,37 @@ typedef enum {
// * the definition of `napi_status` in doc/api/n-api.md to reflect the newly
// added value(s).

#ifdef NAPI_EXPERIMENTAL
// Features allow changing internal behavior of existing Node-API functions.
//
// We pass a napi_features pointer to the napi_module struct
// in the NAPI_MODULE_X macro. This macro is used for the module registration.
// If the module is initialized without using this macro, then there will be
// no features selected and the module will use the napi_feature_none.
//
// Each Node-API version defines its own default set of features.
// For the current version it can be accessed using napi_default_features.
// A module can override the set of its enabled features by adding
// NAPI_CUSTOM_FEATURES definition to the .gyp file and then defining the
// value of the global napi_module_features variable.
// To check enabled features use the `napi_is_feature_enabled` function.
//
// For example, to disables napi_feature_reference_all_types:
// napi_features napi_module_features =
// napi_default_features & ~napi_feature_reference_all_types;
typedef enum {
// To be used when no features needs to be set.
napi_feature_none = 0,
// Use napi_ref for all value types.
// Not only objects, functions, and symbols as before.
napi_feature_reference_all_types = 1 << 0,
// Each version of NAPI is going to have its own default set of features.
napi_default_experimental_features = napi_feature_reference_all_types,
// This variable must be conditionally set depending on the NAPI version.
napi_default_features = napi_default_experimental_features,
} napi_features;
#endif

typedef napi_value(NAPI_CDECL* napi_callback)(napi_env env,
napi_callback_info info);
typedef void(NAPI_CDECL* napi_finalize)(napi_env env,
Expand Down
32 changes: 25 additions & 7 deletions src/js_native_api_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,9 @@ Reference::Reference(napi_env env, v8::Local<v8::Value> value, Args&&... args)
: RefBase(env, std::forward<Args>(args)...),
_persistent(env->isolate, value),
_secondPassParameter(new SecondPassCallParameterRef(this)),
_secondPassScheduled(false) {
_secondPassScheduled(false),
_canBeWeak(!env->IsFeatureEnabled(napi_feature_reference_all_types) ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never allowed creating a reference on primitive values (except Symbols). So I believe there is no need to check the feature bit here: reference on primitive values can not be a weak reference.

Suggested change
_canBeWeak(!env->IsFeatureEnabled(napi_feature_reference_all_types) ||
_canBeWeak(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent here is to stop offering weak references for Symbols with the new flag and only do it for Objects and Functions to better match the JavaScript spec. But since currently we support weak references for Symbols and have unit tests in napi_references for them, I had to put the flag check here. This way the old code works without changes if the flag is not set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the _canBeWeak field completely to let V8 engine to decide on the weak references' behavior.
The only true weak references, as it used to be before, are object, external objects, and functions.
All other types are strong references when the ref count is 0.
This behavior can be seen in the new tests.
The only difference between what we used to have before and now is that we allow all types instead of only four (object, external object, function, and symbol) to be ref counted.

It almost feels like that allowing use of napi_ref for all value types may not need a special feature, but we should rather just extend the existing behavior as we did previously for Symbols.

value->IsObject() || value->IsFunction()) {
if (RefCount() == 0) {
SetWeak();
}
Expand Down Expand Up @@ -652,7 +654,7 @@ void Reference::Finalize(bool is_env_teardown) {
// the secondPassParameter so that even if it has been
// scheduled no Finalization will be run.
void Reference::ClearWeak() {
if (!_persistent.IsEmpty()) {
if (!_persistent.IsEmpty() && _canBeWeak) {
_persistent.ClearWeak();
}
if (_secondPassParameter != nullptr) {
Expand All @@ -669,8 +671,13 @@ void Reference::SetWeak() {
// nothing
return;
}
_persistent.SetWeak(
_secondPassParameter, FinalizeCallback, v8::WeakCallbackType::kParameter);
if (_canBeWeak) {
_persistent.SetWeak(_secondPassParameter,
FinalizeCallback,
v8::WeakCallbackType::kParameter);
} else {
_persistent.Reset();
}
*_secondPassParameter = this;
}

Expand Down Expand Up @@ -2495,9 +2502,11 @@ napi_status NAPI_CDECL napi_create_reference(napi_env env,
CHECK_ARG(env, result);

v8::Local<v8::Value> 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->IsFeatureEnabled(napi_feature_reference_all_types)) {
if (!(v8_value->IsObject() || v8_value->IsFunction() ||
v8_value->IsSymbol())) {
return napi_set_last_error(env, napi_invalid_arg);
}
}

v8impl::Reference* reference =
Expand Down Expand Up @@ -3257,3 +3266,12 @@ napi_status NAPI_CDECL napi_is_detached_arraybuffer(napi_env env,

return napi_clear_last_error(env);
}

napi_status NAPI_CDECL napi_is_feature_enabled(napi_env env,
napi_features feature,
bool* result) {
CHECK_ENV(env);
CHECK_ARG(env, result);
*result = env->IsFeatureEnabled(feature);
return napi_clear_last_error(env);
}
22 changes: 20 additions & 2 deletions src/js_native_api_v8.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ class RefTracker {
} // end of namespace v8impl

struct napi_env__ {
explicit napi_env__(v8::Local<v8::Context> context)
explicit napi_env__(v8::Local<v8::Context> context, napi_features* features)
: isolate(context->GetIsolate()), context_persistent(isolate, context) {
CHECK_EQ(isolate, context->GetIsolate());
SetFeatures(features);
napi_clear_last_error(this);
}

Expand Down Expand Up @@ -89,13 +90,28 @@ struct napi_env__ {
}
}

// This should be overridden to schedule the finalization to a properiate
// This should be overridden to schedule the finalization to appropriate
// timing, like next tick of the event loop.
virtual void CallFinalizer(napi_finalize cb, void* data, void* hint) {
v8::HandleScope handle_scope(isolate);
CallIntoModule([&](napi_env env) { cb(env, data, hint); });
}

bool IsFeatureEnabled(napi_features feature) {
// By comparing results of `&` operation to the feature parameter
// we allow to test for multiple feature flags.
return (_features & feature) == feature;
}

void SetFeatures(napi_features* features) {
if (features == nullptr) {
_features = napi_feature_none;
} else {
const napi_features availableFeatures = napi_default_features;
_features = static_cast<napi_features>(availableFeatures & *features);
}
}

virtual void DeleteMe() {
// First we must finalize those references that have `napi_finalizer`
// callbacks. The reason is that addons might store other references which
Expand All @@ -122,6 +138,7 @@ struct napi_env__ {
int open_callback_scopes = 0;
int refs = 1;
void* instance_data = nullptr;
napi_features _features = napi_feature_none;

protected:
// Should not be deleted directly. Delete with `napi_env__::DeleteMe()`
Expand Down Expand Up @@ -435,6 +452,7 @@ class Reference : public RefBase {
v8impl::Persistent<v8::Value> _persistent;
SecondPassCallParameterRef* _secondPassParameter;
bool _secondPassScheduled;
const bool _canBeWeak;

FRIEND_TEST(JsNativeApiV8Test, Reference);
};
Expand Down
34 changes: 27 additions & 7 deletions src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
#include <memory>

node_napi_env__::node_napi_env__(v8::Local<v8::Context> context,
const std::string& module_filename)
: napi_env__(context), filename(module_filename) {
const std::string& module_filename,
napi_features* features)
: napi_env__(context, features), filename(module_filename) {
CHECK_NOT_NULL(node_env());
}

Expand Down Expand Up @@ -126,10 +127,11 @@ class BufferFinalizer : private Finalizer {
};

inline napi_env NewEnv(v8::Local<v8::Context> context,
const std::string& module_filename) {
const std::string& module_filename,
napi_features* features) {
node_napi_env result;

result = new node_napi_env__(context, module_filename);
result = new node_napi_env__(context, module_filename, features);
// 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,
Expand Down Expand Up @@ -586,24 +588,42 @@ class AsyncContext {

} // end of namespace v8impl

void napi_module_register_by_symbol_with_features(
v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
napi_addon_register_func init,
napi_features* features);

// Intercepts the Node-V8 module registration callback. Converts parameters
// to NAPI equivalents and then calls the registration callback specified
// by the NAPI module.
static void napi_module_register_cb(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
void* priv) {
napi_module_register_by_symbol(
napi_module_register_by_symbol_with_features(
exports,
module,
context,
static_cast<const napi_module*>(priv)->nm_register_func);
static_cast<const napi_module*>(priv)->nm_register_func,
static_cast<const napi_module*>(priv)->nm_features);
}

void napi_module_register_by_symbol(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
napi_addon_register_func init) {
napi_module_register_by_symbol_with_features(
exports, module, context, init, nullptr);
}

void napi_module_register_by_symbol_with_features(
v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
napi_addon_register_func init,
napi_features* features) {
node::Environment* node_env = node::Environment::GetCurrent(context);
std::string module_filename = "";
if (init == nullptr) {
Expand Down Expand Up @@ -631,7 +651,7 @@ void napi_module_register_by_symbol(v8::Local<v8::Object> 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, features);

napi_value _exports;
env->CallIntoModule([&](napi_env env) {
Expand Down
34 changes: 32 additions & 2 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ typedef struct napi_module {
napi_addon_register_func nm_register_func;
const char* nm_modname;
void* nm_priv;
#ifdef NAPI_EXPERIMENTAL
napi_features* nm_features;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a good start for node-api to get started with backward-compatible feature additions!

As you mentioned in the node-api meetings, this bit flag (an enum is an int size) may only represent 32 features at most. I'm wondering if it would be more extensible to save the module's defined NAPI_VERSION as nm_napi_version here like nm_version instead. In this way, node can refuse to load a node-api addon when an addon requires NAPI_VERSION 10, but the node is compiled as NAPI_VERSION 9.

The drawback of the alternative is that people have to pick up all feature changes with the new NAPI_VERSION, not part of it. But this could also be a relief that we don't need to maintain a long list of features, but a version support list instead.

What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of passing Node-API version used for a module. This way we can enforce the version compatibility.
The only drawback could be that developers will not be able to choose functionality depending on the Node-API version at runtime. I am not sure if anyone does it though.

As for the 32-bit feature set limit, the proposal is to pass not the feature bits to the module, but rather the pointer to the feature set. This way we can use the first 31 bits normally. In case if we need more, then we can set the 32nd bit and it will mean that the feature set has the second 32bit number, and the feature set pointer becomes a pointer to the feature set array with two elements. We can extend this array to be as long as needed. Obviously, each new entry in this array will require its own enum type. In practice I doubt that we ever exceed the 31 bit limit, but if we do, then we can extend it using this approach.

void* reserved[3];
#else
void* reserved[4];
#endif
} napi_module;

#define NAPI_MODULE_VERSION 1
Expand Down Expand Up @@ -73,18 +78,43 @@ typedef struct napi_module {
static void fn(void)
#endif

#ifdef NAPI_EXPERIMENTAL
#ifdef NAPI_CUSTOM_FEATURES

// Define value of napi_module_features variable in your module when
// NAPI_CUSTOM_FEATURES is set in gyp file.
extern napi_features napi_module_features;
#define NAPI_DEFINE_DEFAULT_FEATURES

#else // NAPI_CUSTOM_FEATURES

#define NAPI_DEFINE_DEFAULT_FEATURES \
static napi_features napi_module_features = napi_default_features;

#endif // NAPI_CUSTOM_FEATURES

#define NAPI_FEATURES_PTR /* NOLINT */ &napi_module_features,

#else // NAPI_EXPERIMENTAL
#define NAPI_DEFINE_DEFAULT_FEATURES
#define NAPI_FEATURES_PTR
#endif // NAPI_EXPERIMENTAL

#define NAPI_MODULE_X(modname, regfunc, priv, flags) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just create a new NAPI_MODULE_X_FEATURES which takes the features parameter?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be simpler - let me try.

EXTERN_C_START \
NAPI_DEFINE_DEFAULT_FEATURES \
static napi_module _module = { \
NAPI_MODULE_VERSION, \
flags, \
__FILE__, \
regfunc, \
#modname, \
priv, \
{0}, \
NAPI_FEATURES_PTR{0}, \
}; \
NAPI_C_CTOR(_register_##modname) { napi_module_register(&_module); } \
NAPI_C_CTOR(_register_##modname) { \
napi_module_register(&_module); \
} \
EXTERN_C_END

#define NAPI_MODULE_INITIALIZER_X(base, version) \
Expand Down
Loading