Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
71 changes: 58 additions & 13 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// Note: Do not include this file directly! Include "napi.h" instead.

#include <algorithm>
#include <atomic>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a new change, not part of any cherry-pick.

#include <cstring>
#include <mutex>
#include <type_traits>
Expand All @@ -19,6 +20,8 @@ namespace Napi {
// Helpers to handle functions exposed from C++.
namespace details {

extern std::atomic_bool needs_objectwrap_destructor_fix;

Comment on lines +23 to +24

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a new change, not part of any cherry-pick.

// Attach a data item to an object and delete it when the object gets
// garbage-collected.
// TODO: Replace this code with `napi_add_finalizer()` whenever it becomes
Expand Down Expand Up @@ -251,11 +254,16 @@ struct AccessorCallbackData {
// Module registration
////////////////////////////////////////////////////////////////////////////////

#define NODE_API_MODULE(modname, regfunc) \
napi_value __napi_ ## regfunc(napi_env env, \
napi_value exports) { \
return Napi::RegisterModule(env, exports, regfunc); \
} \
#define NODE_API_MODULE(modname, regfunc) \
namespace Napi { \
namespace details { \
std::atomic_bool needs_objectwrap_destructor_fix(false); \
} \
} \
Comment on lines +258 to +262

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a new change, not part of any cherry-pick.

napi_value __napi_ ## regfunc(napi_env env, \
napi_value exports) { \
return Napi::RegisterModule(env, exports, regfunc); \
} \
NAPI_MODULE(modname, __napi_ ## regfunc)

// Adapt the NAPI_MODULE registration function:
Expand All @@ -264,6 +272,12 @@ struct AccessorCallbackData {
inline napi_value RegisterModule(napi_env env,
napi_value exports,
ModuleRegisterCallback registerCallback) {
const napi_node_version* nver = Napi::VersionManagement::GetNodeVersion(env);
Napi::details::needs_objectwrap_destructor_fix =
(nver->major < 10 ||
(nver->major == 10 && nver->minor < 15) ||
(nver->major == 10 && nver->minor == 15 && nver->patch < 3));

Comment on lines +275 to +280

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a new change, not part of any cherry-pick

return details::WrapCallback([&] {
return napi_value(registerCallback(Napi::Env(env),
Napi::Object(env, exports)));
Expand Down Expand Up @@ -2968,16 +2982,36 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
napi_value wrapper = callbackInfo.This();
napi_status status;
napi_ref ref;
T* instance = static_cast<T*>(this);
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
NAPI_THROW_IF_FAILED_VOID(env, status);

Reference<Object>* instanceRef = instance;
Reference<Object>* instanceRef = this;
*instanceRef = Reference<Object>(env, ref);
}

template<typename T>
inline ObjectWrap<T>::~ObjectWrap() {}
template <typename T>
inline ObjectWrap<T>::~ObjectWrap() {
// If the JS object still exists at this point, remove the finalizer added
// through `napi_wrap()`.
if (!IsEmpty()) {
Object object = Value();
// It is not valid to call `napi_remove_wrap()` with an empty `object`.
// This happens e.g. during garbage collection.
if (!object.IsEmpty() && _construction_failed) {
napi_remove_wrap(Env(), object, nullptr);

if (Napi::details::needs_objectwrap_destructor_fix) {
// If construction failed we delete the reference via
// `napi_remove_wrap()`, not via `napi_delete_reference()` in the
// `Reference<Object>` destructor. This will prevent the
// `Reference<Object>` destructor from doing a double delete of this
// reference.
_ref = nullptr;
_env = nullptr;
}
Comment on lines +3003 to +3011

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a new change, not part of any cherry-pick.

}
}
}

template<typename T>
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
Expand Down Expand Up @@ -3369,10 +3403,21 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
return nullptr;
}

T* instance;
napi_value wrapper = details::WrapCallback([&] {
CallbackInfo callbackInfo(env, info);
instance = new T(callbackInfo);
T* instance = new T(callbackInfo);
#ifdef NAPI_CPP_EXCEPTIONS
instance->_construction_failed = false;
#else
if (callbackInfo.Env().IsExceptionPending()) {
// We need to clear the exception so that removing the wrap might work.
Error e = callbackInfo.Env().GetAndClearPendingException();
delete instance;
e.ThrowAsJavaScriptException();
} else {
instance->_construction_failed = false;
}
# endif // NAPI_CPP_EXCEPTIONS
return callbackInfo.This();
});

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

template <typename T>
inline void ObjectWrap<T>::FinalizeCallback(napi_env env, void* data, void* /*hint*/) {
T* instance = reinterpret_cast<T*>(data);
ObjectWrap<T>* instance = static_cast<ObjectWrap<T>*>(data);
instance->Finalize(Napi::Env(env));
delete instance;
}
Expand Down
2 changes: 2 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,8 @@ namespace Napi {
StaticAccessorCallbackData;
typedef AccessorCallbackData<InstanceGetterCallback, InstanceSetterCallback>
InstanceAccessorCallbackData;

bool _construction_failed = true;
};

class HandleScope {
Expand Down
4 changes: 2 additions & 2 deletions test/bigint.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function test(binding) {
});

assert.throws(TestTooBigBigInt, {
name: 'RangeError',
message: 'Maximum BigInt size exceeded',
name: /^(RangeError|Error)$/,
message: /^(Maximum BigInt size exceeded|Invalid argument)$/,
});
}
5 changes: 5 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Object InitThreadSafeFunction(Env env);
#endif
Object InitTypedArray(Env env);
Object InitObjectWrap(Env env);
Object InitObjectWrapConstructorException(Env env);
Object InitObjectWrapRemoveWrap(Env env);
Object InitObjectReference(Env env);
Object InitVersionManagement(Env env);
Object InitThunkingManual(Env env);
Expand Down Expand Up @@ -102,6 +104,9 @@ Object Init(Env env, Object exports) {
#endif
exports.Set("typedarray", InitTypedArray(env));
exports.Set("objectwrap", InitObjectWrap(env));
exports.Set("objectwrapConstructorException",
InitObjectWrapConstructorException(env));
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
exports.Set("objectreference", InitObjectReference(env));
exports.Set("version_management", InitVersionManagement(env));
exports.Set("thunking_manual", InitThunkingManual(env));
Expand Down
2 changes: 2 additions & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
'threadsafe_function/threadsafe_function.cc',
'typedarray.cc',
'objectwrap.cc',
'objectwrap_constructor_exception.cc',
'objectwrap-removewrap.cc',
'objectreference.cc',
'version_management.cc',
'thunking_manual.cc',
Expand Down
2 changes: 2 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ let testModules = [
'typedarray',
'typedarray-bigint',
'objectwrap',
'objectwrap_constructor_exception',
'objectwrap-removewrap',
'objectreference',
'version_management'
];
Expand Down
45 changes: 45 additions & 0 deletions test/objectwrap-removewrap.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <napi.h>
#include <assert.h>

namespace {

static int dtor_called = 0;

class DtorCounter {
public:
~DtorCounter() {
assert(dtor_called == 0);
dtor_called++;
}
};

Napi::Value GetDtorCalled(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), dtor_called);
}

class Test : public Napi::ObjectWrap<Test> {
public:
Test(const Napi::CallbackInfo& info) : Napi::ObjectWrap<Test>(info) {
#ifdef NAPI_CPP_EXCEPTIONS
throw Napi::Error::New(Env(), "Some error");
#else
Napi::Error::New(Env(), "Some error").ThrowAsJavaScriptException();
#endif
}

static void Initialize(Napi::Env env, Napi::Object exports) {
exports.Set("Test", DefineClass(env, "Test", {}));
exports.Set("getDtorCalled", Napi::Function::New(env, GetDtorCalled));
}

private:
DtorCounter dtor_counter_;
};

} // anonymous namespace

Napi::Object InitObjectWrapRemoveWrap(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
Test::Initialize(env, exports);
return exports;
}
35 changes: 35 additions & 0 deletions test/objectwrap-removewrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

if (process.argv[2] === 'child') {
// Create a single wrapped instance then exit.
return new (require(process.argv[3]).objectwrap.Test)();
}

const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');
const { spawnSync } = require('child_process');

const test = (bindingName) => {
const binding = require(bindingName);
const Test = binding.objectwrap_removewrap.Test;
const getDtorCalled = binding.objectwrap_removewrap.getDtorCalled;

assert.strictEqual(getDtorCalled(), 0);
assert.throws(() => {
new Test();
});
assert.strictEqual(getDtorCalled(), 1);
global.gc(); // Does not crash.

// Start a child process that creates a single wrapped instance to ensure that
// it is properly freed at its exit. It must not segfault.
// Re: https://github.com/nodejs/node-addon-api/issues/660
const child = spawnSync(process.execPath, [
__filename, 'child', bindingName
]);
assert.strictEqual(child.signal, null);
assert.strictEqual(child.status, 0);
}

test(`./build/${buildType}/binding.node`);
test(`./build/${buildType}/binding_noexcept.node`);
3 changes: 3 additions & 0 deletions test/objectwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ const test = (binding) => {
// `Test` is needed for accessing exposed symbols
testObj(new Test(), Test);
testClass(Test);

// Make sure the C++ object can be garbage collected without issues.
setImmediate(global.gc);
}

test(require(`./build/${buildType}/binding.node`));
Expand Down
27 changes: 27 additions & 0 deletions test/objectwrap_constructor_exception.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include <napi.h>

class ConstructorExceptionTest :
public Napi::ObjectWrap<ConstructorExceptionTest> {
public:
ConstructorExceptionTest(const Napi::CallbackInfo& info) :
Napi::ObjectWrap<ConstructorExceptionTest>(info) {
fprintf(stderr, "That was the interesting reference\n");
Napi::Error error = Napi::Error::New(info.Env(), "an exception");
#ifdef NAPI_DISABLE_CPP_EXCEPTIONS
error.ThrowAsJavaScriptException();
#else
throw error;
#endif // NAPI_DISABLE_CPP_EXCEPTIONS
}

static void Initialize(Napi::Env env, Napi::Object exports) {
const char* name = "ConstructorExceptionTest";
exports.Set(name, DefineClass(env, name, {}));
}
};

Napi::Object InitObjectWrapConstructorException(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
ConstructorExceptionTest::Initialize(env, exports);
return exports;
}
12 changes: 12 additions & 0 deletions test/objectwrap_constructor_exception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');

const test = (binding) => {
const { ConstructorExceptionTest } = binding.objectwrapConstructorException;
assert.throws(() => (new ConstructorExceptionTest()), /an exception/);
global.gc();
}

test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding_noexcept.node`));