Skip to content

Commit 6ac8233

Browse files
committed
src: add utilities for cppgc classes
1 parent cc26951 commit 6ac8233

File tree

4 files changed

+297
-9
lines changed

4 files changed

+297
-9
lines changed

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
'src/compile_cache.h',
203203
'src/connect_wrap.h',
204204
'src/connection_wrap.h',
205+
'src/cppgc_helpers.h',
205206
'src/dataqueue/queue.h',
206207
'src/debug_utils.h',
207208
'src/debug_utils-inl.h',

src/README.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,166 @@ overview over libuv handles managed by Node.js.
975975

976976
<a id="callback-scopes"></a>
977977

978+
### `CppgcMixin`
979+
980+
V8 comes with a trace-based C++ garbage collection library called
981+
[Oilpan][], whose API is in headers under`deps/v8/include/cppgc`.
982+
In this document we refer to it as `cppgc` since that's the namespace
983+
of the library.
984+
985+
C++ objects managed using `cppgc` are allocated in the V8 heap
986+
and traced by V8's garbage collector. The `cppgc` library provides
987+
APIs for embedders to create references between cppgc-managed objects
988+
and other objects in the V8 heap (such as JavaScript objects or other
989+
objects in the V8 C++ API that can be passed around with V8 handles)
990+
in a way that's understood by V8's garbage collector.
991+
This helps avoiding accidental memory leaks and use-after-frees coming
992+
from incorrect cross-heap reference tracking, especially when there are
993+
cyclic references. This is what powers the
994+
[unified heap design in Chromium][] to avoid cross-heap memory issues,
995+
and it's being rolled out in Node.js to reap similar benefits.
996+
997+
For general guidance on how to use `cppgc`, see the
998+
[Oilpan documentation in Chromium][]. In Node.js there is a helper
999+
mixin `node::CppgcMixin` from `cppgc_helpers.h` to help implementing
1000+
`cppgc`-managed wrapper objects with a [`BaseObject`][]-like interface.
1001+
`cppgc`-manged objects in Node.js internals should extend this mixin,
1002+
while non-`cppgc`-managed objects typically extend `BaseObject` - the
1003+
latter are being migrated to be `cppgc`-managed wherever it's beneficial
1004+
and practical. Typically `cppgc`-managed objects are more efficient to
1005+
keep track of (which lowers initialization cost) and work better
1006+
with V8's GC scheduling.
1007+
1008+
A `cppgc`-managed native wrapper should look something like this, note
1009+
that per cppgc rules, `cppgc::GarbageCollected<>` must be the left-most
1010+
base class.
1011+
1012+
```cpp
1013+
#include "cppgc_helpers.h"
1014+
1015+
class MyWrap final : public cppgc::GarbageCollected<MyWrap>,
1016+
public cppgc::NameProvider,
1017+
public CppgcMixin {
1018+
public:
1019+
SET_CPPGC_NAME(MyWrap) // Sets the heap snapshot name to "Node / MyWrap"
1020+
1021+
MyWrap(Environment* env, v8::Local<v8::Object> object);
1022+
MyWrap* New(Environment* env, v8::Local<v8::Object> object);
1023+
1024+
void Trace(cppgc::Visitor* visitor) const final;
1025+
}
1026+
```
1027+
1028+
`cppgc::GarbageCollected` objects should not be allocated with usual C++
1029+
primitives (e.g. using `new` or `std::make_unique` is forbidden). Instead
1030+
they must be allocated using `cppgc::MakeGarbageCollected` - this would
1031+
allocate them in the V8 heap and allow V8's garbage collector to trace them.
1032+
It's recommended to use a `New` method to prepare the arguments and invoke
1033+
`cppgc::MakeGarbageCollected` (which forwards the arguments to the child class
1034+
constructor).
1035+
1036+
```cpp
1037+
MyWrap::MyWrap(Environment* env, v8::Local<v8::Object> object) {
1038+
// This cannot invoke the mixin constructor and has to invoke a static
1039+
// method from it, per cppgc rules. node::CppgcMixin provides
1040+
// InitializeCppgc() for wrapping the C++ object with the provided
1041+
// JavaScript object.
1042+
InitializeCppgc(this, env, object);
1043+
}
1044+
1045+
MyWrap* MyWrap::New(Environment* env, v8::Local<v8::Object> object) {
1046+
// Per cppgc rules, the constructor of MyWrap cannot be invoked directly.
1047+
// It's recommended to implement a New() static method that prepares
1048+
// and forwards the necessary arguments to cppgc::MakeGarbageCollected()
1049+
// and just return the raw pointer around - do not use any C++ smart
1050+
// pointer with this, as this is not managed by the native memory
1051+
// allocator but by V8.
1052+
return cppgc::MakeGarbageCollected<MyWrap>(
1053+
env->isolate()->GetCppHeap()->GetAllocationHandle(), env, object);
1054+
}
1055+
```
1056+
1057+
`cppgc::GarbageCollected` types are expected to implement a
1058+
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
1059+
final class in the hierarchy, this method must be marked `final`. For
1060+
classes extending `node::CppgcMixn`, this should typically dispatch a
1061+
call to `CppgcMixin::Trace()` first, then trace any additional owned data
1062+
it has. See `deps/v8/include/cppgc/garbage-collected.h` see what types of
1063+
data can be traced.
1064+
1065+
```cpp
1066+
void MyWrap::Trace(cppgc::Visitor* visitor) const {
1067+
CppgcMixin::Trace(visitor);
1068+
visitor->Trace(...); // Trace any additional data MyWrap has
1069+
}
1070+
```
1071+
1072+
#### Creating C++ to JavaScript references in cppgc-managed objects
1073+
1074+
Unlike `BaseObject` which typically uses a `v8::Global` (either weak or strong)
1075+
to reference an object from the V8 heap, cppgc-managed objects are expected to
1076+
use `v8::TracedReference` (which supports any `v8::Data`). For example if the
1077+
`MyWrap` object owns a `v8::UnboundScript`, in the class body the reference
1078+
should be declared as
1079+
1080+
```cpp
1081+
class MyWrap : ... {
1082+
v8::TracedReference<v8::UnboundScript> script;
1083+
}
1084+
```
1085+
1086+
V8's garbage collector traces the references from `MyWrap` through the
1087+
`MyWrap::Trace()` method, which should call `cppgc::Visitor::Trace` on the
1088+
`v8::TracedReference`.
1089+
1090+
```cpp
1091+
void MyWrap::Trace(cppgc::Visitor* visitor) const {
1092+
CppgcMixin::Trace(visitor);
1093+
visitor->Trace(script); // v8::TracedReference is supported by cppgc::Visitor
1094+
}
1095+
```
1096+
1097+
As long as a `MyWrap` object is alive, the `v8::UnboundScript` in its
1098+
`v8::TracedReference` will be kept alive. When the `MyWrap` object is no longer
1099+
reachable from the V8 heap, and there are no other references to the
1100+
`v8::UnboundScript` it owns, the `v8::UnboundScript` will be garbage collected
1101+
along with its owning `MyWrap`. The reference will also be automatically
1102+
captured in the heap snapshots.
1103+
1104+
#### Creating JavaScript to C++ references for cppgc-managed objects
1105+
1106+
All C++ objects using `node::CppgcMixin` have a counterpart JavaScript object,
1107+
which can be obtained in C++ using `node::CppgcMixin::object()`.
1108+
The two references each other internally - this cycle is well-understood by V8's
1109+
garbage collector and can be managed properly. To create a reference from another
1110+
JavaScript object to a C++ wrapper extending `node::CppgcMixin`, just create a
1111+
JavaScript to JavaScript reference via its counterpart JavaScript object instead.
1112+
1113+
```cpp
1114+
MyWrap* wrap = ....; // Obtain a reference to the cppgc-managed object.
1115+
Local<Object> referrer = ...; // This is the referrer object.
1116+
// To reference the C++ wrap from the JavaScript referrer, simply creates
1117+
// a usual JavaScript property reference - the key can be a symbol or a
1118+
// number too if necessary, or it can be a private symbol property added
1119+
// using SetPrivate(). wrap->object() can also be passed to the JavaScript
1120+
// land, which can be referenced by any JavaScript objects in an invisible
1121+
// manner using a WeakMap or being inside a closure.
1122+
referrer->Set(
1123+
context, FIXED_ONE_BYTE_STRING(isolate, "ref"), wrap->object()
1124+
).ToLocalChecked();
1125+
```
1126+
1127+
Typically, a newly created cppgc-managed wrapper object should be held alive
1128+
by the JavaScript land (for example, by being returned by a method and
1129+
staying alive in a closure). Long-lived cppgc objects can also
1130+
be held alive from C++ using persistent handles (see
1131+
`deps/v8/include/cppgc/persistent.h`) or as members of other living
1132+
cppgc-managed objects (see `deps/v8/include/cppgc/member.h`) if necessary.
1133+
Its destructor will be called when no other objects from the V8 heap reference
1134+
it, this can happen at any time after the garbage collector notices that
1135+
it's no longer reachable and before the V8 isolate is torn down.
1136+
See the [Oilpan documentation in Chromium][] for more details.
1137+
9781138
### Callback scopes
9791139

9801140
The public `CallbackScope` and the internally used `InternalCallbackScope`
@@ -1082,6 +1242,8 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
10821242
[ECMAScript realm]: https://tc39.es/ecma262/#sec-code-realms
10831243
[JavaScript value handles]: #js-handles
10841244
[N-API]: https://nodejs.org/api/n-api.html
1245+
[Oilpan]: https://v8.dev/blog/oilpan-library
1246+
[Oilpan documentation in Chromium]: https://chromium.googlesource.com/v8/v8/+/main/include/cppgc/README.md
10851247
[`BaseObject`]: #baseobject
10861248
[`Context`]: #context
10871249
[`Environment`]: #environment
@@ -1117,3 +1279,4 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
11171279
[libuv handles]: #libuv-handles-and-requests
11181280
[libuv requests]: #libuv-handles-and-requests
11191281
[reference documentation for the libuv API]: http://docs.libuv.org/en/v1.x/
1282+
[unified heap design in Chromium]: https://docs.google.com/document/d/1Hs60Zx1WPJ_LUjGvgzt1OQ5Cthu-fG-zif-vquUH_8c/edit

src/cppgc_helpers.h

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#ifndef SRC_CPPGC_HELPERS_H_
2+
#define SRC_CPPGC_HELPERS_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include <type_traits> // std::remove_reference
7+
#include "cppgc/garbage-collected.h"
8+
#include "cppgc/name-provider.h"
9+
#include "env.h"
10+
#include "memory_tracker.h"
11+
#include "v8-cppgc.h"
12+
#include "v8.h"
13+
14+
namespace node {
15+
16+
/**
17+
* This is a helper mixin with a BaseObject-like interface to help
18+
* implementing wrapper objects managed by V8's cppgc (Oilpan) library.
19+
* cppgc-manged objects in Node.js internals should extend this mixin,
20+
* while non-cppgc-managed objects typically extend BaseObject - the
21+
* latter are being migrated to be cppgc-managed wherever it's beneficial
22+
* and practical. Typically cppgc-managed objects are more efficient to
23+
* keep track of (which lowers initialization cost) and work better
24+
* with V8's GC scheduling.
25+
*
26+
* A cppgc-managed native wrapper should look something like this, note
27+
* that per cppgc rules, cppgc::GarbageCollected<> must be the left-most
28+
* base class.
29+
*
30+
* class Klass final : public cppgc::GarbageCollected<Klass>,
31+
* public cppgc::NameProvider,
32+
* public CppgcMixin {
33+
* public:
34+
* SET_CPPGC_NAME(Klass) // Sets the heap snapshot name to "Node / Klass"
35+
* void Trace(cppgc::Visitor* visitor) const final {
36+
* CppgcMixin::Trace(visitor);
37+
* visitor->Trace(...); // Trace any additional owned traceable data
38+
* }
39+
* }
40+
*/
41+
class CppgcMixin : public cppgc::GarbageCollectedMixin {
42+
public:
43+
// To help various callbacks access wrapper objects with different memory
44+
// management, cppgc-managed objects share the same layout as BaseObjects.
45+
enum InternalFields { kEmbedderType = 0, kSlot, kInternalFieldCount };
46+
47+
// The initialization cannot be done in the mixin constructor but has to be
48+
// invoked from the child class constructor, per cppgc::GarbageCollectedMixin
49+
// rules.
50+
template <typename T>
51+
void InitializeCppgc(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
52+
env_ = env;
53+
traced_reference_ = v8::TracedReference<v8::Object>(env->isolate(), obj);
54+
SetCppgcReference(env->isolate(), obj, ptr);
55+
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
56+
obj->SetAlignedPointerInInternalField(T::kSlot, ptr);
57+
}
58+
59+
v8::Local<v8::Object> object() const {
60+
return traced_reference_.Get(env_->isolate());
61+
}
62+
63+
Environment* env() const { return env_; }
64+
65+
template <typename T>
66+
static T* Unwrap(v8::Local<v8::Object> obj) {
67+
// We are not using v8::Object::Unwrap currently because that requires
68+
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
69+
// with cppgc-allocated types doesn't take.
70+
// Since cppgc-managed objects share the same layout as BaseObjects, just
71+
// unwrap from the pointer in the internal field, which should be valid as
72+
// long as the object is still alive.
73+
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
74+
return nullptr;
75+
}
76+
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
77+
return ptr;
78+
}
79+
80+
// Subclasses are expected to invoke CppgcMixin::Trace() in their own Trace()
81+
// methods.
82+
void Trace(cppgc::Visitor* visitor) const override {
83+
visitor->Trace(traced_reference_);
84+
}
85+
86+
private:
87+
Environment* env_;
88+
v8::TracedReference<v8::Object> traced_reference_;
89+
};
90+
91+
// If the class doesn't have additional owned traceable data, use this macro to
92+
// save the implementation of a custom Trace() method.
93+
#define DEFAULT_CPPGC_TRACE() \
94+
void Trace(cppgc::Visitor* visitor) const final { \
95+
CppgcMixin::Trace(visitor); \
96+
}
97+
98+
// This macro sets the node name in the heap snapshot with a "Node /" prefix.
99+
// Classes that use this macro must extend cppgc::NameProvider.
100+
#define SET_CPPGC_NAME(Klass) \
101+
inline const char* GetHumanReadableName() const final { \
102+
return "Node / " #Klass; \
103+
}
104+
} // namespace node
105+
106+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
107+
108+
#endif // SRC_CPPGC_HELPERS_H_

src/heap_utils.cc

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using v8::Array;
2222
using v8::Boolean;
2323
using v8::Context;
24+
using v8::Data;
2425
using v8::EmbedderGraph;
2526
using v8::EscapableHandleScope;
2627
using v8::FunctionCallbackInfo;
@@ -50,18 +51,20 @@ class JSGraphJSNode : public EmbedderGraph::Node {
5051
const char* Name() override { return "<JS Node>"; }
5152
size_t SizeInBytes() override { return 0; }
5253
bool IsEmbedderNode() override { return false; }
53-
Local<Value> JSValue() { return PersistentToLocal::Strong(persistent_); }
54+
Local<Data> V8Value() { return PersistentToLocal::Strong(persistent_); }
5455

5556
int IdentityHash() {
56-
Local<Value> v = JSValue();
57+
Local<Data> d = V8Value();
58+
// TODO(joyeecheung): return something better?
59+
if (!d->IsValue()) return reinterpret_cast<std::uintptr_t>(this);
60+
Local<Value> v = d.As<Value>();
5761
if (v->IsObject()) return v.As<Object>()->GetIdentityHash();
5862
if (v->IsName()) return v.As<v8::Name>()->GetIdentityHash();
5963
if (v->IsInt32()) return v.As<v8::Int32>()->Value();
6064
return 0;
6165
}
6266

63-
JSGraphJSNode(Isolate* isolate, Local<Value> val)
64-
: persistent_(isolate, val) {
67+
JSGraphJSNode(Isolate* isolate, Local<Data> val) : persistent_(isolate, val) {
6568
CHECK(!val.IsEmpty());
6669
}
6770

@@ -73,19 +76,27 @@ class JSGraphJSNode : public EmbedderGraph::Node {
7376

7477
struct Equal {
7578
inline bool operator()(JSGraphJSNode* a, JSGraphJSNode* b) const {
76-
return a->JSValue()->SameValue(b->JSValue());
79+
Local<Data> data_a = a->V8Value();
80+
Local<Data> data_b = a->V8Value();
81+
if (data_a->IsValue()) {
82+
if (!data_b->IsValue()) {
83+
return false;
84+
}
85+
return data_a.As<Value>()->SameValue(data_b.As<Value>());
86+
}
87+
return data_a == data_b;
7788
}
7889
};
7990

8091
private:
81-
Global<Value> persistent_;
92+
Global<Data> persistent_;
8293
};
8394

8495
class JSGraph : public EmbedderGraph {
8596
public:
8697
explicit JSGraph(Isolate* isolate) : isolate_(isolate) {}
8798

88-
Node* V8Node(const Local<Value>& value) override {
99+
Node* V8Node(const Local<v8::Data>& value) override {
89100
std::unique_ptr<JSGraphJSNode> n { new JSGraphJSNode(isolate_, value) };
90101
auto it = engine_nodes_.find(n.get());
91102
if (it != engine_nodes_.end())
@@ -94,6 +105,10 @@ class JSGraph : public EmbedderGraph {
94105
return AddNode(std::unique_ptr<Node>(n.release()));
95106
}
96107

108+
Node* V8Node(const Local<v8::Value>& value) override {
109+
return V8Node(value.As<v8::Data>());
110+
}
111+
97112
Node* AddNode(std::unique_ptr<Node> node) override {
98113
Node* n = node.get();
99114
nodes_.emplace(std::move(node));
@@ -154,8 +169,9 @@ class JSGraph : public EmbedderGraph {
154169
if (nodes->Set(context, i++, obj).IsNothing())
155170
return MaybeLocal<Array>();
156171
if (!n->IsEmbedderNode()) {
157-
value = static_cast<JSGraphJSNode*>(n.get())->JSValue();
158-
if (obj->Set(context, value_string, value).IsNothing())
172+
Local<Data> data = static_cast<JSGraphJSNode*>(n.get())->V8Value();
173+
if (data->IsValue() &&
174+
obj->Set(context, value_string, data.As<Value>()).IsNothing())
159175
return MaybeLocal<Array>();
160176
}
161177
}

0 commit comments

Comments
 (0)