Skip to content

Commit 663d627

Browse files
Gabriel Schulhofblagoev
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#599 Co-authored-by: blagoev <[email protected]> PR-URL: nodejs#600 Reviewed-By: Chengzhong Wu <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent f677794 commit 663d627

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
@@ -2666,6 +2666,37 @@ inline Object FunctionReference::New(const std::vector<napi_value>& args) const
26662666
// CallbackInfo class
26672667
////////////////////////////////////////////////////////////////////////////////
26682668

2669+
class ObjectWrapConstructionContext {
2670+
public:
2671+
ObjectWrapConstructionContext(CallbackInfo* info) {
2672+
info->_objectWrapConstructionContext = this;
2673+
}
2674+
2675+
static inline void SetObjectWrapped(const CallbackInfo& info) {
2676+
if (info._objectWrapConstructionContext == nullptr) {
2677+
Napi::Error::Fatal("ObjectWrapConstructionContext::SetObjectWrapped",
2678+
"_objectWrapConstructionContext is NULL");
2679+
}
2680+
info._objectWrapConstructionContext->_objectWrapped = true;
2681+
}
2682+
2683+
inline void Cleanup(const CallbackInfo& info) {
2684+
if (_objectWrapped) {
2685+
napi_status status = napi_remove_wrap(info.Env(), info.This(), nullptr);
2686+
2687+
// There's already a pending exception if we are at this point, so we have
2688+
// no choice but to fatally fail here.
2689+
NAPI_FATAL_IF_FAILED(status,
2690+
"ObjectWrapConstructionContext::Cleanup",
2691+
"Failed to remove wrap from unsuccessfully "
2692+
"constructed ObjectWrap instance");
2693+
}
2694+
}
2695+
2696+
private:
2697+
bool _objectWrapped = false;
2698+
};
2699+
26692700
inline CallbackInfo::CallbackInfo(napi_env env, napi_callback_info info)
26702701
: _env(env), _info(info), _this(nullptr), _dynamicArgs(nullptr), _data(nullptr) {
26712702
_argc = _staticArgCount;
@@ -2968,11 +2999,11 @@ inline ObjectWrap<T>::ObjectWrap(const Napi::CallbackInfo& callbackInfo) {
29682999
napi_value wrapper = callbackInfo.This();
29693000
napi_status status;
29703001
napi_ref ref;
2971-
T* instance = static_cast<T*>(this);
2972-
status = napi_wrap(env, wrapper, instance, FinalizeCallback, nullptr, &ref);
3002+
status = napi_wrap(env, wrapper, this, FinalizeCallback, nullptr, &ref);
29733003
NAPI_THROW_IF_FAILED_VOID(env, status);
29743004

2975-
Reference<Object>* instanceRef = instance;
3005+
ObjectWrapConstructionContext::SetObjectWrapped(callbackInfo);
3006+
Reference<Object>* instanceRef = this;
29763007
*instanceRef = Reference<Object>(env, ref);
29773008
}
29783009

@@ -3369,10 +3400,27 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(
33693400
return nullptr;
33703401
}
33713402

3372-
T* instance;
33733403
napi_value wrapper = details::WrapCallback([&] {
33743404
CallbackInfo callbackInfo(env, info);
3375-
instance = new T(callbackInfo);
3405+
ObjectWrapConstructionContext constructionContext(&callbackInfo);
3406+
#ifdef NAPI_CPP_EXCEPTIONS
3407+
try {
3408+
new T(callbackInfo);
3409+
} catch (const Error& e) {
3410+
// Re-throw the error after removing the failed wrap.
3411+
constructionContext.Cleanup(callbackInfo);
3412+
throw e;
3413+
}
3414+
#else
3415+
T* instance = new T(callbackInfo);
3416+
if (callbackInfo.Env().IsExceptionPending()) {
3417+
// We need to clear the exception so that removing the wrap might work.
3418+
Error e = callbackInfo.Env().GetAndClearPendingException();
3419+
constructionContext.Cleanup(callbackInfo);
3420+
e.ThrowAsJavaScriptException();
3421+
delete instance;
3422+
}
3423+
# endif // NAPI_CPP_EXCEPTIONS
33763424
return callbackInfo.This();
33773425
});
33783426

napi.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ namespace Napi {
132132
template <typename T> class Reference;
133133
class TypedArray;
134134
template <typename T> class TypedArrayOf;
135+
class ObjectWrapConstructionContext;
135136

136137
typedef TypedArrayOf<int8_t> Int8Array; ///< Typed-array of signed 8-bit integers
137138
typedef TypedArrayOf<uint8_t> Uint8Array; ///< Typed-array of unsigned 8-bit integers
@@ -1377,6 +1378,7 @@ namespace Napi {
13771378

13781379
class CallbackInfo {
13791380
public:
1381+
friend class ObjectWrapConstructionContext;
13801382
CallbackInfo(napi_env env, napi_callback_info info);
13811383
~CallbackInfo();
13821384

@@ -1403,6 +1405,7 @@ namespace Napi {
14031405
napi_value _staticArgs[6];
14041406
napi_value* _dynamicArgs;
14051407
void* _data;
1408+
ObjectWrapConstructionContext* _objectWrapConstructionContext;
14061409
};
14071410

14081411
class PropertyDescriptor {

test/binding.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Object InitThreadSafeFunction(Env env);
4949
#endif
5050
Object InitTypedArray(Env env);
5151
Object InitObjectWrap(Env env);
52+
Object InitObjectWrapConstructorException(Env env);
5253
Object InitObjectReference(Env env);
5354
Object InitVersionManagement(Env env);
5455
Object InitThunkingManual(Env env);
@@ -102,6 +103,8 @@ Object Init(Env env, Object exports) {
102103
#endif
103104
exports.Set("typedarray", InitTypedArray(env));
104105
exports.Set("objectwrap", InitObjectWrap(env));
106+
exports.Set("objectwrapConstructorException",
107+
InitObjectWrapConstructorException(env));
105108
exports.Set("objectreference", InitObjectReference(env));
106109
exports.Set("version_management", InitVersionManagement(env));
107110
exports.Set("thunking_manual", InitThunkingManual(env));

test/binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
'threadsafe_function/threadsafe_function.cc',
4545
'typedarray.cc',
4646
'objectwrap.cc',
47+
'objectwrap_constructor_exception.cc',
4748
'objectreference.cc',
4849
'version_management.cc',
4950
'thunking_manual.cc',

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ let testModules = [
4848
'typedarray',
4949
'typedarray-bigint',
5050
'objectwrap',
51+
'objectwrap_constructor_exception',
5152
'objectreference',
5253
'version_management'
5354
];
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)