Skip to content

Commit 9b53840

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 5c83957 commit 9b53840

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
@@ -1050,6 +1050,17 @@ class MyWrap final : CPPGC_MIXIN(MyWrap) {
10501050
}
10511051
```
10521052
1053+
If the wrapper needs to perform cleanups when it's destroyed and that
1054+
cleanup relies on a living Node.js `Environment`, it should implement a
1055+
pattern like this:
1056+
1057+
```cpp
1058+
~MyWrap() { this->Clean(); }
1059+
void CleanEnvResource(Environment* env) override {
1060+
// Do cleanup that relies on a living Environemnt.
1061+
}
1062+
```
1063+
10531064
`cppgc::GarbageCollected` types are expected to implement a
10541065
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
10551066
final class in the hierarchy, this method must be marked `final`. For
@@ -1204,6 +1215,8 @@ referrer->Set(
12041215
).ToLocalChecked();
12051216
```
12061217

1218+
#### Lifetime and cleanups of cppgc-managed objects
1219+
12071220
Typically, a newly created cppgc-managed wrapper object should be held alive
12081221
by the JavaScript land (for example, by being returned by a method and
12091222
staying alive in a closure). Long-lived cppgc objects can also
@@ -1215,6 +1228,27 @@ it, this can happen at any time after the garbage collector notices that
12151228
it's no longer reachable and before the V8 isolate is torn down.
12161229
See the [Oilpan documentation in Chromium][] for more details.
12171230

1231+
If the cppgc-managed objects does not need to perform any special cleanup,
1232+
it's fine to use the default destructor. If it needs to perform only trivial
1233+
cleanup that only affects its own members without calling into JS, potentially
1234+
triggering garbage collection or relying on a living `Environment`, then it's
1235+
fine to just implement the trivial cleanup in the destructor directly. If it
1236+
needs to do any substantial cleanup that involves a living `Environment`, because
1237+
the destructor can be called at any time by the garbage collection, even after
1238+
the `Environment` is already gone, it must implement the cleanup with this pattern:
1239+
1240+
```cpp
1241+
~MyWrap() { this->Clean(); }
1242+
void CleanEnvResource(Environment* env) override {
1243+
// Do cleanup that relies on a living Environemnt. This would be
1244+
// called by CppgcMixin::Clean() first during Environment shutdown,
1245+
// while the Environment is still alive. If the destructor calls
1246+
// Clean() again later during garbage collection that happens after
1247+
// Environment shutdown, CleanEnvResource() would be skipped, preventing
1248+
// invalid access to the Environment.
1249+
}
1250+
```
1251+
12181252
### Callback scopes
12191253
12201254
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
@@ -1299,6 +1299,8 @@ void Environment::RunCleanup() {
12991299
CleanupHandles();
13001300
}
13011301

1302+
for (CppgcWrapperListNode* handle : cppgc_wrapper_list_) handle->Clean();
1303+
13021304
for (const int fd : unmanaged_fds_) {
13031305
uv_fs_t close_req;
13041306
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)