Skip to content

Commit

Permalink
node-api: get Node API version used by addon
Browse files Browse the repository at this point in the history
  • Loading branch information
vmoroz committed Apr 7, 2023
1 parent 0c76922 commit 722dbc5
Show file tree
Hide file tree
Showing 16 changed files with 625 additions and 19 deletions.
3 changes: 2 additions & 1 deletion src/api/environment.cc
Expand Up @@ -884,7 +884,8 @@ void AddLinkedBinding(Environment* env,
exports,
module,
context,
reinterpret_cast<napi_addon_register_func>(priv));
reinterpret_cast<napi_addon_register_func>(priv),
NAPI_DEFAULT_MODULE_API_VERSION);
},
name,
reinterpret_cast<void*>(fn),
Expand Down
8 changes: 5 additions & 3 deletions src/js_native_api_v8.cc
Expand Up @@ -2419,9 +2419,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->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(
Expand Down
10 changes: 8 additions & 2 deletions src/js_native_api_v8.h
Expand Up @@ -51,8 +51,13 @@ class Finalizer;
} // end of namespace v8impl

struct napi_env__ {
explicit napi_env__(v8::Local<v8::Context> context)
: isolate(context->GetIsolate()), context_persistent(isolate, context) {
explicit napi_env__(v8::Local<v8::Context> context,
int32_t module_api_version)
: isolate(context->GetIsolate()),
context_persistent(isolate, context),
module_api_version(module_api_version != 0
? module_api_version
: NAPI_DEFAULT_MODULE_API_VERSION) {
napi_clear_last_error(this);
}

Expand Down Expand Up @@ -148,6 +153,7 @@ struct napi_env__ {
int open_callback_scopes = 0;
int refs = 1;
void* instance_data = nullptr;
int32_t module_api_version = NAPI_DEFAULT_MODULE_API_VERSION;

protected:
// Should not be deleted directly. Delete with `napi_env__::DeleteMe()`
Expand Down
19 changes: 12 additions & 7 deletions src/node_api.cc
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,
int32_t module_api_version)
: napi_env__(context, module_api_version), filename(module_filename) {
CHECK_NOT_NULL(node_env());
}

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

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

result = new node_napi_env__(context, module_filename);
result = new node_napi_env__(context, module_filename, module_api_version);
// TODO(addaleax): There was previously code that tried to delete the
// napi_env when its v8::Context was garbage collected;
// However, as long as N-API addons using this napi_env are in place,
Expand Down Expand Up @@ -623,17 +625,20 @@ static void napi_module_register_cb(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
void* priv) {
const napi_module* napi_mod = static_cast<const napi_module*>(priv);
napi_module_register_by_symbol(
exports,
module,
context,
static_cast<const napi_module*>(priv)->nm_register_func);
napi_mod->nm_register_func,
NAPI_DEFAULT_MODULE_API_VERSION);
}

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_addon_register_func init,
int32_t module_api_version) {
node::Environment* node_env = node::Environment::GetCurrent(context);
std::string module_filename = "";
if (init == nullptr) {
Expand Down Expand Up @@ -661,7 +666,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, module_api_version);

napi_value _exports = nullptr;
env->CallIntoModule([&](napi_env env) {
Expand Down
10 changes: 10 additions & 0 deletions src/node_api.h
Expand Up @@ -30,6 +30,7 @@ struct uv_loop_s; // Forward declaration.

typedef napi_value(NAPI_CDECL* napi_addon_register_func)(napi_env env,
napi_value exports);
typedef int32_t(NAPI_CDECL* napi_addon_get_api_version_func)();

// Used by deprecated registration method napi_module_register.
typedef struct napi_module {
Expand All @@ -54,11 +55,20 @@ typedef struct napi_module {
#define NAPI_MODULE_INITIALIZER_BASE napi_register_module_v
#endif

#define NAPI_MODULE_GET_API_VERSION_BASE napi_module_get_api_version_v

#define NAPI_MODULE_INITIALIZER \
NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_INITIALIZER_BASE, NAPI_MODULE_VERSION)

#define NAPI_MODULE_GET_API_VERSION \
NAPI_MODULE_INITIALIZER_X(NAPI_MODULE_GET_API_VERSION_BASE, \
NAPI_MODULE_VERSION)

#define NAPI_MODULE_INIT() \
EXTERN_C_START \
NAPI_MODULE_EXPORT int32_t NAPI_MODULE_GET_API_VERSION() { \
return NAPI_VERSION; \
} \
NAPI_MODULE_EXPORT napi_value NAPI_MODULE_INITIALIZER(napi_env env, \
napi_value exports); \
EXTERN_C_END \
Expand Down
3 changes: 2 additions & 1 deletion src/node_api_internals.h
Expand Up @@ -10,7 +10,8 @@

struct node_napi_env__ : public napi_env__ {
node_napi_env__(v8::Local<v8::Context> context,
const std::string& module_filename);
const std::string& module_filename,
int32_t module_api_version);

bool can_call_into_js() const override;
v8::Maybe<bool> mark_arraybuffer_as_untransferable(
Expand Down
13 changes: 12 additions & 1 deletion src/node_binding.cc
Expand Up @@ -408,6 +408,12 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) {
dlib->GetSymbolAddress(name));
}

inline napi_addon_get_api_version_func GetNapiAddonGetApiVersionCallback(
DLib* dlib) {
return reinterpret_cast<napi_addon_get_api_version_func>(
dlib->GetSymbolAddress(STRINGIFY(NAPI_MODULE_GET_API_VERSION)));
}

// DLOpen is process.dlopen(module, filename, flags).
// Used to load 'module.node' dynamically shared objects.
//
Expand Down Expand Up @@ -484,7 +490,12 @@ void DLOpen(const FunctionCallbackInfo<Value>& args) {
callback(exports, module, context);
return true;
} else if (auto napi_callback = GetNapiInitializerCallback(dlib)) {
napi_module_register_by_symbol(exports, module, context, napi_callback);
int32_t module_api_version = 0;
if (auto get_version = GetNapiAddonGetApiVersionCallback(dlib)) {
module_api_version = get_version();
}
napi_module_register_by_symbol(
exports, module, context, napi_callback, module_api_version);
return true;
} else {
mp = dlib->GetSavedModuleFromGlobalHandleMap();
Expand Down
5 changes: 3 additions & 2 deletions src/node_binding.h
Expand Up @@ -21,7 +21,7 @@ enum {

// Make sure our internal values match the public API's values.
static_assert(static_cast<int>(NM_F_LINKED) ==
static_cast<int>(node::ModuleFlags::kLinked),
static_cast<int>(node::ModuleFlags::kLinked),
"NM_F_LINKED != node::ModuleFlags::kLinked");

#if NODE_HAVE_I18N_SUPPORT
Expand Down Expand Up @@ -52,7 +52,8 @@ static_assert(static_cast<int>(NM_F_LINKED) ==
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_addon_register_func init,
int32_t module_api_version);

namespace node {

Expand Down
6 changes: 5 additions & 1 deletion src/node_version.h
Expand Up @@ -93,6 +93,10 @@

// The NAPI_VERSION provided by this version of the runtime. This is the version
// which the Node binary being built supports.
#define NAPI_VERSION 8
#define NAPI_VERSION 8

// The NAPI_VERSION used by default by Node API modules if it is not explicitly
// specified. It must be always 8.
#define NAPI_DEFAULT_MODULE_API_VERSION 8

#endif // SRC_NODE_VERSION_H_
3 changes: 2 additions & 1 deletion test/cctest/test_node_api.cc
Expand Up @@ -34,7 +34,8 @@ TEST_F(NodeApiTest, CreateNodeApiEnv) {
};
Local<Object> module_obj = Object::New(isolate_);
Local<Object> exports_obj = Object::New(isolate_);
napi_module_register_by_symbol(exports_obj, module_obj, env->context(), init);
napi_module_register_by_symbol(
exports_obj, module_obj, env->context(), init, 0);
ASSERT_NE(addon_env, nullptr);
node_napi_env internal_env = reinterpret_cast<node_napi_env>(addon_env);
EXPECT_EQ(internal_env->node_env(), env);
Expand Down
9 changes: 9 additions & 0 deletions 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' ]
}
]
}
90 changes: 90 additions & 0 deletions 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();

0 comments on commit 722dbc5

Please sign in to comment.