Skip to content

Commit

Permalink
src,tools: initialize cppgc
Browse files Browse the repository at this point in the history
This patch:

- Initializes cppgc in InitializeOncePerProcess() when
  kNoInitializeCppgc is not set
- Create a CppHeap and attach it to the Isolate when
  there isn't one already during IsolateData initialization.
  The CppHeap is detached and terminated when IsolateData
  is freed.
- Publishes the cppgc headers in the tarball.

This allows C++ addons to start using cppgc to manage objects.

A helper node::SetCppgcReference() is also added to help addons
enable cppgc tracing in a user-defined object.

Co-authored-by: Joyee Cheung <[email protected]>
Refs: #40786
PR-URL: #45704
Refs: https://docs.google.com/document/d/1ny2Qz_EsUnXGKJRGxoA-FXIE2xpLgaMAN6jD7eAkqFQ/edit
Reviewed-By: Stephen Belanger <[email protected]>
Reviewed-By: Rafael Gonzaga <[email protected]>
  • Loading branch information
dharesign and joyeecheung authored Aug 11, 2023
1 parent 7bbcb29 commit 7f2c810
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 3 deletions.
26 changes: 26 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit 7f2c810

Please sign in to comment.