Skip to content

Commit 5abf602

Browse files
authored
Merge pull request #723 from gabrielschulhof/backport-4e885069-pr-475
[v2.x] Backport ObjectWrap reference-related changes
2 parents 2b78b54 + 470f130 commit 5abf602

11 files changed

+193
-15
lines changed

napi-inl.h

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// Note: Do not include this file directly! Include "napi.h" instead.
1111

1212
#include <algorithm>
13+
#include <atomic>
1314
#include <cstring>
1415
#include <mutex>
1516
#include <type_traits>
@@ -19,6 +20,8 @@ namespace Napi {
1920
// Helpers to handle functions exposed from C++.
2021
namespace details {
2122

23+
extern std::atomic_bool needs_objectwrap_destructor_fix;
24+
2225
// Attach a data item to an object and delete it when the object gets
2326
// garbage-collected.
2427
// TODO: Replace this code with `napi_add_finalizer()` whenever it becomes
@@ -251,11 +254,16 @@ struct AccessorCallbackData {
251254
// Module registration
252255
////////////////////////////////////////////////////////////////////////////////
253256

254-
#define NODE_API_MODULE(modname, regfunc) \
255-
napi_value __napi_ ## regfunc(napi_env env, \
256-
napi_value exports) { \
257-
return Napi::RegisterModule(env, exports, regfunc); \
258-
} \
257+
#define NODE_API_MODULE(modname, regfunc) \
258+
namespace Napi { \
259+
namespace details { \
260+
std::atomic_bool needs_objectwrap_destructor_fix(false); \
261+
} \
262+
} \
263+
napi_value __napi_ ## regfunc(napi_env env, \
264+
napi_value exports) { \
265+
return Napi::RegisterModule(env, exports, regfunc); \
266+
} \
259267
NAPI_MODULE(modname, __napi_ ## regfunc)
260268

261269
// Adapt the NAPI_MODULE registration function:
@@ -264,6 +272,12 @@ struct AccessorCallbackData {
264272
inline napi_value RegisterModule(napi_env env,
265273
napi_value exports,
266274
ModuleRegisterCallback registerCallback) {
275+
const napi_node_version* nver = Napi::VersionManagement::GetNodeVersion(env);
276+
Napi::details::needs_objectwrap_destructor_fix =
277+
(nver->major < 10 ||
278+
(nver->major == 10 && nver->minor < 15) ||
279+
(nver->major == 10 && nver->minor == 15 && nver->patch < 3));
280+
267281
return details::WrapCallback([&] {
268282
return napi_value(registerCallback(Napi::Env(env),
269283
Napi::Object(env, exports)));
@@ -2968,16 +2982,36 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
29682982
napi_value wrapper = callbackInfo.This();
29692983
napi_status status;
29702984
napi_ref ref;
2971-
T* instance = static_cast<T*>(this);
2972-
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
2985+
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
29732986
NAPI_THROW_IF_FAILED_VOID(env, status);
29742987

2975-
Reference<Object>* instanceRef = instance;
2988+
Reference<Object>* instanceRef = this;
29762989
*instanceRef = Reference<Object>(env, ref);
29772990
}
29782991

2979-
template<typename T>
2980-
inline ObjectWrap<T>::~ObjectWrap() {}
2992+
template <typename T>
2993+
inline ObjectWrap<T>::~ObjectWrap() {
2994+
// If the JS object still exists at this point, remove the finalizer added
2995+
// through `napi_wrap()`.
2996+
if (!IsEmpty()) {
2997+
Object object = Value();
2998+
// It is not valid to call `napi_remove_wrap()` with an empty `object`.
2999+
// This happens e.g. during garbage collection.
3000+
if (!object.IsEmpty() && _construction_failed) {
3001+
napi_remove_wrap(Env(), object, nullptr);
3002+
3003+
if (Napi::details::needs_objectwrap_destructor_fix) {
3004+
// If construction failed we delete the reference via
3005+
// `napi_remove_wrap()`, not via `napi_delete_reference()` in the
3006+
// `Reference<Object>` destructor. This will prevent the
3007+
// `Reference<Object>` destructor from doing a double delete of this
3008+
// reference.
3009+
_ref = nullptr;
3010+
_env = nullptr;
3011+
}
3012+
}
3013+
}
3014+
}
29813015

29823016
template<typename T>
29833017
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
@@ -3369,10 +3403,21 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
33693403
return nullptr;
33703404
}
33713405

3372-
T* instance;
33733406
napi_value wrapper = details::WrapCallback([&] {
33743407
CallbackInfo callbackInfo(env, info);
3375-
instance = new T(callbackInfo);
3408+
T* instance = new T(callbackInfo);
3409+
#ifdef NAPI_CPP_EXCEPTIONS
3410+
instance->_construction_failed = false;
3411+
#else
3412+
if (callbackInfo.Env().IsExceptionPending()) {
3413+
// We need to clear the exception so that removing the wrap might work.
3414+
Error e = callbackInfo.Env().GetAndClearPendingException();
3415+
delete instance;
3416+
e.ThrowAsJavaScriptException();
3417+
} else {
3418+
instance->_construction_failed = false;
3419+
}
3420+
# endif // NAPI_CPP_EXCEPTIONS
33763421
return callbackInfo.This();
33773422
});
33783423

@@ -3497,7 +3542,7 @@ inline napi_value ObjectWrap<T>::InstanceSetterCallbackWrapper(
34973542

34983543
template <typename T>
34993544
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* /*hint*/) {
3500-
T* instance = reinterpret_cast<T*>(data);
3545+
ObjectWrap<T>* instance = static_cast<ObjectWrap<T>*>(data);
35013546
instance->Finalize(Napi::Env(env));
35023547
delete instance;
35033548
}

napi.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,8 @@ namespace Napi {
17351735
StaticAccessorCallbackData;
17361736
typedef AccessorCallbackData<InstanceGetterCallback, InstanceSetterCallback>
17371737
InstanceAccessorCallbackData;
1738+
1739+
bool _construction_failed = true;
17381740
};
17391741

17401742
class HandleScope {

test/bigint.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function test(binding) {
4646
});
4747

4848
assert.throws(TestTooBigBigInt, {
49-
name: 'RangeError',
50-
message: 'Maximum BigInt size exceeded',
49+
name: /^(RangeError|Error)$/,
50+
message: /^(Maximum BigInt size exceeded|Invalid argument)$/,
5151
});
5252
}

test/binding.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Object InitThreadSafeFunction(Env env);
4949
#endif
5050
Object InitTypedArray(Env env);
5151
Object InitObjectWrap(Env env);
52+
Object InitObjectWrapConstructorException(Env env);
53+
Object InitObjectWrapRemoveWrap(Env env);
5254
Object InitObjectReference(Env env);
5355
Object InitVersionManagement(Env env);
5456
Object InitThunkingManual(Env env);
@@ -102,6 +104,9 @@ Object Init(Env env, Object exports) {
102104
#endif
103105
exports.Set("typedarray", InitTypedArray(env));
104106
exports.Set("objectwrap", InitObjectWrap(env));
107+
exports.Set("objectwrapConstructorException",
108+
InitObjectWrapConstructorException(env));
109+
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
105110
exports.Set("objectreference", InitObjectReference(env));
106111
exports.Set("version_management", InitVersionManagement(env));
107112
exports.Set("thunking_manual", InitThunkingManual(env));

test/binding.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
'threadsafe_function/threadsafe_function.cc',
4545
'typedarray.cc',
4646
'objectwrap.cc',
47+
'objectwrap_constructor_exception.cc',
48+
'objectwrap-removewrap.cc',
4749
'objectreference.cc',
4850
'version_management.cc',
4951
'thunking_manual.cc',

test/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ let testModules = [
4848
'typedarray',
4949
'typedarray-bigint',
5050
'objectwrap',
51+
'objectwrap_constructor_exception',
52+
'objectwrap-removewrap',
5153
'objectreference',
5254
'version_management'
5355
];

test/objectwrap-removewrap.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include <napi.h>
2+
#include <assert.h>
3+
4+
namespace {
5+
6+
static int dtor_called = 0;
7+
8+
class DtorCounter {
9+
public:
10+
~DtorCounter() {
11+
assert(dtor_called == 0);
12+
dtor_called++;
13+
}
14+
};
15+
16+
Napi::Value GetDtorCalled(const Napi::CallbackInfo& info) {
17+
return Napi::Number::New(info.Env(), dtor_called);
18+
}
19+
20+
class Test : public Napi::ObjectWrap<Test> {
21+
public:
22+
Test(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Test>(info) {
23+
#ifdef NAPI_CPP_EXCEPTIONS
24+
throw Napi::Error::New(Env(), "Some error");
25+
#else
26+
Napi::Error::New(Env(), "Some error").ThrowAsJavaScriptException();
27+
#endif
28+
}
29+
30+
static void Initialize(Napi::Env env, Napi::Object exports) {
31+
exports.Set("Test", DefineClass(env, "Test", {}));
32+
exports.Set("getDtorCalled", Napi::Function::New(env, GetDtorCalled));
33+
}
34+
35+
private:
36+
DtorCounter dtor_counter_;
37+
};
38+
39+
} // anonymous namespace
40+
41+
Napi::Object InitObjectWrapRemoveWrap(Napi::Env env) {
42+
Napi::Object exports = Napi::Object::New(env);
43+
Test::Initialize(env, exports);
44+
return exports;
45+
}

test/objectwrap-removewrap.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
3+
if (process.argv[2] === 'child') {
4+
// Create a single wrapped instance then exit.
5+
return new (require(process.argv[3]).objectwrap.Test)();
6+
}
7+
8+
const buildType = process.config.target_defaults.default_configuration;
9+
const assert = require('assert');
10+
const { spawnSync } = require('child_process');
11+
12+
const test = (bindingName) => {
13+
const binding = require(bindingName);
14+
const Test = binding.objectwrap_removewrap.Test;
15+
const getDtorCalled = binding.objectwrap_removewrap.getDtorCalled;
16+
17+
assert.strictEqual(getDtorCalled(), 0);
18+
assert.throws(() => {
19+
new Test();
20+
});
21+
assert.strictEqual(getDtorCalled(), 1);
22+
global.gc(); // Does not crash.
23+
24+
// Start a child process that creates a single wrapped instance to ensure that
25+
// it is properly freed at its exit. It must not segfault.
26+
// Re: https://github.com/nodejs/node-addon-api/issues/660
27+
const child = spawnSync(process.execPath, [
28+
__filename, 'child', bindingName
29+
]);
30+
assert.strictEqual(child.signal, null);
31+
assert.strictEqual(child.status, 0);
32+
}
33+
34+
test(`./build/${buildType}/binding.node`);
35+
test(`./build/${buildType}/binding_noexcept.node`);

test/objectwrap.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ const test = (binding) => {
222222
// `Test` is needed for accessing exposed symbols
223223
testObj(new Test(), Test);
224224
testClass(Test);
225+
226+
// Make sure the C++ object can be garbage collected without issues.
227+
setImmediate(global.gc);
225228
}
226229

227230
test(require(`./build/${buildType}/binding.node`));
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#include <napi.h>
2+
3+
class ConstructorExceptionTest :
4+
public Napi::ObjectWrap<ConstructorExceptionTest> {
5+
public:
6+
ConstructorExceptionTest(const Napi::CallbackInfo& info) :
7+
Napi::ObjectWrap<ConstructorExceptionTest>(info) {
8+
fprintf(stderr, "That was the interesting reference\n");
9+
Napi::Error error = Napi::Error::New(info.Env(), "an exception");
10+
#ifdef NAPI_DISABLE_CPP_EXCEPTIONS
11+
error.ThrowAsJavaScriptException();
12+
#else
13+
throw error;
14+
#endif // NAPI_DISABLE_CPP_EXCEPTIONS
15+
}
16+
17+
static void Initialize(Napi::Env env, Napi::Object exports) {
18+
const char* name = "ConstructorExceptionTest";
19+
exports.Set(name, DefineClass(env, name, {}));
20+
}
21+
};
22+
23+
Napi::Object InitObjectWrapConstructorException(Napi::Env env) {
24+
Napi::Object exports = Napi::Object::New(env);
25+
ConstructorExceptionTest::Initialize(env, exports);
26+
return exports;
27+
}

0 commit comments

Comments
 (0)