Skip to content

Commit 1f9ac2c

Browse files
committed
fixup! src: track cppgc wrappers with a list in Realm
1 parent 459885b commit 1f9ac2c

13 files changed

+304
-106
lines changed

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
'src/connect_wrap.h',
207207
'src/connection_wrap.h',
208208
'src/cppgc_helpers.h',
209+
'src/cppgc_helpers.cc',
209210
'src/dataqueue/queue.h',
210211
'src/debug_utils.h',
211212
'src/debug_utils-inl.h',

src/README.md

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,13 +1113,13 @@ class MyWrap final : CPPGC_MIXIN(MyWrap) {
11131113
```
11141114

11151115
If the wrapper needs to perform cleanups when it's destroyed and that
1116-
cleanup relies on a living Node.js `Environment`, it should implement a
1116+
cleanup relies on a living Node.js `Realm`, it should implement a
11171117
pattern like this:
11181118

11191119
```cpp
1120-
~MyWrap() { this->Clean(); }
1121-
void CleanEnvResource(Environment* env) override {
1122-
// Do cleanup that relies on a living Environemnt.
1120+
~MyWrap() { this->Finalize(); }
1121+
void Clean(Realm* env) override {
1122+
// Do cleanup that relies on a living Realm.
11231123
}
11241124
```
11251125
@@ -1277,6 +1277,17 @@ referrer->Set(
12771277
).ToLocalChecked();
12781278
```
12791279

1280+
#### Creating references between cppgc-managed objects and `BaseObject`s
1281+
1282+
This is currently unsupported with the existing helpers. If this has
1283+
to be done, new helpers must be implemented first. Consult the cppgc
1284+
headers when trying to implement it.
1285+
1286+
Another way to work around it is to always do the migration bottom-to-top.
1287+
If a cppgc-managed object needs to reference a `BaseObject`, convert
1288+
that `BaseObject` to be cppgc-managed first, and then use `cppgc::Member`
1289+
to create the references.
1290+
12801291
#### Lifetime and cleanups of cppgc-managed objects
12811292

12821293
Typically, a newly created cppgc-managed wrapper object should be held alive
@@ -1285,31 +1296,57 @@ staying alive in a closure). Long-lived cppgc objects can also
12851296
be held alive from C++ using persistent handles (see
12861297
`deps/v8/include/cppgc/persistent.h`) or as members of other living
12871298
cppgc-managed objects (see `deps/v8/include/cppgc/member.h`) if necessary.
1288-
Its destructor will be called when no other objects from the V8 heap reference
1289-
it, this can happen at any time after the garbage collector notices that
1290-
it's no longer reachable and before the V8 isolate is torn down.
1291-
See the [Oilpan documentation in Chromium][] for more details.
1292-
1293-
If the cppgc-managed objects does not need to perform any special cleanup,
1294-
it's fine to use the default destructor. If it needs to perform only trivial
1295-
cleanup that only affects its own members without calling into JS, potentially
1296-
triggering garbage collection or relying on a living `Environment`, then it's
1297-
fine to just implement the trivial cleanup in the destructor directly. If it
1298-
needs to do any substantial cleanup that involves a living `Environment`, because
1299-
the destructor can be called at any time by the garbage collection, even after
1300-
the `Environment` is already gone, it must implement the cleanup with this pattern:
13011299

1302-
```cpp
1303-
~MyWrap() { this->Clean(); }
1304-
void CleanEnvResource(Environment* env) override {
1305-
// Do cleanup that relies on a living Environemnt. This would be
1306-
// called by CppgcMixin::Clean() first during Environment shutdown,
1307-
// while the Environment is still alive. If the destructor calls
1308-
// Clean() again later during garbage collection that happens after
1309-
// Environment shutdown, CleanEnvResource() would be skipped, preventing
1310-
// invalid access to the Environment.
1311-
}
1312-
```
1300+
When a cppgc-managed object is no longer reachable in the heap, its destructor
1301+
will be invoked by the garbage collection, which can happen after the `Realm`
1302+
is already gone, or after any object it references is gone. It is therefore
1303+
unsafe to invoke V8 APIs directly in the destructors. To ensure safety,
1304+
the cleanups of a cppgc-managed object should adhere to different patterns,
1305+
depending on what it needs to do:
1306+
1307+
1. If it does not need to do any non-trivial cleanup, nor does its members, just use
1308+
the default destructor. Note that cleanup of `v8::TracedReference` and
1309+
`cppgc::Member` are already handled automatically by V8 so if they are all the
1310+
non-trivial members the class has, this case applies.
1311+
2. If the cleanup relies on a living `Realm`, but does not need to access V8
1312+
APIs, the class should use this pattern in its class body:
1313+
1314+
```cpp
1315+
~MyWrap() { this->Finalize(); }
1316+
void Clean(Realm* env) override {
1317+
// Do cleanup that relies on a living Realm. This would be
1318+
// called by CppgcMixin::Finalize() first during Realm shutdown,
1319+
// while the Realm is still alive. If the destructor calls
1320+
// Finalize() again later during garbage collection that happens after
1321+
// Realm shutdown, Clean() would be skipped, preventing
1322+
// invalid access to the Realm.
1323+
}
1324+
```
1325+
1326+
If implementers want to call `Finalize()` from `Clean()` again, they
1327+
need to make sure that calling `Clean()` recursively is safe.
1328+
3. If the cleanup relies on access to the V8 heap, including using any V8
1329+
handles, in addition to 2, it should use the `CPPGC_USING_PRE_FINALIZER`
1330+
macro (from the [`cppgc/prefinalizer.h` header][]) in the private
1331+
section of its class body:
1332+
1333+
```cpp
1334+
private:
1335+
CPPGC_USING_PRE_FINALIZER(MyWrap, Finalize);
1336+
```
1337+
1338+
Both the destructor and the pre-finalizer are always called on the thread
1339+
in which the object is created.
1340+
1341+
It's worth noting that the use of pre-finalizers would have a negative impact
1342+
on the garbage collection performance as V8 needs to scan all of them during
1343+
each sweeping. If the object is expected to be created frequently in large
1344+
amounts in the application, it's better to avoid access to the V8 heap in its
1345+
cleanup to avoid having to use a pre-finalizer.
1346+
1347+
For more information about the cleanup of cppgc-managed objects and
1348+
what can be done in a pre-finalizer, see the [cppgc documentation][] and
1349+
the [`cppgc/prefinalizer.h` header][].
13131350

13141351
### Callback scopes
13151352

@@ -1436,6 +1473,7 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
14361473
[`async_hooks` module]: https://nodejs.org/api/async_hooks.html
14371474
[`async_wrap.h`]: async_wrap.h
14381475
[`base_object.h`]: base_object.h
1476+
[`cppgc/prefinalizer.h` header]: ../deps/v8/include/cppgc/prefinalizer.h
14391477
[`handle_wrap.h`]: handle_wrap.h
14401478
[`memory_tracker.h`]: memory_tracker.h
14411479
[`req_wrap.h`]: req_wrap.h
@@ -1446,6 +1484,7 @@ static void GetUserInfo(const FunctionCallbackInfo<Value>& args) {
14461484
[`vm` module]: https://nodejs.org/api/vm.html
14471485
[binding function]: #binding-functions
14481486
[cleanup hooks]: #cleanup-hooks
1487+
[cppgc documentation]: ../deps/v8/include/cppgc/README.md
14491488
[event loop]: #event-loop
14501489
[exception handling]: #exception-handling
14511490
[fast API calls]: ../doc/contributing/adding-v8-fast-api.md

src/cppgc_helpers-inl.h

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#ifndef SRC_CPPGC_HELPERS_INL_H_
2+
#define SRC_CPPGC_HELPERS_INL_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include "cppgc_helpers.h"
7+
#include "env-inl.h"
8+
9+
namespace node {
10+
11+
template <typename T>
12+
void CppgcMixin::Wrap(T* ptr, Realm* realm, v8::Local<v8::Object> obj) {
13+
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
14+
ptr->realm_ = realm;
15+
v8::Isolate* isolate = realm->isolate();
16+
ptr->traced_reference_ = v8::TracedReference<v8::Object>(isolate, obj);
17+
// Note that ptr must be of concrete type T in Wrap.
18+
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(isolate, obj, ptr);
19+
// Keep the layout consistent with BaseObjects.
20+
obj->SetAlignedPointerInInternalField(
21+
kEmbedderType, realm->isolate_data()->embedder_id_for_cppgc());
22+
obj->SetAlignedPointerInInternalField(kSlot, ptr);
23+
realm->TrackCppgcWrapper(ptr);
24+
}
25+
26+
template <typename T>
27+
void CppgcMixin::Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
28+
Wrap(ptr, env->principal_realm(), obj);
29+
}
30+
31+
template <typename T>
32+
T* CppgcMixin::Unwrap(v8::Local<v8::Object> obj) {
33+
// We are not using v8::Object::Unwrap currently because that requires
34+
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
35+
// with ASSIGN_OR_RETURN_UNWRAP_GC doesn't take, and we also want a
36+
// signature consistent with BaseObject::Unwrap() to avoid churn. Since
37+
// cppgc-managed objects share the same layout as BaseObjects, just unwrap
38+
// from the pointer in the internal field, which should be valid as long as
39+
// the object is still alive.
40+
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
41+
return nullptr;
42+
}
43+
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
44+
return ptr;
45+
}
46+
47+
v8::Local<v8::Object> CppgcMixin::object() const {
48+
return traced_reference_.Get(realm_->isolate());
49+
}
50+
51+
Environment* CppgcMixin::env() const {
52+
return realm_->env();
53+
}
54+
55+
} // namespace node
56+
57+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
58+
59+
#endif // SRC_CPPGC_HELPERS_INL_H_

src/cppgc_helpers.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "cppgc_helpers.h"
2+
#include "env-inl.h"
3+
4+
namespace node {
5+
6+
void CppgcWrapperList::Cleanup() {
7+
for (auto handle : *this) {
8+
handle->Finalize();
9+
}
10+
}
11+
12+
void CppgcWrapperList::MemoryInfo(MemoryTracker* tracker) const {
13+
for (auto handle : *this) {
14+
tracker->Track(handle);
15+
}
16+
}
17+
18+
} // namespace node

src/cppgc_helpers.h

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
#include <type_traits> // std::remove_reference
77
#include "cppgc/garbage-collected.h"
88
#include "cppgc/name-provider.h"
9-
#include "env.h"
109
#include "memory_tracker.h"
10+
#include "util.h"
1111
#include "v8-cppgc.h"
1212
#include "v8-sandbox.h"
1313
#include "v8.h"
1414

1515
namespace node {
1616

17+
class Environment;
18+
class Realm;
19+
class CppgcWrapperList;
20+
1721
/**
1822
* This is a helper mixin with a BaseObject-like interface to help
1923
* implementing wrapper objects managed by V8's cppgc (Oilpan) library.
@@ -39,16 +43,15 @@ namespace node {
3943
* }
4044
*
4145
* If the wrapper needs to perform cleanups when it's destroyed and that
42-
* cleanup relies on a living Node.js `Environment`, it should implement a
46+
* cleanup relies on a living Node.js `Realm`, it should implement a
4347
* pattern like this:
4448
*
45-
* ~MyWrap() { this->Clean(); }
46-
* void CleanEnvResource(Environment* env) override {
49+
* ~MyWrap() { this->Destroy(); }
50+
* void Clean(Realm* env) override {
4751
* // Do cleanup that relies on a living Environemnt.
4852
* }
4953
*/
50-
class CppgcMixin : public cppgc::GarbageCollectedMixin,
51-
public CppgcWrapperListNode {
54+
class CppgcMixin : public cppgc::GarbageCollectedMixin, public MemoryRetainer {
5255
public:
5356
// To help various callbacks access wrapper objects with different memory
5457
// management, cppgc-managed objects share the same layout as BaseObjects.
@@ -58,71 +61,55 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin,
5861
// invoked from the child class constructor, per cppgc::GarbageCollectedMixin
5962
// rules.
6063
template <typename T>
61-
static void Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj) {
62-
CHECK_GE(obj->InternalFieldCount(), T::kInternalFieldCount);
63-
ptr->env_ = env;
64-
v8::Isolate* isolate = env->isolate();
65-
ptr->traced_reference_ = v8::TracedReference<v8::Object>(isolate, obj);
66-
v8::Object::Wrap<v8::CppHeapPointerTag::kDefaultTag>(isolate, obj, ptr);
67-
// Keep the layout consistent with BaseObjects.
68-
obj->SetAlignedPointerInInternalField(
69-
kEmbedderType, env->isolate_data()->embedder_id_for_cppgc());
70-
obj->SetAlignedPointerInInternalField(kSlot, ptr);
71-
env->cppgc_wrapper_list()->PushFront(ptr);
72-
}
64+
static inline void Wrap(T* ptr, Realm* realm, v8::Local<v8::Object> obj);
65+
template <typename T>
66+
static inline void Wrap(T* ptr, Environment* env, v8::Local<v8::Object> obj);
7367

74-
v8::Local<v8::Object> object() const {
75-
return traced_reference_.Get(env_->isolate());
68+
inline v8::Local<v8::Object> object() const;
69+
inline Environment* env() const;
70+
inline v8::Local<v8::Object> object(v8::Isolate* isolate) const {
71+
return traced_reference_.Get(isolate);
7672
}
7773

78-
Environment* env() const { return env_; }
79-
8074
template <typename T>
81-
static T* Unwrap(v8::Local<v8::Object> obj) {
82-
// We are not using v8::Object::Unwrap currently because that requires
83-
// access to isolate which the ASSIGN_OR_RETURN_UNWRAP macro that we'll shim
84-
// with ASSIGN_OR_RETURN_UNWRAP_GC doesn't take, and we also want a
85-
// signature consistent with BaseObject::Unwrap() to avoid churn. Since
86-
// cppgc-managed objects share the same layout as BaseObjects, just unwrap
87-
// from the pointer in the internal field, which should be valid as long as
88-
// the object is still alive.
89-
if (obj->InternalFieldCount() != T::kInternalFieldCount) {
90-
return nullptr;
91-
}
92-
T* ptr = static_cast<T*>(obj->GetAlignedPointerFromInternalField(T::kSlot));
93-
return ptr;
94-
}
75+
static inline T* Unwrap(v8::Local<v8::Object> obj);
9576

9677
// Subclasses are expected to invoke CppgcMixin::Trace() in their own Trace()
9778
// methods.
9879
void Trace(cppgc::Visitor* visitor) const override {
9980
visitor->Trace(traced_reference_);
10081
}
10182

102-
// This implements CppgcWrapperListNode::Clean and is run for all the
103-
// remaining Cppgc wrappers tracked in the Environment during Environment
104-
// shutdown. The destruction of the wrappers would happen later, when the
105-
// final garbage collection is triggered when CppHeap is torn down as part of
106-
// the Isolate teardown. If subclasses of CppgcMixin wish to perform cleanups
107-
// that depend on the Environment during destruction, they should implment it
108-
// in a CleanEnvResource() override, and then call this->Clean() from their
109-
// destructor. Outside of CleanEnvResource(), subclasses should avoid calling
83+
// TODO(joyeecheung): use ObjectSizeTrait;
84+
inline size_t SelfSize() const override { return sizeof(*this); }
85+
inline bool IsCppgcWrapper() const override { return true; }
86+
87+
// This is run for all the remaining Cppgc wrappers tracked in the Realm
88+
// during Realm shutdown. The destruction of the wrappers would happen later,
89+
// when the final garbage collection is triggered when CppHeap is torn down as
90+
// part of the Isolate teardown. If subclasses of CppgcMixin wish to perform
91+
// cleanups that depend on the Realm during destruction, they should implment
92+
// it in a Clean() override, and then call this->Finalize() from their
93+
// destructor. Outside of Finalize(), subclasses should avoid calling
11094
// into JavaScript or perform any operation that can trigger garbage
11195
// collection during the destruction.
112-
void Clean() override {
113-
if (env_ == nullptr) return;
114-
this->CleanEnvResource(env_);
115-
env_ = nullptr;
96+
void Finalize() {
97+
if (realm_ == nullptr) return;
98+
this->Clean(realm_);
99+
realm_ = nullptr;
116100
}
117101

118-
// The default implementation of CleanEnvResource() is a no-op. Subclasses
119-
// should override it to perform cleanup that require a living Environment,
102+
// The default implementation of Clean() is a no-op. Subclasses
103+
// should override it to perform cleanup that require a living Realm,
120104
// instead of doing these cleanups directly in the destructor.
121-
virtual void CleanEnvResource(Environment* env) {}
105+
virtual void Clean(Realm* realm) {}
106+
107+
friend class CppgcWrapperList;
122108

123109
private:
124-
Environment* env_ = nullptr;
110+
Realm* realm_ = nullptr;
125111
v8::TracedReference<v8::Object> traced_reference_;
112+
ListNode<CppgcMixin> wrapper_list_node_;
126113
};
127114

128115
// If the class doesn't have additional owned traceable data, use this macro to
@@ -137,7 +124,8 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin,
137124
#define SET_CPPGC_NAME(Klass) \
138125
inline const char* GetHumanReadableName() const final { \
139126
return "Node / " #Klass; \
140-
}
127+
} \
128+
inline const char* MemoryInfoName() const override { return #Klass; }
141129

142130
/**
143131
* Similar to ASSIGN_OR_RETURN_UNWRAP() but works on cppgc-managed types

src/env.cc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,8 +1302,6 @@ void Environment::RunCleanup() {
13021302
CleanupHandles();
13031303
}
13041304

1305-
for (CppgcWrapperListNode* handle : cppgc_wrapper_list_) handle->Clean();
1306-
13071305
for (const int fd : unmanaged_fds_) {
13081306
uv_fs_t close_req;
13091307
uv_fs_close(nullptr, &close_req, fd, nullptr);

0 commit comments

Comments
 (0)