Skip to content

Commit fa965bd

Browse files
committed
src: track cppgc wrappers with CppgcWrapperList in Environment
This allows us to perform cleanups of cppgc wrappers that rely on a living Environment during Environment shutdown. Otherwise the cleanup may happen during object destruction, which can be triggered by GC after Enivronment shutdown, leading to invalid access to Environment. The general pattern for this type of non-trivial destruction is designed to be: ``` class MyWrap final : CPPGC_MIXIN(MyWrap) { public: ~MyWrap() { this->Clean(); } void CleanEnvResource(Environment* env) override { // Do cleanup that relies on a living Environemnt. This would be // called by CppgcMixin::Clean() first during Environment shutdown, // while the Environment is still alive. If the destructor calls // Clean() again later during garbage collection that happens after // Environment shutdown, CleanEnvResource() would be skipped, preventing // invalid access to the Environment. } } ``` In addition, this allows us to trace external memory held by the wrappers in the heap snapshots if we add synthethic edges between the wrappers and other nodes in the embdder graph callback, or to perform snapshot serialization for them.
1 parent 4cc69f9 commit fa965bd

File tree

4 files changed

+87
-5
lines changed

4 files changed

+87
-5
lines changed

src/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,17 @@ class MyWrap final : CPPGC_MIXIN(MyWrap) {
11121112
}
11131113
```
11141114

1115+
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
1117+
pattern like this:
1118+
1119+
```cpp
1120+
~MyWrap() { this->Clean(); }
1121+
void CleanEnvResource(Environment* env) override {
1122+
// Do cleanup that relies on a living Environemnt.
1123+
}
1124+
```
1125+
11151126
`cppgc::GarbageCollected` types are expected to implement a
11161127
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
11171128
final class in the hierarchy, this method must be marked `final`. For
@@ -1266,6 +1277,8 @@ referrer->Set(
12661277
).ToLocalChecked();
12671278
```
12681279

1280+
#### Lifetime and cleanups of cppgc-managed objects
1281+
12691282
Typically, a newly created cppgc-managed wrapper object should be held alive
12701283
by the JavaScript land (for example, by being returned by a method and
12711284
staying alive in a closure). Long-lived cppgc objects can also
@@ -1277,6 +1290,27 @@ it, this can happen at any time after the garbage collector notices that
12771290
it's no longer reachable and before the V8 isolate is torn down.
12781291
See the [Oilpan documentation in Chromium][] for more details.
12791292

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:
1301+
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+
```
1313+
12801314
### Callback scopes
12811315
12821316
The public `CallbackScope` and the internally used `InternalCallbackScope`

src/cppgc_helpers.h

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,30 @@ namespace node {
2525
* with V8's GC scheduling.
2626
*
2727
* A cppgc-managed native wrapper should look something like this, note
28-
* that per cppgc rules, CPPGC_MIXIN(Klass) must be at the left-most
28+
* that per cppgc rules, CPPGC_MIXIN(MyWrap) must be at the left-most
2929
* position in the hierarchy (which ensures cppgc::GarbageCollected
3030
* is at the left-most position).
3131
*
32-
* class Klass final : CPPGC_MIXIN(Klass) {
32+
* class MyWrap final : CPPGC_MIXIN(MyWrap) {
3333
* public:
34-
* SET_CPPGC_NAME(Klass) // Sets the heap snapshot name to "Node / Klass"
34+
* SET_CPPGC_NAME(MyWrap) // Sets the heap snapshot name to "Node / MyWrap"
3535
* void Trace(cppgc::Visitor* visitor) const final {
3636
* CppgcMixin::Trace(visitor);
3737
* visitor->Trace(...); // Trace any additional owned traceable data
3838
* }
3939
* }
40+
*
41+
* 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
43+
* pattern like this:
44+
*
45+
* ~MyWrap() { this->Clean(); }
46+
* void CleanEnvResource(Environment* env) override {
47+
* // Do cleanup that relies on a living Environemnt.
48+
* }
4049
*/
41-
class CppgcMixin : public cppgc::GarbageCollectedMixin {
50+
class CppgcMixin : public cppgc::GarbageCollectedMixin,
51+
public CppgcWrapperListNode {
4252
public:
4353
// To help various callbacks access wrapper objects with different memory
4454
// management, cppgc-managed objects share the same layout as BaseObjects.
@@ -58,6 +68,7 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin {
5868
obj->SetAlignedPointerInInternalField(
5969
kEmbedderType, env->isolate_data()->embedder_id_for_cppgc());
6070
obj->SetAlignedPointerInInternalField(kSlot, ptr);
71+
env->cppgc_wrapper_list()->PushFront(ptr);
6172
}
6273

6374
v8::Local<v8::Object> object() const {
@@ -88,8 +99,29 @@ class CppgcMixin : public cppgc::GarbageCollectedMixin {
8899
visitor->Trace(traced_reference_);
89100
}
90101

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
110+
// into JavaScript or perform any operation that can trigger garbage
111+
// collection during the destruction.
112+
void Clean() override {
113+
if (env_ == nullptr) return;
114+
this->CleanEnvResource(env_);
115+
env_ = nullptr;
116+
}
117+
118+
// The default implementation of CleanEnvResource() is a no-op. Subclasses
119+
// should override it to perform cleanup that require a living Environment,
120+
// instead of doing these cleanups directly in the destructor.
121+
virtual void CleanEnvResource(Environment* env) {}
122+
91123
private:
92-
Environment* env_;
124+
Environment* env_ = nullptr;
93125
v8::TracedReference<v8::Object> traced_reference_;
94126
};
95127

src/env.cc

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

1305+
for (CppgcWrapperListNode* handle : cppgc_wrapper_list_) handle->Clean();
1306+
13051307
for (const int fd : unmanaged_fds_) {
13061308
uv_fs_t close_req;
13071309
uv_fs_close(nullptr, &close_req, fd, nullptr);

src/env.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,12 @@ class Cleanable {
602602
friend class Environment;
603603
};
604604

605+
class CppgcWrapperListNode {
606+
public:
607+
virtual void Clean() = 0;
608+
ListNode<CppgcWrapperListNode> wrapper_list_node;
609+
};
610+
605611
/**
606612
* Environment is a per-isolate data structure that represents an execution
607613
* environment. Each environment has a principal realm. An environment can
@@ -897,12 +903,18 @@ class Environment final : public MemoryRetainer {
897903
typedef ListHead<HandleWrap, &HandleWrap::handle_wrap_queue_> HandleWrapQueue;
898904
typedef ListHead<ReqWrapBase, &ReqWrapBase::req_wrap_queue_> ReqWrapQueue;
899905
typedef ListHead<Cleanable, &Cleanable::cleanable_queue_> CleanableQueue;
906+
typedef ListHead<CppgcWrapperListNode,
907+
&CppgcWrapperListNode::wrapper_list_node>
908+
CppgcWrapperList;
900909

901910
inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; }
902911
inline CleanableQueue* cleanable_queue() {
903912
return &cleanable_queue_;
904913
}
905914
inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; }
915+
inline CppgcWrapperList* cppgc_wrapper_list() {
916+
return &cppgc_wrapper_list_;
917+
}
906918

907919
// https://w3c.github.io/hr-time/#dfn-time-origin
908920
inline uint64_t time_origin() {
@@ -1191,6 +1203,8 @@ class Environment final : public MemoryRetainer {
11911203
CleanableQueue cleanable_queue_;
11921204
HandleWrapQueue handle_wrap_queue_;
11931205
ReqWrapQueue req_wrap_queue_;
1206+
CppgcWrapperList cppgc_wrapper_list_;
1207+
11941208
int handle_cleanup_waiting_ = 0;
11951209
int request_waiting_ = 0;
11961210

0 commit comments

Comments
 (0)