Skip to content

Commit 849db10

Browse files
joyeecheungnodejs-github-bot
authored andcommitted
src: add helpers for creating cppgc-managed wrappers
This patch adds helpers for wrapper classes based on cppgc (Oilpan) in `src/cppgc_helpers.h`, including `node::CppgcMixin` and `ASSIGN_OR_RETURN_UNWRAP_CPPGC`, which are designed to have similar interface to BaseObject helpers to help migration. They are documented in the `CppgcMixin` section in `src/README.md` To disambiguate, the global `node::Unwrap<>` has now been moved as `node::BaseObject::Unwrap<>`, and `node::Cppgc::Unwrap<>` implements a similar unwrapping mechanism for cppgc-managed wrappers. PR-URL: #52295 Refs: #40786 Refs: https://docs.google.com/document/d/1ny2Qz_EsUnXGKJRGxoA-FXIE2xpLgaMAN6jD7eAkqFQ/edit Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
1 parent 4568df4 commit 849db10

11 files changed

+466
-21
lines changed

node.gyp

+1
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

+225
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,228 @@ 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:
1009+
1010+
```cpp
1011+
#include "cppgc_helpers.h"
1012+
1013+
// CPPGC_MIXIN is a helper macro for inheriting from cppgc::GarbageCollected,
1014+
// cppgc::NameProvider and public CppgcMixin. Per cppgc rules, it must be
1015+
// placed at the left-most position in the class hierarchy.
1016+
class MyWrap final : CPPGC_MIXIN(ContextifyScript) {
1017+
public:
1018+
SET_CPPGC_NAME(MyWrap) // Sets the heap snapshot name to "Node / MyWrap"
1019+
1020+
// The constructor can only be called by `cppgc::MakeGarbageCollected()`.
1021+
MyWrap(Environment* env, v8::Local<v8::Object> object);
1022+
1023+
// Helper for constructing MyWrap via `cppgc::MakeGarbageCollected()`.
1024+
// Can be invoked by other C++ code outside of this class if necessary.
1025+
// In that case the raw pointer returned may need to be managed by
1026+
// cppgc::Persistent<> or cppgc::Member<> with corresponding tracing code.
1027+
static MyWrap* New(Environment* env, v8::Local<v8::Object> object);
1028+
// Binding method to help constructing MyWrap in JavaScript.
1029+
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
1030+
1031+
void Trace(cppgc::Visitor* visitor) const final;
1032+
}
1033+
```
1034+
1035+
`cppgc::GarbageCollected` types are expected to implement a
1036+
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
1037+
final class in the hierarchy, this method must be marked `final`. For
1038+
classes extending `node::CppgcMixn`, this should typically dispatch a
1039+
call to `CppgcMixin::Trace()` first, then trace any additional owned data
1040+
it has. See `deps/v8/include/cppgc/garbage-collected.h` see what types of
1041+
data can be traced.
1042+
1043+
```cpp
1044+
void MyWrap::Trace(cppgc::Visitor* visitor) const {
1045+
CppgcMixin::Trace(visitor);
1046+
visitor->Trace(...); // Trace any additional data MyWrap has
1047+
}
1048+
```
1049+
1050+
#### Constructing and wrapping `cppgc`-managed objects
1051+
1052+
C++ objects subclassing `node::CppgcMixin` have a counterpart JavaScript object.
1053+
The two references each other internally - this cycle is well-understood by V8's
1054+
garbage collector and can be managed properly.
1055+
1056+
Similar to `BaseObject`s, `cppgc`-managed wrappers objects must be created from
1057+
object templates with at least `node::CppgcMixin::kInternalFieldCount` internal
1058+
fields. To unify handling of the wrappers, the internal fields of
1059+
`node::CppgcMixin` wrappers would have the same layout as `BaseObject`.
1060+
1061+
```cpp
1062+
// To create the v8::FunctionTemplate that can be used to instantiate a
1063+
// v8::Function for that serves as the JavaScript constructor of MyWrap:
1064+
Local<FunctionTemplate> ctor_template = NewFunctionTemplate(isolate, MyWrap::New);
1065+
ctor_template->InstanceTemplate()->SetInternalFieldCount(
1066+
ContextifyScript::kInternalFieldCount);
1067+
```
1068+
1069+
`cppgc::GarbageCollected` objects should not be allocated with usual C++
1070+
primitives (e.g. using `new` or `std::make_unique` is forbidden). Instead
1071+
they must be allocated using `cppgc::MakeGarbageCollected` - this would
1072+
allocate them in the V8 heap and allow V8's garbage collector to trace them.
1073+
It's recommended to use a `New` method to wrap the `cppgc::MakeGarbageCollected`
1074+
call so that external C++ code does not need to know about its memory management
1075+
scheme to construct it.
1076+
1077+
```cpp
1078+
MyWrap* MyWrap::New(Environment* env, v8::Local<v8::Object> object) {
1079+
// Per cppgc rules, the constructor of MyWrap cannot be invoked directly.
1080+
// It's recommended to implement a New() static method that prepares
1081+
// and forwards the necessary arguments to cppgc::MakeGarbageCollected()
1082+
// and just return the raw pointer around - do not use any C++ smart
1083+
// pointer with this, as this is not managed by the native memory
1084+
// allocator but by V8.
1085+
return cppgc::MakeGarbageCollected<MyWrap>(
1086+
env->isolate()->GetCppHeap()->GetAllocationHandle(), env, object);
1087+
}
1088+
1089+
// Binding method to be invoked by JavaScript.
1090+
void MyWrap::New(const FunctionCallbackInfo<Value>& args) {
1091+
Environment* env = Environment::GetCurrent(args);
1092+
Isolate* isolate = env->isolate();
1093+
Local<Context> context = env->context();
1094+
1095+
CHECK(args.IsConstructCall());
1096+
1097+
// Get more arguments from JavaScript land if necessary.
1098+
New(env, args.This());
1099+
}
1100+
```
1101+
1102+
In the constructor of `node::CppgcMixin` types, use
1103+
`node::CppgcMixin::Wrap()` to finish the wrapping so that
1104+
V8 can trace the C++ object from the JavaScript object.
1105+
1106+
```cpp
1107+
MyWrap::MyWrap(Environment* env, v8::Local<v8::Object> object) {
1108+
// This cannot invoke the mixin constructor and has to invoke via a static
1109+
// method from it, per cppgc rules.
1110+
CppgcMixin::Wrap(this, env, object);
1111+
}
1112+
```
1113+
1114+
#### Unwrapping `cppgc`-managed wrapper objects
1115+
1116+
When given a `v8::Local<v8::Object>` that is known to be the JavaScript
1117+
wrapper object for `MyWrap`, uses the `node::CppgcMixin::Unwrap()` to
1118+
get the C++ object from it:
1119+
1120+
```cpp
1121+
v8::Local<v8::Object> object = ...; // Obtain the JavaScript from somewhere.
1122+
MyWrap* wrap = CppgcMixin::Unwrap<MyWrap>(object);
1123+
```
1124+
1125+
Similar to `ASSIGN_OR_RETURN_UNWRAP`, there is a `ASSIGN_OR_RETURN_UNWRAP_CPPGC`
1126+
that can be used in binding methods to return early if the JavaScript object does
1127+
not wrap the desired type. And similar to `BaseObject`, `node::CppgcMixin`
1128+
provides `env()` and `object()` methods to quickly access the associated
1129+
`node::Environment` and its JavaScript wrapper object.
1130+
1131+
```cpp
1132+
ASSIGN_OR_RETURN_UNWRAP_CPPGC(&wrap, object);
1133+
CHECK_EQ(wrap->object(), object);
1134+
```
1135+
1136+
#### Creating C++ to JavaScript references in cppgc-managed objects
1137+
1138+
Unlike `BaseObject` which typically uses a `v8::Global` (either weak or strong)
1139+
to reference an object from the V8 heap, cppgc-managed objects are expected to
1140+
use `v8::TracedReference` (which supports any `v8::Data`). For example if the
1141+
`MyWrap` object owns a `v8::UnboundScript`, in the class body the reference
1142+
should be declared as
1143+
1144+
```cpp
1145+
class MyWrap : ... {
1146+
v8::TracedReference<v8::UnboundScript> script;
1147+
}
1148+
```
1149+
1150+
V8's garbage collector traces the references from `MyWrap` through the
1151+
`MyWrap::Trace()` method, which should call `cppgc::Visitor::Trace` on the
1152+
`v8::TracedReference`.
1153+
1154+
```cpp
1155+
void MyWrap::Trace(cppgc::Visitor* visitor) const {
1156+
CppgcMixin::Trace(visitor);
1157+
visitor->Trace(script); // v8::TracedReference is supported by cppgc::Visitor
1158+
}
1159+
```
1160+
1161+
As long as a `MyWrap` object is alive, the `v8::UnboundScript` in its
1162+
`v8::TracedReference` will be kept alive. When the `MyWrap` object is no longer
1163+
reachable from the V8 heap, and there are no other references to the
1164+
`v8::UnboundScript` it owns, the `v8::UnboundScript` will be garbage collected
1165+
along with its owning `MyWrap`. The reference will also be automatically
1166+
captured in the heap snapshots.
1167+
1168+
#### Creating JavaScript to C++ references for cppgc-managed objects
1169+
1170+
To create a reference from another JavaScript object to a C++ wrapper
1171+
extending `node::CppgcMixin`, just create a JavaScript to JavaScript
1172+
reference using the JavaScript side of the wrapper, which can be accessed
1173+
using `node::CppgcMixin::object()`.
1174+
1175+
```cpp
1176+
MyWrap* wrap = ....; // Obtain a reference to the cppgc-managed object.
1177+
Local<Object> referrer = ...; // This is the referrer object.
1178+
// To reference the C++ wrap from the JavaScript referrer, simply creates
1179+
// a usual JavaScript property reference - the key can be a symbol or a
1180+
// number too if necessary, or it can be a private symbol property added
1181+
// using SetPrivate(). wrap->object() can also be passed to the JavaScript
1182+
// land, which can be referenced by any JavaScript objects in an invisible
1183+
// manner using a WeakMap or being inside a closure.
1184+
referrer->Set(
1185+
context, FIXED_ONE_BYTE_STRING(isolate, "ref"), wrap->object()
1186+
).ToLocalChecked();
1187+
```
1188+
1189+
Typically, a newly created cppgc-managed wrapper object should be held alive
1190+
by the JavaScript land (for example, by being returned by a method and
1191+
staying alive in a closure). Long-lived cppgc objects can also
1192+
be held alive from C++ using persistent handles (see
1193+
`deps/v8/include/cppgc/persistent.h`) or as members of other living
1194+
cppgc-managed objects (see `deps/v8/include/cppgc/member.h`) if necessary.
1195+
Its destructor will be called when no other objects from the V8 heap reference
1196+
it, this can happen at any time after the garbage collector notices that
1197+
it's no longer reachable and before the V8 isolate is torn down.
1198+
See the [Oilpan documentation in Chromium][] for more details.
1199+
9781200
### Callback scopes
9791201

9801202
The public `CallbackScope` and the internally used `InternalCallbackScope`
@@ -1082,6 +1304,8 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
10821304
[ECMAScript realm]: https://tc39.es/ecma262/#sec-code-realms
10831305
[JavaScript value handles]: #js-handles
10841306
[N-API]: https://nodejs.org/api/n-api.html
1307+
[Oilpan]: https://v8.dev/blog/oilpan-library
1308+
[Oilpan documentation in Chromium]: https://chromium.googlesource.com/v8/v8/+/main/include/cppgc/README.md
10851309
[`BaseObject`]: #baseobject
10861310
[`Context`]: #context
10871311
[`Environment`]: #environment
@@ -1117,3 +1341,4 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
11171341
[libuv handles]: #libuv-handles-and-requests
11181342
[libuv requests]: #libuv-handles-and-requests
11191343
[reference documentation for the libuv API]: http://docs.libuv.org/en/v1.x/
1344+
[unified heap design in Chromium]: https://docs.google.com/document/d/1Hs60Zx1WPJ_LUjGvgzt1OQ5Cthu-fG-zif-vquUH_8c/edit

src/base_object.h

+5-6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ class BaseObject : public MemoryRetainer {
8484
static inline BaseObject* FromJSObject(v8::Local<v8::Value> object);
8585
template <typename T>
8686
static inline T* FromJSObject(v8::Local<v8::Value> object);
87+
// Global alias for FromJSObject() to avoid churn.
88+
template <typename T>
89+
static inline T* Unwrap(v8::Local<v8::Value> obj) {
90+
return BaseObject::FromJSObject<T>(obj);
91+
}
8792

8893
// Make the `v8::Global` a weak reference and, `delete` this object once
8994
// the JS object has been garbage collected and there are no (strong)
@@ -234,12 +239,6 @@ class BaseObject : public MemoryRetainer {
234239
PointerData* pointer_data_ = nullptr;
235240
};
236241

237-
// Global alias for FromJSObject() to avoid churn.
238-
template <typename T>
239-
inline T* Unwrap(v8::Local<v8::Value> obj) {
240-
return BaseObject::FromJSObject<T>(obj);
241-
}
242-
243242
#define ASSIGN_OR_RETURN_UNWRAP(ptr, obj, ...) \
244243
do { \
245244
*ptr = static_cast<typename std::remove_reference<decltype(*ptr)>::type>( \

0 commit comments

Comments
 (0)