Skip to content

Commit 85435af

Browse files
kevindavies8blagoev
andcommitted
objectwrap: gracefully handle constructor exceptions
Ensure that no native instance pointer is associated with the JavaScript object under construction if the native constructor causes a JavaScript exception to be thrown. Two different cases must be taken into consideration: 1. The exception in the constructor was caused by `ObjectWrap<T>::ObjectWrap` when the call to `napi_wrap()` failed. 2. The exception in the constructor was caused by the constructor of the subclass of `ObjectWrap<T>` after `napi_wrap()` was already successful. Fixes: nodejs/node-addon-api#599 Co-authored-by: blagoev <[email protected]> PR-URL: nodejs/node-addon-api#600 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent 17dfdfe commit 85435af

File tree

7 files changed

+99
-5
lines changed

7 files changed

+99
-5
lines changed

napi-inl.h

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,6 +2702,37 @@ inline Object FunctionReference::New(const std::vector<napi_value>& args) const
27022702
// CallbackInfo class
27032703
////////////////////////////////////////////////////////////////////////////////
27042704

2705+
class ObjectWrapConstructionContext {
2706+
public:
2707+
ObjectWrapConstructionContext(CallbackInfo* info) {
2708+
info->_objectWrapConstructionContext = this;
2709+
}
2710+
2711+
static inline void SetObjectWrapped(const CallbackInfo& info) {
2712+
if (info._objectWrapConstructionContext == nullptr) {
2713+
Napi::Error::Fatal("ObjectWrapConstructionContext::SetObjectWrapped",
2714+
"_objectWrapConstructionContext is NULL");
2715+
}
2716+
info._objectWrapConstructionContext->_objectWrapped = true;
2717+
}
2718+
2719+
inline void Cleanup(const CallbackInfo& info) {
2720+
if (_objectWrapped) {
2721+
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);
2722+
2723+
// There's already a pending exception if we are at this point, so we have
2724+
// no choice but to fatally fail here.
2725+
NAPI_FATAL_IF_FAILED(status,
2726+
"ObjectWrapConstructionContext::Cleanup",
2727+
"Failed to remove wrap from unsuccessfully "
2728+
"constructed ObjectWrap instance");
2729+
}
2730+
}
2731+
2732+
private:
2733+
bool _objectWrapped = false;
2734+
};
2735+
27052736
inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info)
27062737
: _env(env), _info(info), _this(nullptr), _dynamicArgs(nullptr), _data(nullptr) {
27072738
_argc = _staticArgCount;
@@ -3106,11 +3137,11 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
31063137
napi_value wrapper = callbackInfo.This();
31073138
napi_status status;
31083139
napi_ref ref;
3109-
T* instance = static_cast<T*>(this);
3110-
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
3140+
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
31113141
NAPI_THROW_IF_FAILED_VOID(env, status);
31123142

3113-
Reference<Object>* instanceRef = instance;
3143+
ObjectWrapConstructionContext::SetObjectWrapped(callbackInfo);
3144+
Reference<Object>* instanceRef = this;
31143145
*instanceRef = Reference<Object>(env, ref);
31153146
}
31163147

@@ -3683,10 +3714,27 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
36833714
return nullptr;
36843715
}
36853716

3686-
T* instance;
36873717
napi_value wrapper = details::WrapCallback([&] {
36883718
CallbackInfo callbackInfo(env, info);
3689-
instance = new T(callbackInfo);
3719+
ObjectWrapConstructionContext constructionContext(&callbackInfo);
3720+
#ifdef NAPI_CPP_EXCEPTIONS
3721+
try {
3722+
new T(callbackInfo);
3723+
} catch (const Error& e) {
3724+
// Re-throw the error after removing the failed wrap.
3725+
constructionContext.Cleanup(callbackInfo);
3726+
throw e;
3727+
}
3728+
#else
3729+
T* instance = new T(callbackInfo);
3730+
if (callbackInfo.Env().IsExceptionPending()) {
3731+
// We need to clear the exception so that removing the wrap might work.
3732+
Error e = callbackInfo.Env().GetAndClearPendingException();
3733+
constructionContext.Cleanup(callbackInfo);
3734+
e.ThrowAsJavaScriptException();
3735+
delete instance;
3736+
}
3737+
# endif // NAPI_CPP_EXCEPTIONS
36903738
return callbackInfo.This();
36913739
});
36923740

napi.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ namespace Napi {
138138
class CallbackInfo;
139139
class TypedArray;
140140
template <typename T> class TypedArrayOf;
141+
class ObjectWrapConstructionContext;
141142

142143
typedef TypedArrayOf<int8_t> Int8Array; ///< Typed-array of signed 8-bit integers
143144
typedef TypedArrayOf<uint8_t> Uint8Array; ///< Typed-array of unsigned 8-bit integers
@@ -1402,6 +1403,7 @@ namespace Napi {
14021403

14031404
class CallbackInfo {
14041405
public:
1406+
friend class ObjectWrapConstructionContext;
14051407
CallbackInfo(napi_env env, napi_callback_info info);
14061408
~CallbackInfo();
14071409

@@ -1427,6 +1429,7 @@ namespace Napi {
14271429
napi_value _staticArgs[6];
14281430
napi_value* _dynamicArgs;
14291431
void* _data;
1432+
ObjectWrapConstructionContext* _objectWrapConstructionContext;
14301433
};
14311434

14321435
class PropertyDescriptor {

test/binding.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Object InitThreadSafeFunction(Env env);
5050
#endif
5151
Object InitTypedArray(Env env);
5252
Object InitObjectWrap(Env env);
53+
Object InitObjectWrapConstructorException(Env env);
5354
Object InitObjectReference(Env env);
5455
Object InitVersionManagement(Env env);
5556
Object InitThunkingManual(Env env);
@@ -104,6 +105,8 @@ Object Init(Env env, Object exports) {
104105
#endif
105106
exports.Set("typedarray", InitTypedArray(env));
106107
exports.Set("objectwrap", InitObjectWrap(env));
108+
exports.Set("objectwrapConstructorException",
109+
InitObjectWrapConstructorException(env));
107110
exports.Set("objectreference", InitObjectReference(env));
108111
exports.Set("version_management", InitVersionManagement(env));
109112
exports.Set("thunking_manual", InitThunkingManual(env));

test/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
'threadsafe_function/threadsafe_function.cc',
4242
'typedarray.cc',
4343
'objectwrap.cc',
44+
'objectwrap_constructor_exception.cc',
4445
'objectreference.cc',
4546
'version_management.cc',
4647
'thunking_manual.cc',

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ let testModules = [
4949
'typedarray',
5050
'typedarray-bigint',
5151
'objectwrap',
52+
'objectwrap_constructor_exception',
5253
'objectreference',
5354
'version_management'
5455
];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
Napi::Error error = Napi::Error::New(info.Env(), "an exception");
9+
#ifdef NAPI_DISABLE_CPP_EXCEPTIONS
10+
error.ThrowAsJavaScriptException();
11+
#else
12+
throw error;
13+
#endif // NAPI_DISABLE_CPP_EXCEPTIONS
14+
}
15+
16+
static void Initialize(Napi::Env env, Napi::Object exports) {
17+
const char* name = "ConstructorExceptionTest";
18+
exports.Set(name, DefineClass(env, name, {}));
19+
}
20+
};
21+
22+
Napi::Object InitObjectWrapConstructorException(Napi::Env env) {
23+
Napi::Object exports = Napi::Object::New(env);
24+
ConstructorExceptionTest::Initialize(env, exports);
25+
return exports;
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
const buildType = process.config.target_defaults.default_configuration;
3+
const assert = require('assert');
4+
5+
const test = (binding) => {
6+
const { ConstructorExceptionTest } = binding.objectwrapConstructorException;
7+
assert.throws(() => (new ConstructorExceptionTest()), /an exception/);
8+
global.gc();
9+
}
10+
11+
test(require(`./build/${buildType}/binding.node`));
12+
test(require(`./build/${buildType}/binding_noexcept.node`));

0 commit comments

Comments
 (0)