Skip to content

Commit 37e7bac

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 7c3aa9f commit 37e7bac

File tree

4 files changed

+88
-6
lines changed

4 files changed

+88
-6
lines changed

src/README.md

+34
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,17 @@ class MyWrap final : CPPGC_MIXIN(MyWrap) {
10411041
}
10421042
```
10431043
1044+
If the wrapper needs to perform cleanups when it's destroyed and that
1045+
cleanup relies on a living Node.js `Environment`, it should implement a
1046+
pattern like this:
1047+
1048+
```c++
1049+
~MyWrap() { this->Clean(); }
1050+
void CleanEnvResource(Environment* env) override {
1051+
// Do cleanup that relies on a living Environemnt.
1052+
}
1053+
```
1054+
10441055
`cppgc::GarbageCollected` types are expected to implement a
10451056
`void Trace(cppgc::Visitor* visitor) const` method. When they are the
10461057
final class in the hierarchy, this method must be marked `final`. For
@@ -1195,6 +1206,8 @@ referrer->Set(
11951206
).ToLocalChecked();
11961207
```
11971208

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

1222+
If the cppgc-managed objects does not need to perform any special cleanup,
1223+
it's fine to use the default destructor. If it needs to perform only trivial
1224+
cleanup that only affects its own members without calling into JS, potentially
1225+
triggering garbage collection or relying on a living `Environment`, then it's
1226+
fine to just implement the trivial cleanup in the destructor directly. If it
1227+
needs to do any substantial cleanup that involves a living `Environment`, because
1228+
the destructor can be called at any time by the garbage collection, even after
1229+
the `Environment` is already gone, it must implement the cleanup with this pattern:
1230+
1231+
```c++
1232+
~MyWrap() { this->Clean(); }
1233+
void CleanEnvResource(Environment* env) override {
1234+
// Do cleanup that relies on a living Environemnt. This would be
1235+
// called by CppgcMixin::Clean() first during Environment shutdown,
1236+
// while the Environment is still alive. If the destructor calls
1237+
// Clean() again later during garbage collection that happens after
1238+
// Environment shutdown, CleanEnvResource() would be skipped, preventing
1239+
// invalid access to the Environment.
1240+
}
1241+
```
1242+
12091243
### Callback scopes
12101244
12111245
The public `CallbackScope` and the internally used `InternalCallbackScope`

src/cppgc_helpers.h

+37-5
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

+2
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,8 @@ void Environment::RunCleanup() {
12901290
CleanupHandles();
12911291
}
12921292

1293+
for (CppgcWrapperListNode* handle : cppgc_wrapper_list_) handle->Clean();
1294+
12931295
for (const int fd : unmanaged_fds_) {
12941296
uv_fs_t close_req;
12951297
uv_fs_close(nullptr, &close_req, fd, nullptr);

src/env.h

+15-1
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,12 @@ class Cleanable {
615615
friend class Environment;
616616
};
617617

618+
class CppgcWrapperListNode {
619+
public:
620+
virtual void Clean() = 0;
621+
ListNode<CppgcWrapperListNode> wrapper_list_node;
622+
};
623+
618624
/**
619625
* Environment is a per-isolate data structure that represents an execution
620626
* environment. Each environment has a principal realm. An environment can
@@ -703,6 +709,7 @@ class Environment final : public MemoryRetainer {
703709
Realm* realm,
704710
const ContextInfo& info);
705711
void UnassignFromContext(v8::Local<v8::Context> context);
712+
void UntrackContext(v8::Local<v8::Context> context);
706713
void TrackShadowRealm(shadow_realm::ShadowRealm* realm);
707714
void UntrackShadowRealm(shadow_realm::ShadowRealm* realm);
708715

@@ -910,12 +917,18 @@ class Environment final : public MemoryRetainer {
910917
typedef ListHead<HandleWrap, &HandleWrap::handle_wrap_queue_> HandleWrapQueue;
911918
typedef ListHead<ReqWrapBase, &ReqWrapBase::req_wrap_queue_> ReqWrapQueue;
912919
typedef ListHead<Cleanable, &Cleanable::cleanable_queue_> CleanableQueue;
920+
typedef ListHead<CppgcWrapperListNode,
921+
&CppgcWrapperListNode::wrapper_list_node>
922+
CppgcWrapperList;
913923

914924
inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; }
915925
inline CleanableQueue* cleanable_queue() {
916926
return &cleanable_queue_;
917927
}
918928
inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; }
929+
inline CppgcWrapperList* cppgc_wrapper_list() {
930+
return &cppgc_wrapper_list_;
931+
}
919932

920933
// https://w3c.github.io/hr-time/#dfn-time-origin
921934
inline uint64_t time_origin() {
@@ -1084,7 +1097,6 @@ class Environment final : public MemoryRetainer {
10841097
v8::Local<v8::Value>),
10851098
const char* errmsg);
10861099
void TrackContext(v8::Local<v8::Context> context);
1087-
void UntrackContext(v8::Local<v8::Context> context);
10881100

10891101
std::list<binding::DLib> loaded_addons_;
10901102
v8::Isolate* const isolate_;
@@ -1203,6 +1215,8 @@ class Environment final : public MemoryRetainer {
12031215
CleanableQueue cleanable_queue_;
12041216
HandleWrapQueue handle_wrap_queue_;
12051217
ReqWrapQueue req_wrap_queue_;
1218+
CppgcWrapperList cppgc_wrapper_list_;
1219+
12061220
int handle_cleanup_waiting_ = 0;
12071221
int request_waiting_ = 0;
12081222

0 commit comments

Comments
 (0)