Skip to content

Commit

Permalink
n-api: defer Buffer finalizer with SetImmediate
Browse files Browse the repository at this point in the history
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 <michael_dawson@ca.ibm.com>
Reviewed-By: Gabriel Schulhof <gabriel.schulhof@intel.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
addaleax authored and targos committed Jun 18, 2019
1 parent a599815 commit a40cfb3
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 24 deletions.
29 changes: 19 additions & 10 deletions src/node_api.cc
Expand Up @@ -32,21 +32,30 @@ namespace v8impl {

namespace {

class BufferFinalizer: private Finalizer {
class BufferFinalizer : private Finalizer {
public:
// node::Buffer::FreeCallback
static void FinalizeBufferCallback(char* data, void* hint) {
BufferFinalizer* finalizer = static_cast<BufferFinalizer*>(hint);
if (finalizer->_finalize_callback != nullptr) {
NapiCallIntoModuleThrow(finalizer->_env, [&]() {
finalizer->_finalize_callback(
finalizer->_env,
data,
finalizer->_finalize_hint);
});
}
finalizer->_finalize_data = data;
static_cast<node_napi_env>(finalizer->_env)->node_env()
->SetImmediate([](node::Environment* env, void* hint) {
BufferFinalizer* finalizer = static_cast<BufferFinalizer*>(hint);

if (finalizer->_finalize_callback != nullptr) {
v8::HandleScope handle_scope(finalizer->_env->isolate);
v8::Context::Scope context_scope(finalizer->_env->context());

NapiCallIntoModuleThrow(finalizer->_env, [&]() {
finalizer->_finalize_callback(
finalizer->_env,
finalizer->_finalize_data,
finalizer->_finalize_hint);
});
}

Delete(finalizer);
Delete(finalizer);
}, hint);
}
};

Expand Down
33 changes: 20 additions & 13 deletions test/node-api/test_buffer/test.js
Expand Up @@ -4,18 +4,25 @@
const common = require('../../common');
const binding = require(`./build/${common.buildType}/test_buffer`);
const assert = require('assert');
const setImmediatePromise = require('util').promisify(setImmediate);

assert.strictEqual(binding.newBuffer().toString(), binding.theText);
assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText);
console.log('gc1');
global.gc();
assert.strictEqual(binding.getDeleterCallCount(), 1);
assert.strictEqual(binding.copyBuffer().toString(), binding.theText);
(async function() {
assert.strictEqual(binding.newBuffer().toString(), binding.theText);
assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText);
console.log('gc1');
global.gc();
assert.strictEqual(binding.getDeleterCallCount(), 0);
await setImmediatePromise();
assert.strictEqual(binding.getDeleterCallCount(), 1);
assert.strictEqual(binding.copyBuffer().toString(), binding.theText);

let buffer = binding.staticBuffer();
assert.strictEqual(binding.bufferHasInstance(buffer), true);
assert.strictEqual(binding.bufferInfo(buffer), true);
buffer = null;
global.gc();
console.log('gc2');
assert.strictEqual(binding.getDeleterCallCount(), 2);
let buffer = binding.staticBuffer();
assert.strictEqual(binding.bufferHasInstance(buffer), true);
assert.strictEqual(binding.bufferInfo(buffer), true);
buffer = null;
global.gc();
assert.strictEqual(binding.getDeleterCallCount(), 1);
await setImmediatePromise();
console.log('gc2');
assert.strictEqual(binding.getDeleterCallCount(), 2);
})().then(common.mustCall());
5 changes: 4 additions & 1 deletion test/node-api/test_exception/test.js
Expand Up @@ -9,7 +9,10 @@ const test_exception = require(`./build/${common.buildType}/test_exception`);
function testFinalize(binding) {
let x = test_exception[binding]();
x = null;
assert.throws(() => { global.gc(); }, /Error during Finalize/);
global.gc();
process.on('uncaughtException', (err) => {
assert.strictEqual(err.message, 'Error during Finalize');
});

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

0 comments on commit a40cfb3

Please sign in to comment.