Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: fix ObjectWrap destruction #475

Closed
wants to merge 1 commit into from
Closed
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
60 changes: 15 additions & 45 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2702,37 +2702,6 @@ inline Object FunctionReference::New(const std::vector<napi_value>& args) const
// CallbackInfo class
////////////////////////////////////////////////////////////////////////////////

class ObjectWrapConstructionContext {
public:
ObjectWrapConstructionContext(CallbackInfo* info) {
info->_objectWrapConstructionContext = this;
}

static inline void SetObjectWrapped(const CallbackInfo& info) {
if (info._objectWrapConstructionContext == nullptr) {
Napi::Error::Fatal("ObjectWrapConstructionContext::SetObjectWrapped",
"_objectWrapConstructionContext is NULL");
}
info._objectWrapConstructionContext->_objectWrapped = true;
}

inline void Cleanup(const CallbackInfo& info) {
if (_objectWrapped) {
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);

// There's already a pending exception if we are at this point, so we have
// no choice but to fatally fail here.
NAPI_FATAL_IF_FAILED(status,
"ObjectWrapConstructionContext::Cleanup",
"Failed to remove wrap from unsuccessfully "
"constructed ObjectWrap instance");
}
}

private:
bool _objectWrapped = false;
};

inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info)
: _env(env), _info(info), _this(nullptr), _dynamicArgs(nullptr), _data(nullptr) {
_argc = _staticArgCount;
Expand Down Expand Up @@ -3140,13 +3109,22 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
NAPI_THROW_IF_FAILED_VOID(env, status);

ObjectWrapConstructionContext::SetObjectWrapped(callbackInfo);
Reference<Object>* instanceRef = this;
*instanceRef = Reference<Object>(env, ref);
Copy link

Choose a reason for hiding this comment

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

These two statements though work, as intended but seem to be a little bit misleading. Having a fast peek over the code you could think that we re-move-assign the whole *this object.
I suggest making an explicit call to parent move-assignment operator, namely:

Reference::operator=({env, ref});

It is more laconic and clear from my point of view.

}

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())
Copy link
Contributor

Choose a reason for hiding this comment

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

This code could probably benefit from a comment

Copy link
Member Author

Choose a reason for hiding this comment

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

@digitalinfinity Yup, added a comment!

napi_remove_wrap(Env(), object, nullptr);
}
}

template<typename T>
inline T* ObjectWrap<T>::Unwrap(Object wrapper) {
Expand Down Expand Up @@ -3716,23 +3694,15 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(

napi_value wrapper = details::WrapCallback([&] {
CallbackInfo callbackInfo(env, info);
ObjectWrapConstructionContext constructionContext(&callbackInfo);
#ifdef NAPI_CPP_EXCEPTIONS
try {
new T(callbackInfo);
} catch (const Error& e) {
// Re-throw the error after removing the failed wrap.
constructionContext.Cleanup(callbackInfo);
throw e;
}
new T(callbackInfo);
#else
T* instance = new T(callbackInfo);
if (callbackInfo.Env().IsExceptionPending()) {
// We need to clear the exception so that removing the wrap might work.
Error e = callbackInfo.Env().GetAndClearPendingException();
constructionContext.Cleanup(callbackInfo);
e.ThrowAsJavaScriptException();
delete instance;
e.ThrowAsJavaScriptException();
}
# endif // NAPI_CPP_EXCEPTIONS
return callbackInfo.This();
Expand Down Expand Up @@ -3859,7 +3829,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);
Copy link

Choose a reason for hiding this comment

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

I propose using the auto keyword here in order to decrease verbosity. Or you could simply rename argument data to instance and do it in one statement:

inline void ObjectWrap<T>::FinalizeCallback(napi_env /*env*/, void * instance, void * /*hint*/) {
    delete static_cast<ObjectWrap<T>*>(instance);
}

Any of two options are alright from my viewpoint, though it may just be a matter of style.

instance->Finalize(Napi::Env(env));
delete instance;
}
Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Object InitThreadSafeFunction(Env env);
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 @@ -107,6 +108,7 @@ Object Init(Env env, Object exports) {
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
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'typedarray.cc',
'objectwrap.cc',
'objectwrap_constructor_exception.cc',
'objectwrap-removewrap.cc',
'objectreference.cc',
'version_management.cc',
'thunking_manual.cc',
Expand Down
1 change: 1 addition & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ let testModules = [
'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>

#ifdef NAPI_CPP_EXCEPTIONS
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) {
throw Napi::Error::New(Env(), "Some error");
}

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_ounter_;
};

} // anonymous namespace
#endif // NAPI_CPP_EXCEPTIONS

Napi::Object InitObjectWrapRemoveWrap(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
#ifdef NAPI_CPP_EXCEPTIONS
Test::Initialize(env, exports);
#endif
return exports;
}
17 changes: 17 additions & 0 deletions test/objectwrap-removewrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');

const test = (binding) => {
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.
}

test(require(`./build/${buildType}/binding.node`));
3 changes: 3 additions & 0 deletions test/objectwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,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