Skip to content

Commit 39063b3

Browse files
addaleaxBridgeAR
authored andcommitted
n-api: defer Buffer finalizer with SetImmediate
We have a test that verifies that JS execution from the Buffer finalizer is accepted, and that errors thrown are passed down synchronously. However, since the finalizer executes during GC, this is behaviour is fundamentally invalid and, for good reasons, disallowed by the JS engine. This leaves us with the options of either finding a way to allow JS execution from the callback, or explicitly forbidding it on the N-API side as well. This commit implements the former option, since it is the more backwards-compatible one, in the sense that the current situation sometimes appears to work as well and we should not break that behaviour if we don’t have to, but rather try to actually make it work reliably. Since GC timing is largely unobservable anyway, this commit moves the callback into a `SetImmediate()`, as we do elsewhere in the code, and a second pass callback is not an easily implemented option, as the API is supposed to wrap around Node’s `Buffer` API. In this case, exceptions are handled like other uncaught exceptions. Two tests have to be adjusted to account for the timing difference. This is unfortunate, but unavoidable if we want to conform to the JS engine API contract and keep all tests. Fixes: #26754 PR-URL: #28082 Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent b9d21e5 commit 39063b3

File tree

3 files changed

+43
-24
lines changed

3 files changed

+43
-24
lines changed

src/node_api.cc

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,30 @@ namespace v8impl {
3232

3333
namespace {
3434

35-
class BufferFinalizer: private Finalizer {
35+
class BufferFinalizer : private Finalizer {
3636
public:
3737
// node::Buffer::FreeCallback
3838
static void FinalizeBufferCallback(char* data, void* hint) {
3939
BufferFinalizer* finalizer = static_cast<BufferFinalizer*>(hint);
40-
if (finalizer->_finalize_callback != nullptr) {
41-
NapiCallIntoModuleThrow(finalizer->_env, [&]() {
42-
finalizer->_finalize_callback(
43-
finalizer->_env,
44-
data,
45-
finalizer->_finalize_hint);
46-
});
47-
}
40+
finalizer->_finalize_data = data;
41+
static_cast<node_napi_env>(finalizer->_env)->node_env()
42+
->SetImmediate([](node::Environment* env, void* hint) {
43+
BufferFinalizer* finalizer = static_cast<BufferFinalizer*>(hint);
44+
45+
if (finalizer->_finalize_callback != nullptr) {
46+
v8::HandleScope handle_scope(finalizer->_env->isolate);
47+
v8::Context::Scope context_scope(finalizer->_env->context());
48+
49+
NapiCallIntoModuleThrow(finalizer->_env, [&]() {
50+
finalizer->_finalize_callback(
51+
finalizer->_env,
52+
finalizer->_finalize_data,
53+
finalizer->_finalize_hint);
54+
});
55+
}
4856

49-
Delete(finalizer);
57+
Delete(finalizer);
58+
}, hint);
5059
}
5160
};
5261

test/node-api/test_buffer/test.js

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,25 @@
44
const common = require('../../common');
55
const binding = require(`./build/${common.buildType}/test_buffer`);
66
const assert = require('assert');
7+
const setImmediatePromise = require('util').promisify(setImmediate);
78

8-
assert.strictEqual(binding.newBuffer().toString(), binding.theText);
9-
assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText);
10-
console.log('gc1');
11-
global.gc();
12-
assert.strictEqual(binding.getDeleterCallCount(), 1);
13-
assert.strictEqual(binding.copyBuffer().toString(), binding.theText);
9+
(async function() {
10+
assert.strictEqual(binding.newBuffer().toString(), binding.theText);
11+
assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText);
12+
console.log('gc1');
13+
global.gc();
14+
assert.strictEqual(binding.getDeleterCallCount(), 0);
15+
await setImmediatePromise();
16+
assert.strictEqual(binding.getDeleterCallCount(), 1);
17+
assert.strictEqual(binding.copyBuffer().toString(), binding.theText);
1418

15-
let buffer = binding.staticBuffer();
16-
assert.strictEqual(binding.bufferHasInstance(buffer), true);
17-
assert.strictEqual(binding.bufferInfo(buffer), true);
18-
buffer = null;
19-
global.gc();
20-
console.log('gc2');
21-
assert.strictEqual(binding.getDeleterCallCount(), 2);
19+
let buffer = binding.staticBuffer();
20+
assert.strictEqual(binding.bufferHasInstance(buffer), true);
21+
assert.strictEqual(binding.bufferInfo(buffer), true);
22+
buffer = null;
23+
global.gc();
24+
assert.strictEqual(binding.getDeleterCallCount(), 1);
25+
await setImmediatePromise();
26+
console.log('gc2');
27+
assert.strictEqual(binding.getDeleterCallCount(), 2);
28+
})().then(common.mustCall());

test/node-api/test_exception/test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ const test_exception = require(`./build/${common.buildType}/test_exception`);
99
function testFinalize(binding) {
1010
let x = test_exception[binding]();
1111
x = null;
12-
assert.throws(() => { global.gc(); }, /Error during Finalize/);
12+
global.gc();
13+
process.on('uncaughtException', (err) => {
14+
assert.strictEqual(err.message, 'Error during Finalize');
15+
});
1316

1417
// To assuage the linter's concerns.
1518
(function() {})(x);

0 commit comments

Comments
 (0)