buffer,n-api: properly handle Environment cleanup by addaleax · Pull Request #30551 · nodejs/node · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions src/js_native_api_v8.cc
45 changes: 36 additions & 9 deletions src/node_buffer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ using v8::Context;
using v8::EscapableHandleScope;
using v8::FunctionCallbackInfo;
using v8::Global;
using v8::HandleScope;
using v8::Int32;
using v8::Integer;
using v8::Isolate;
Expand All @@ -73,8 +74,10 @@ namespace {

class CallbackInfo {
public:
~CallbackInfo();

static inline void Free(char* data, void* hint);
static inline CallbackInfo* New(Isolate* isolate,
static inline CallbackInfo* New(Environment* env,
Local<ArrayBuffer> object,
FreeCallback callback,
char* data,
Expand All @@ -84,9 +87,10 @@ class CallbackInfo {
CallbackInfo& operator=(const CallbackInfo&) = delete;

private:
static void CleanupHook(void* data);
static void WeakCallback(const WeakCallbackInfo<CallbackInfo>&);
inline void WeakCallback(Isolate* isolate);
inline CallbackInfo(Isolate* isolate,
inline CallbackInfo(Environment* env,
Local<ArrayBuffer> object,
FreeCallback callback,
char* data,
Expand All @@ -95,6 +99,7 @@ class CallbackInfo {
FreeCallback const callback_;
char* const data_;
void* const hint_;
Environment* const env_;
};


Expand All @@ -103,31 +108,53 @@ void CallbackInfo::Free(char* data, void*) {
}


CallbackInfo* CallbackInfo::New(Isolate* isolate,
CallbackInfo* CallbackInfo::New(Environment* env,
Local<ArrayBuffer> object,
FreeCallback callback,
char* data,
void* hint) {
return new CallbackInfo(isolate, object, callback, data, hint);
return new CallbackInfo(env, object, callback, data, hint);
}


CallbackInfo::CallbackInfo(Isolate* isolate,
CallbackInfo::CallbackInfo(Environment* env,
Local<ArrayBuffer> object,
FreeCallback callback,
char* data,
void* hint)
: persistent_(isolate, object),
: persistent_(env->isolate(), object),
callback_(callback),
data_(data),
hint_(hint) {
hint_(hint),
env_(env) {
ArrayBuffer::Contents obj_c = object->GetContents();
CHECK_EQ(data_, static_cast<char*>(obj_c.Data()));
if (object->ByteLength() != 0)
CHECK_NOT_NULL(data_);

persistent_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
isolate->AdjustAmountOfExternalAllocatedMemory(sizeof(*this));
env->AddCleanupHook(CleanupHook, this);
env->isolate()->AdjustAmountOfExternalAllocatedMemory(sizeof(*this));
}


CallbackInfo::~CallbackInfo() {
persistent_.Reset();
env_->RemoveCleanupHook(CleanupHook, this);
}


void CallbackInfo::CleanupHook(void* data) {
CallbackInfo* self = static_cast<CallbackInfo*>(data);

{
HandleScope handle_scope(self->env_->isolate());
Local<ArrayBuffer> ab = self->persistent_.Get(self->env_->isolate());
CHECK(!ab.IsEmpty());
ab->Detach();
}

self->WeakCallback(self->env_->isolate());
}


Expand Down Expand Up @@ -388,7 +415,7 @@ MaybeLocal<Object> New(Environment* env,
}
MaybeLocal<Uint8Array> ui = Buffer::New(env, ab, 0, length);

CallbackInfo::New(env->isolate(), ab, callback, data, hint);
CallbackInfo::New(env, ab, callback, data, hint);

if (ui.IsEmpty())
return MaybeLocal<Object>();
Expand Down
11 changes: 10 additions & 1 deletion test/addons/worker-buffer-callback/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
#include <v8.h>

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Value;

uint32_t free_call_count = 0;
char data[] = "hello";

void GetFreeCallCount(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(free_call_count);
}

void Initialize(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
Isolate* isolate = context->GetIsolate();
NODE_SET_METHOD(exports, "getFreeCallCount", GetFreeCallCount);
exports->Set(context,
v8::String::NewFromUtf8(
isolate, "buffer", v8::NewStringType::kNormal)
Expand All @@ -22,7 +29,9 @@ void Initialize(Local<Object> exports,
isolate,
data,
sizeof(data),
[](char* data, void* hint) {},
[](char* data, void* hint) {
free_call_count++;
},
nullptr).ToLocalChecked()).Check();
}

Expand Down
17 changes: 17 additions & 0 deletions test/addons/worker-buffer-callback/test-free-called.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';
const common = require('../../common');
const path = require('path');
const assert = require('assert');
const { Worker } = require('worker_threads');
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
const { getFreeCallCount } = require(binding);

// Test that buffers allocated with a free callback through our APIs are
// released when a Worker owning it exits.

const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });

assert.strictEqual(getFreeCallCount(), 0);
w.on('exit', common.mustCall(() => {
assert.strictEqual(getFreeCallCount(), 1);
}));
32 changes: 32 additions & 0 deletions test/cctest/test_environment.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "node_buffer.h"
#include "node_internals.h"
#include "libplatform/libplatform.h"

Expand Down Expand Up @@ -208,3 +209,34 @@ TEST_F(EnvironmentTest, SetImmediateCleanup) {
EXPECT_EQ(called, 1);
EXPECT_EQ(called_unref, 0);
}

static char hello[] = "hello";

TEST_F(EnvironmentTest, BufferWithFreeCallbackIsDetached) {
// Test that a Buffer allocated with a free callback is detached after
// its callback has been called.
const v8::HandleScope handle_scope(isolate_);
const Argv argv;

int callback_calls = 0;

v8::Local<v8::ArrayBuffer> ab;
{
Env env {handle_scope, argv};
v8::Local<v8::Object> buf_obj = node::Buffer::New(
isolate_,
hello,
sizeof(hello),
[](char* data, void* hint) {
CHECK_EQ(data, hello);
++*static_cast<int*>(hint);
},
&callback_calls).ToLocalChecked();
CHECK(buf_obj->IsUint8Array());
ab = buf_obj.As<v8::Uint8Array>()->Buffer();
CHECK_EQ(ab->ByteLength(), sizeof(hello));
}

CHECK_EQ(callback_calls, 1);
CHECK_EQ(ab->ByteLength(), 0);
}
8 changes: 8 additions & 0 deletions test/node-api/test_worker_buffer_callback/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'test_worker_buffer_callback.c' ]
}
]
}
17 changes: 17 additions & 0 deletions test/node-api/test_worker_buffer_callback/test-free-called.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';
const common = require('../../common');
const path = require('path');
const assert = require('assert');
const { Worker } = require('worker_threads');
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
const { getFreeCallCount } = require(binding);

// Test that buffers allocated with a free callback through our APIs are
// released when a Worker owning it exits.

const w = new Worker(`require(${JSON.stringify(binding)})`, { eval: true });

assert.strictEqual(getFreeCallCount(), 0);
w.on('exit', common.mustCall(() => {
assert.strictEqual(getFreeCallCount(), 1);
}));
15 changes: 15 additions & 0 deletions test/node-api/test_worker_buffer_callback/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const { MessageChannel } = require('worker_threads');
const { buffer } = require(`./build/${common.buildType}/binding`);

// Test that buffers allocated with a free callback through our APIs are not
// transferred.

const { port1 } = new MessageChannel();
const origByteLength = buffer.byteLength;
port1.postMessage(buffer, [buffer]);

assert.strictEqual(buffer.byteLength, origByteLength);
assert.notStrictEqual(buffer.byteLength, 0);