Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src,tools: initialize cppgc #45704

Merged
merged 1 commit into from Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/env-inl.h
Expand Up @@ -34,6 +34,7 @@
#include "node_realm-inl.h"
#include "util-inl.h"
#include "uv.h"
#include "v8-cppgc.h"
#include "v8.h"

#include <cstddef>
Expand Down Expand Up @@ -61,6 +62,31 @@ inline uv_loop_t* IsolateData::event_loop() const {
return event_loop_;
}

inline void IsolateData::SetCppgcReference(v8::Isolate* isolate,
v8::Local<v8::Object> object,
void* wrappable) {
v8::CppHeap* heap = isolate->GetCppHeap();
CHECK_NOT_NULL(heap);
v8::WrapperDescriptor descriptor = heap->wrapper_descriptor();
uint16_t required_size = std::max(descriptor.wrappable_instance_index,
descriptor.wrappable_type_index);
CHECK_GT(object->InternalFieldCount(), required_size);

uint16_t* id_ptr = nullptr;
{
Mutex::ScopedLock lock(isolate_data_mutex_);
auto it =
wrapper_data_map_.find(descriptor.embedder_id_for_garbage_collected);
CHECK_NE(it, wrapper_data_map_.end());
id_ptr = &(it->second->cppgc_id);
}

object->SetAlignedPointerInInternalField(descriptor.wrappable_type_index,
id_ptr);
object->SetAlignedPointerInInternalField(descriptor.wrappable_instance_index,
wrappable);
}

inline uint16_t* IsolateData::embedder_id_for_cppgc() const {
return &(wrapper_data_->cppgc_id);
}
Expand Down
26 changes: 26 additions & 0 deletions src/env.cc
Expand Up @@ -37,6 +37,8 @@ using errors::TryCatchScope;
using v8::Array;
using v8::Boolean;
using v8::Context;
using v8::CppHeap;
using v8::CppHeapCreateParams;
using v8::EmbedderGraph;
using v8::EscapableHandleScope;
using v8::Function;
Expand All @@ -61,6 +63,7 @@ using v8::TracingController;
using v8::TryCatch;
using v8::Undefined;
using v8::Value;
using v8::WrapperDescriptor;
using worker::Worker;

int const ContextEmbedderTag::kNodeContextTag = 0x6e6f64;
Expand Down Expand Up @@ -538,6 +541,14 @@ IsolateData::IsolateData(Isolate* isolate,
// for embedder ID, V8 could accidentally enable cppgc on them. So
// safe guard against this.
DCHECK_NE(descriptor.wrappable_type_index, BaseObject::kSlot);
} else {
cpp_heap_ = CppHeap::Create(
platform,
CppHeapCreateParams{
{},
WrapperDescriptor(
BaseObject::kEmbedderType, BaseObject::kSlot, cppgc_id)});
isolate->AttachCppHeap(cpp_heap_.get());
}
// We do not care about overflow since we just want this to be different
// from the cppgc id.
Expand Down Expand Up @@ -565,6 +576,21 @@ IsolateData::IsolateData(Isolate* isolate,
}
}

IsolateData::~IsolateData() {
if (cpp_heap_ != nullptr) {
// The CppHeap must be detached before being terminated.
isolate_->DetachCppHeap();
cpp_heap_->Terminate();
}
}

// Public API
void SetCppgcReference(Isolate* isolate,
Local<Object> object,
void* wrappable) {
IsolateData::SetCppgcReference(isolate, object, wrappable);
}

void IsolateData::MemoryInfo(MemoryTracker* tracker) const {
#define V(PropertyName, StringValue) \
tracker->TrackField(#PropertyName, PropertyName());
Expand Down
10 changes: 10 additions & 0 deletions src/env.h
Expand Up @@ -62,6 +62,10 @@
#include <unordered_set>
#include <vector>

namespace v8 {
class CppHeap;
}

namespace node {

namespace shadow_realm {
Expand Down Expand Up @@ -136,6 +140,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
MultiIsolatePlatform* platform = nullptr,
ArrayBufferAllocator* node_allocator = nullptr,
const SnapshotData* snapshot_data = nullptr);
~IsolateData();

SET_MEMORY_INFO_NAME(IsolateData)
SET_SELF_SIZE(IsolateData)
Expand All @@ -148,6 +153,10 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
uint16_t* embedder_id_for_cppgc() const;
uint16_t* embedder_id_for_non_cppgc() const;

static inline void SetCppgcReference(v8::Isolate* isolate,
v8::Local<v8::Object> object,
void* wrappable);

inline uv_loop_t* event_loop() const;
inline MultiIsolatePlatform* platform() const;
inline const SnapshotData* snapshot_data() const;
Expand Down Expand Up @@ -229,6 +238,7 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
NodeArrayBufferAllocator* const node_allocator_;
MultiIsolatePlatform* platform_;
const SnapshotData* snapshot_data_;
std::unique_ptr<v8::CppHeap> cpp_heap_;
std::shared_ptr<PerIsolateOptions> options_;
worker::Worker* worker_context_ = nullptr;
bool is_building_snapshot_ = false;
Expand Down
14 changes: 14 additions & 0 deletions src/node.cc
Expand Up @@ -62,6 +62,8 @@
#endif // NODE_USE_V8_PLATFORM
#include "v8-profiler.h"

#include "cppgc/platform.h"

#if HAVE_INSPECTOR
#include "inspector/worker_inspector.h" // ParentInspectorHandle
#endif
Expand Down Expand Up @@ -1096,6 +1098,14 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
V8::Initialize();
}

if (!(flags & ProcessInitializationFlags::kNoInitializeCppgc)) {
v8::PageAllocator* allocator = nullptr;
if (result->platform_ != nullptr) {
allocator = result->platform_->GetPageAllocator();
}
cppgc::InitializeProcess(allocator);
}

performance::performance_v8_start = PERFORMANCE_NOW();
per_process::v8_initialized = true;

Expand All @@ -1115,6 +1125,10 @@ void TearDownOncePerProcess() {
ResetSignalHandlers();
}

if (!(flags & ProcessInitializationFlags::kNoInitializeCppgc)) {
cppgc::ShutdownProcess();
}

per_process::v8_initialized = false;
if (!(flags & ProcessInitializationFlags::kNoInitializeV8)) {
V8::Dispose();
Expand Down
25 changes: 24 additions & 1 deletion src/node.h
Expand Up @@ -261,6 +261,10 @@ enum Flags : uint32_t {
kNoUseLargePages = 1 << 11,
// Skip printing output for --help, --version, --v8-options.
kNoPrintHelpOrVersionOutput = 1 << 12,
// Do not perform cppgc initialization. If set, the embedder must call
// cppgc::InitializeProcess() before creating a Node.js environment
// and call cppgc::ShutdownProcess() before process shutdown.
kNoInitializeCppgc = 1 << 13,

// Emulate the behavior of InitializeNodeWithArgs() when passing
// a flags argument to the InitializeOncePerProcess() replacement
Expand All @@ -269,7 +273,7 @@ enum Flags : uint32_t {
kNoStdioInitialization | kNoDefaultSignalHandling | kNoInitializeV8 |
kNoInitializeNodeV8Platform | kNoInitOpenSSL |
kNoParseGlobalDebugVariables | kNoAdjustResourceLimits |
kNoUseLargePages | kNoPrintHelpOrVersionOutput,
kNoUseLargePages | kNoPrintHelpOrVersionOutput | kNoInitializeCppgc,
};
} // namespace ProcessInitializationFlags
namespace ProcessFlags = ProcessInitializationFlags; // Legacy alias.
Expand Down Expand Up @@ -1486,6 +1490,25 @@ void RegisterSignalHandler(int signal,
bool reset_handler = false);
#endif // _WIN32

// Configure the layout of the JavaScript object with a cppgc::GarbageCollected
// instance so that when the JavaScript object is reachable, the garbage
// collected instance would have its Trace() method invoked per the cppgc
// contract. To make it work, the process must have called
// cppgc::InitializeProcess() before, which is usually the case for addons
// loaded by the stand-alone Node.js executable. Embedders of Node.js can use
// either need to call it themselves or make sure that
// ProcessInitializationFlags::kNoInitializeCppgc is *not* set for cppgc to
// work.
// If the CppHeap is owned by Node.js, which is usually the case for addon,
// the object must be created with at least two internal fields available,
// and the first two internal fields would be configured by Node.js.
// This may be superseded by a V8 API in the future, see
// https://bugs.chromium.org/p/v8/issues/detail?id=13960. Until then this
// serves as a helper for Node.js isolates.
NODE_EXTERN void SetCppgcReference(v8::Isolate* isolate,
v8::Local<v8::Object> object,
void* wrappable);

} // namespace node

#endif // SRC_NODE_H_
2 changes: 2 additions & 0 deletions src/node_main_instance.cc
Expand Up @@ -68,6 +68,8 @@ NodeMainInstance::~NodeMainInstance() {
return;
}
// This should only be done on a main instance that owns its isolate.
// IsolateData must be freed before UnregisterIsolate() is called.
isolate_data_.reset();
platform_->UnregisterIsolate(isolate_);
isolate_->Dispose();
}
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.cc
Expand Up @@ -11,6 +11,7 @@
#include "node_snapshot_builder.h"
#include "permission/permission.h"
#include "util-inl.h"
#include "v8-cppgc.h"

#include <memory>
#include <string>
Expand Down
78 changes: 78 additions & 0 deletions test/addons/cppgc-object/binding.cc
@@ -0,0 +1,78 @@
#include <cppgc/allocation.h>
#include <cppgc/garbage-collected.h>
#include <cppgc/heap.h>
#include <node.h>
#include <v8-cppgc.h>
#include <v8.h>
#include <algorithm>

class CppGCed : public cppgc::GarbageCollected<CppGCed> {
public:
static uint16_t states[2];
static constexpr int kDestructCount = 0;
static constexpr int kTraceCount = 1;

static void New(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
v8::Local<v8::Object> js_object = args.This();
CppGCed* gc_object = cppgc::MakeGarbageCollected<CppGCed>(
isolate->GetCppHeap()->GetAllocationHandle());
node::SetCppgcReference(isolate, js_object, gc_object);
args.GetReturnValue().Set(js_object);
}

static v8::Local<v8::Function> GetConstructor(
v8::Local<v8::Context> context) {
auto ft = v8::FunctionTemplate::New(context->GetIsolate(), New);
auto ot = ft->InstanceTemplate();
v8::WrapperDescriptor descriptor =
context->GetIsolate()->GetCppHeap()->wrapper_descriptor();
uint16_t required_size = std::max(descriptor.wrappable_instance_index,
descriptor.wrappable_type_index);
ot->SetInternalFieldCount(required_size + 1);
return ft->GetFunction(context).ToLocalChecked();
}

CppGCed() = default;

~CppGCed() { states[kDestructCount]++; }

void Trace(cppgc::Visitor* visitor) const { states[kTraceCount]++; }
};

uint16_t CppGCed::states[] = {0, 0};

void InitModule(v8::Local<v8::Object> exports) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto context = isolate->GetCurrentContext();

auto store = v8::ArrayBuffer::NewBackingStore(
CppGCed::states,
sizeof(uint16_t) * 2,
[](void*, size_t, void*) {},
nullptr);
auto ab = v8::ArrayBuffer::New(isolate, std::move(store));

exports
->Set(context,
v8::String::NewFromUtf8(isolate, "CppGCed").ToLocalChecked(),
CppGCed::GetConstructor(context))
.FromJust();
exports
->Set(context,
v8::String::NewFromUtf8(isolate, "states").ToLocalChecked(),
v8::Uint16Array::New(ab, 0, 2))
.FromJust();
exports
->Set(context,
v8::String::NewFromUtf8(isolate, "kDestructCount").ToLocalChecked(),
v8::Integer::New(isolate, CppGCed::kDestructCount))
.FromJust();
exports
->Set(context,
v8::String::NewFromUtf8(isolate, "kTraceCount").ToLocalChecked(),
v8::Integer::New(isolate, CppGCed::kTraceCount))
.FromJust();
}

NODE_MODULE(NODE_GYP_MODULE_NAME, InitModule)
9 changes: 9 additions & 0 deletions test/addons/cppgc-object/binding.gyp
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
51 changes: 51 additions & 0 deletions test/addons/cppgc-object/test.js
@@ -0,0 +1,51 @@
'use strict';

// Flags: --expose-gc

const common = require('../../common');

// Verify that addons can create GarbageCollected objects and
// have them traced properly.

const assert = require('assert');
const {
CppGCed, states, kDestructCount, kTraceCount,
} = require(`./build/${common.buildType}/binding`);

assert.strictEqual(states[kDestructCount], 0);
assert.strictEqual(states[kTraceCount], 0);

let array = [];
const count = 100;
for (let i = 0; i < count; ++i) {
array.push(new CppGCed());
}

globalThis.gc();

setTimeout(async function() {
// GC should have invoked Trace() on at least some of the CppGCed objects,
// but they should all be alive at this point.
assert.strictEqual(states[kDestructCount], 0);
assert.notStrictEqual(states[kTraceCount], 0);

// Replace the old CppGCed objects with new ones, after GC we should have
// destructed all the old ones and called Trace() on the
// new ones.
for (let i = 0; i < count; ++i) {
array[i] = new CppGCed();
}
await common.gcUntil(
'All old CppGCed are destroyed',
() => states[kDestructCount] === count,
);
// Release all the CppGCed objects, after GC we should have destructed
// all of them.
array = null;
globalThis.gc();

await common.gcUntil(
'All old CppGCed are destroyed',
() => states[kDestructCount] === count * 2,
);
}, 1);