node-api: add nested object wrap and napi_ref test

Test that an napi_ref can be nested inside another ObjectWrap. The test
shows a critical case where a finalizer deletes an `napi_ref` whose
finalizer is also scheduled.

PR-URL: https://github.com/nodejs/node/pull/57981
Reviewed-By: Vladimir Morozov <vmorozov@microsoft.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
This commit is contained in:
Chengzhong Wu 2025-04-26 00:22:30 +02:00 committed by GitHub
parent 25fe802fdc
commit 3b90f3454d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 207 additions and 26 deletions

View File

@ -1,17 +1,28 @@
{
"targets": [
{
"target_name": "6_object_wrap",
"target_name": "myobject",
"sources": [
"6_object_wrap.cc"
"myobject.cc",
"myobject.h",
]
},
{
"target_name": "6_object_wrap_basic_finalizer",
"target_name": "myobject_basic_finalizer",
"defines": [ "NAPI_EXPERIMENTAL" ],
"sources": [
"6_object_wrap.cc"
]
}
"myobject.cc",
"myobject.h",
]
},
{
"target_name": "nested_wrap",
# Test without basic finalizers as it schedules differently.
"defines": [ "NAPI_VERSION=10" ],
"sources": [
"nested_wrap.cc",
"nested_wrap.h",
],
},
]
}

View File

@ -1,7 +1,7 @@
#include "myobject.h"
#include "../common.h"
#include "../entry_point.h"
#include "assert.h"
#include "myobject.h"
typedef int32_t FinalizerData;
@ -10,7 +10,9 @@ napi_ref MyObject::constructor;
MyObject::MyObject(double value)
: value_(value), env_(nullptr), wrapper_(nullptr) {}
MyObject::~MyObject() { napi_delete_reference(env_, wrapper_); }
MyObject::~MyObject() {
napi_delete_reference(env_, wrapper_);
}
void MyObject::Destructor(node_api_basic_env env,
void* nativeObject,
@ -27,23 +29,35 @@ void MyObject::Destructor(node_api_basic_env env,
void MyObject::Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
{"value", nullptr, nullptr, GetValue, SetValue, 0, napi_default, 0},
{ "valueReadonly", nullptr, nullptr, GetValue, nullptr, 0, napi_default,
{"valueReadonly",
nullptr,
nullptr,
GetValue,
nullptr,
0,
napi_default,
0},
DECLARE_NODE_API_PROPERTY("plusOne", PlusOne),
DECLARE_NODE_API_PROPERTY("multiply", Multiply),
};
napi_value cons;
NODE_API_CALL_RETURN_VOID(env, napi_define_class(
env, "MyObject", -1, New, nullptr,
NODE_API_CALL_RETURN_VOID(
env,
napi_define_class(env,
"MyObject",
-1,
New,
nullptr,
sizeof(properties) / sizeof(napi_property_descriptor),
properties, &cons));
properties,
&cons));
NODE_API_CALL_RETURN_VOID(env,
napi_create_reference(env, cons, 1, &constructor));
NODE_API_CALL_RETURN_VOID(env,
napi_set_named_property(env, exports, "MyObject", cons));
NODE_API_CALL_RETURN_VOID(
env, napi_set_named_property(env, exports, "MyObject", cons));
}
napi_value MyObject::New(napi_env env, napi_callback_info info) {
@ -71,8 +85,12 @@ napi_value MyObject::New(napi_env env, napi_callback_info info) {
obj->env_ = env;
NODE_API_CALL(env,
napi_wrap(env, _this, obj, MyObject::Destructor,
nullptr /* finalize_hint */, &obj->wrapper_));
napi_wrap(env,
_this,
obj,
MyObject::Destructor,
nullptr /* finalize_hint */,
&obj->wrapper_));
return _this;
}

View File

@ -0,0 +1,99 @@
#include "nested_wrap.h"
#include "../common.h"
#include "../entry_point.h"
napi_ref NestedWrap::constructor{};
static int finalization_count = 0;
NestedWrap::NestedWrap() {}
NestedWrap::~NestedWrap() {
napi_delete_reference(env_, wrapper_);
// Delete the nested reference as well.
napi_delete_reference(env_, nested_);
}
void NestedWrap::Destructor(node_api_basic_env env,
void* nativeObject,
void* /*finalize_hint*/) {
// Once this destructor is called, it cancels all pending
// finalizers for the object by deleting the references.
NestedWrap* obj = static_cast<NestedWrap*>(nativeObject);
delete obj;
finalization_count++;
}
void NestedWrap::Init(napi_env env, napi_value exports) {
napi_value cons;
NODE_API_CALL_RETURN_VOID(
env,
napi_define_class(
env, "NestedWrap", -1, New, nullptr, 0, nullptr, &cons));
NODE_API_CALL_RETURN_VOID(env,
napi_create_reference(env, cons, 1, &constructor));
NODE_API_CALL_RETURN_VOID(
env, napi_set_named_property(env, exports, "NestedWrap", cons));
}
napi_value NestedWrap::New(napi_env env, napi_callback_info info) {
napi_value new_target;
NODE_API_CALL(env, napi_get_new_target(env, info, &new_target));
bool is_constructor = (new_target != nullptr);
NODE_API_BASIC_ASSERT_BASE(
is_constructor, "Constructor called without new", nullptr);
napi_value this_val;
NODE_API_CALL(env,
napi_get_cb_info(env, info, 0, nullptr, &this_val, nullptr));
NestedWrap* obj = new NestedWrap();
obj->env_ = env;
NODE_API_CALL(env,
napi_wrap(env,
this_val,
obj,
NestedWrap::Destructor,
nullptr /* finalize_hint */,
&obj->wrapper_));
// Create a second napi_ref to be deleted in the destructor.
NODE_API_CALL(env,
napi_add_finalizer(env,
this_val,
obj,
NestedWrap::Destructor,
nullptr /* finalize_hint */,
&obj->nested_));
return this_val;
}
static napi_value GetFinalizerCallCount(napi_env env, napi_callback_info info) {
napi_value result;
NODE_API_CALL(env, napi_create_int32(env, finalization_count, &result));
return result;
}
EXTERN_C_START
napi_value Init(napi_env env, napi_value exports) {
NestedWrap::Init(env, exports);
napi_property_descriptor descriptors[] = {
DECLARE_NODE_API_PROPERTY("getFinalizerCallCount", GetFinalizerCallCount),
};
NODE_API_CALL(
env,
napi_define_properties(env,
exports,
sizeof(descriptors) / sizeof(*descriptors),
descriptors));
return exports;
}
EXTERN_C_END

View File

@ -0,0 +1,33 @@
#ifndef TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_
#define TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_
#include <js_native_api.h>
/**
* Test that an napi_ref can be nested inside another ObjectWrap.
*
* This test shows a critical case where a finalizer deletes an napi_ref
* whose finalizer is also scheduled.
*/
class NestedWrap {
public:
static void Init(napi_env env, napi_value exports);
static void Destructor(node_api_basic_env env,
void* nativeObject,
void* finalize_hint);
private:
explicit NestedWrap();
~NestedWrap();
static napi_value New(napi_env env, napi_callback_info info);
static napi_ref constructor;
napi_env env_{};
napi_ref wrapper_{};
napi_ref nested_{};
};
#endif // TEST_JS_NATIVE_API_6_OBJECT_WRAP_NESTED_WRAP_H_

View File

@ -0,0 +1,20 @@
// Flags: --expose-gc
'use strict';
const common = require('../../common');
const { gcUntil } = require('../../common/gc');
const assert = require('assert');
const addon = require(`./build/${common.buildType}/nested_wrap`);
// This test verifies that ObjectWrap and napi_ref can be nested and finalized
// correctly with a non-basic finalizer.
(() => {
let obj = new addon.NestedWrap();
obj = null;
// Silent eslint about unused variables.
assert.strictEqual(obj, null);
})();
gcUntil('object-wrap-ref', () => {
return addon.getFinalizerCallCount() === 1;
});

View File

@ -3,7 +3,7 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const addon = require(`./build/${common.buildType}/6_object_wrap_basic_finalizer`);
const addon = require(`./build/${common.buildType}/myobject_basic_finalizer`);
// This test verifies that ObjectWrap can be correctly finalized with a node_api_basic_finalizer
// in the current JS loop tick

View File

@ -2,7 +2,7 @@
'use strict';
const common = require('../../common');
const addon = require(`./build/${common.buildType}/6_object_wrap`);
const addon = require(`./build/${common.buildType}/myobject`);
const { gcUntil } = require('../../common/gc');
(function scope() {

View File

@ -1,7 +1,7 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const addon = require(`./build/${common.buildType}/6_object_wrap`);
const addon = require(`./build/${common.buildType}/myobject`);
const getterOnlyErrorRE =
/^TypeError: Cannot set property .* of #<.*> which has only a getter$/;