Skip to content

Commit cec2c76

Browse files
author
Gabriel Schulhof
committed
src: wrap finalizer callback
Make sure C++ exceptions thrown from a finalizer are converted into JS exceptions just as they are in regular callbacks. Signed-off-by: Gabriel Schulhof <[email protected]> PR-URL: #762 Reviewed-By: Michael Dawson <[email protected]> test: add finalizer exception test src: wrap finalizer callback
1 parent 4ce40d2 commit cec2c76

File tree

3 files changed

+94
-11
lines changed

3 files changed

+94
-11
lines changed

napi-inl.h

+30-8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ inline napi_value WrapCallback(Callable callback) {
8282
#endif // NAPI_CPP_EXCEPTIONS
8383
}
8484

85+
// For use in JS to C++ void callback wrappers to catch any Napi::Error
86+
// exceptions and rethrow them as JavaScript exceptions before returning from the
87+
// callback.
88+
template <typename Callable>
89+
inline void WrapVoidCallback(Callable callback) {
90+
#ifdef NAPI_CPP_EXCEPTIONS
91+
try {
92+
callback();
93+
} catch (const Error& e) {
94+
e.ThrowAsJavaScriptException();
95+
}
96+
#else // NAPI_CPP_EXCEPTIONS
97+
// When C++ exceptions are disabled, errors are immediately thrown as JS
98+
// exceptions, so there is no need to catch and rethrow them here.
99+
callback();
100+
#endif // NAPI_CPP_EXCEPTIONS
101+
}
102+
85103
template <typename Callable, typename Return>
86104
struct CallbackData {
87105
static inline
@@ -120,17 +138,21 @@ struct CallbackData<Callable, void> {
120138
template <typename T, typename Finalizer, typename Hint = void>
121139
struct FinalizeData {
122140
static inline
123-
void Wrapper(napi_env env, void* data, void* finalizeHint) {
124-
FinalizeData* finalizeData = static_cast<FinalizeData*>(finalizeHint);
125-
finalizeData->callback(Env(env), static_cast<T*>(data));
126-
delete finalizeData;
141+
void Wrapper(napi_env env, void* data, void* finalizeHint) noexcept {
142+
WrapVoidCallback([&] {
143+
FinalizeData* finalizeData = static_cast<FinalizeData*>(finalizeHint);
144+
finalizeData->callback(Env(env), static_cast<T*>(data));
145+
delete finalizeData;
146+
});
127147
}
128148

129149
static inline
130-
void WrapperWithHint(napi_env env, void* data, void* finalizeHint) {
131-
FinalizeData* finalizeData = static_cast<FinalizeData*>(finalizeHint);
132-
finalizeData->callback(Env(env), static_cast<T*>(data), finalizeData->hint);
133-
delete finalizeData;
150+
void WrapperWithHint(napi_env env, void* data, void* finalizeHint) noexcept {
151+
WrapVoidCallback([&] {
152+
FinalizeData* finalizeData = static_cast<FinalizeData*>(finalizeHint);
153+
finalizeData->callback(Env(env), static_cast<T*>(data), finalizeData->hint);
154+
delete finalizeData;
155+
});
134156
}
135157

136158
Finalizer callback;

test/external.cc

+15
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,28 @@ Value GetFinalizeCount(const CallbackInfo& info) {
5151
return Number::New(info.Env(), finalizeCount);
5252
}
5353

54+
Value CreateExternalWithFinalizeException(const CallbackInfo& info) {
55+
return External<int>::New(info.Env(), new int(1),
56+
[](Env env, int* data) {
57+
Error error = Error::New(env, "Finalizer exception");
58+
delete data;
59+
#ifdef NAPI_CPP_EXCEPTIONS
60+
throw error;
61+
#else
62+
error.ThrowAsJavaScriptException();
63+
#endif
64+
});
65+
}
66+
5467
} // end anonymous namespace
5568

5669
Object InitExternal(Env env) {
5770
Object exports = Object::New(env);
5871

5972
exports["createExternal"] = Function::New(env, CreateExternal);
6073
exports["createExternalWithFinalize"] = Function::New(env, CreateExternalWithFinalize);
74+
exports["createExternalWithFinalizeException"] =
75+
Function::New(env, CreateExternalWithFinalizeException);
6176
exports["createExternalWithFinalizeHint"] = Function::New(env, CreateExternalWithFinalizeHint);
6277
exports["checkExternal"] = Function::New(env, CheckExternal);
6378
exports["getFinalizeCount"] = Function::New(env, GetFinalizeCount);

test/external.js

+49-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
11
'use strict';
22
const buildType = process.config.target_defaults.default_configuration;
33
const assert = require('assert');
4+
const { spawnSync } = require('child_process');
45
const testUtil = require('./testUtil');
56

6-
module.exports = test(require(`./build/${buildType}/binding.node`))
7-
.then(() => test(require(`./build/${buildType}/binding_noexcept.node`)));
7+
if (process.argv.length === 3) {
8+
let interval;
9+
10+
// Running as the child process, hook up an `uncaughtException` handler to
11+
// examine the error thrown by the finalizer.
12+
process.on('uncaughtException', (error) => {
13+
// TODO (gabrielschulhof): Use assert.matches() when we drop support for
14+
// Node.js v10.x.
15+
assert(!!error.message.match(/Finalizer exception/));
16+
if (interval) {
17+
clearInterval(interval);
18+
}
19+
process.exit(0);
20+
});
21+
22+
// Create an external whose finalizer throws.
23+
(() =>
24+
require(process.argv[2]).external.createExternalWithFinalizeException())();
25+
26+
// gc until the external's finalizer throws or until we give up. Since the
27+
// exception is thrown from a native `SetImmediate()` we cannot catch it
28+
// anywhere except in the process' `uncaughtException` handler.
29+
let maxGCTries = 10;
30+
(function gcInterval() {
31+
global.gc();
32+
if (!interval) {
33+
interval = setInterval(gcInterval, 100);
34+
} else if (--maxGCTries === 0) {
35+
throw new Error('Timed out waiting for the gc to throw');
36+
process.exit(1);
37+
}
38+
})();
39+
40+
return;
41+
}
42+
43+
module.exports = test(require.resolve(`./build/${buildType}/binding.node`))
44+
.then(() =>
45+
test(require.resolve(`./build/${buildType}/binding_noexcept.node`)));
46+
47+
function test(bindingPath) {
48+
const binding = require(bindingPath);
49+
50+
const child = spawnSync(process.execPath, [
51+
'--expose-gc', __filename, bindingPath
52+
], { stdio: 'inherit' });
53+
assert.strictEqual(child.status, 0);
54+
assert.strictEqual(child.signal, null);
855

9-
function test(binding) {
1056
return testUtil.runGCTests([
1157
'External without finalizer',
1258
() => {

0 commit comments

Comments
 (0)