Skip to content

Commit a4ef690

Browse files
committed
src: use cppgc to manage ContextifyContext
This simplifies the memory management of ContextifyContext, making all references visible to V8. The destructors don't need to do anything because when the wrapper is going away, the context is already going away or otherwise it would've been holding the wrapper alive, so there's no need to reset the pointers in the context. Also, any global handles to the context would've been empty at this point, and the per-Environment context tracking code is capable of dealing with empty handles from contexts purged elsewhere. To this end, the context tracking code also purges empty handles from the list now, to prevent keeping too many empty handles around.
1 parent 2f35b1f commit a4ef690

File tree

4 files changed

+111
-51
lines changed

4 files changed

+111
-51
lines changed

src/env.cc

+6-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,12 @@ void AsyncHooks::InstallPromiseHooks(Local<Context> ctx) {
223223
: PersistentToLocal::Strong(js_promise_hooks_[3]));
224224
}
225225

226+
void Environment::PurgeTrackedEmptyContexts() {
227+
std::erase_if(contexts_, [&](auto&& el) { return el.IsEmpty(); });
228+
}
229+
226230
void Environment::TrackContext(Local<Context> context) {
231+
PurgeTrackedEmptyContexts();
227232
size_t id = contexts_.size();
228233
contexts_.resize(id + 1);
229234
contexts_[id].Reset(isolate_, context);
@@ -232,7 +237,7 @@ void Environment::TrackContext(Local<Context> context) {
232237

233238
void Environment::UntrackContext(Local<Context> context) {
234239
HandleScope handle_scope(isolate_);
235-
std::erase_if(contexts_, [&](auto&& el) { return el.IsEmpty(); });
240+
PurgeTrackedEmptyContexts();
236241
for (auto it = contexts_.begin(); it != contexts_.end(); it++) {
237242
if (Local<Context> saved_context = PersistentToLocal::Weak(isolate_, *it);
238243
saved_context == context) {

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ class Environment final : public MemoryRetainer {
10851085
const char* errmsg);
10861086
void TrackContext(v8::Local<v8::Context> context);
10871087
void UntrackContext(v8::Local<v8::Context> context);
1088+
void PurgeTrackedEmptyContexts();
10881089

10891090
std::list<binding::DLib> loaded_addons_;
10901091
v8::Isolate* const isolate_;

src/node_contextify.cc

+33-36
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ Local<Name> Uint32ToName(Local<Context> context, uint32_t index) {
118118

119119
} // anonymous namespace
120120

121-
BaseObjectPtr<ContextifyContext> ContextifyContext::New(
122-
Environment* env, Local<Object> sandbox_obj, ContextOptions* options) {
121+
ContextifyContext* ContextifyContext::New(Environment* env,
122+
Local<Object> sandbox_obj,
123+
ContextOptions* options) {
123124
Local<ObjectTemplate> object_template;
124125
HandleScope scope(env->isolate());
125126
CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla);
@@ -140,41 +141,32 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
140141
if (!(CreateV8Context(env->isolate(), object_template, snapshot_data, queue)
141142
.ToLocal(&v8_context))) {
142143
// Allocation failure, maximum call stack size reached, termination, etc.
143-
return BaseObjectPtr<ContextifyContext>();
144+
return nullptr;
144145
}
145146
return New(v8_context, env, sandbox_obj, options);
146147
}
147148

148-
void ContextifyContext::MemoryInfo(MemoryTracker* tracker) const {}
149+
void ContextifyContext::Trace(cppgc::Visitor* visitor) const {
150+
CppgcMixin::Trace(visitor);
151+
visitor->Trace(context_);
152+
}
149153

150154
ContextifyContext::ContextifyContext(Environment* env,
151155
Local<Object> wrapper,
152156
Local<Context> v8_context,
153157
ContextOptions* options)
154-
: BaseObject(env, wrapper),
155-
microtask_queue_(options->own_microtask_queue
158+
: microtask_queue_(options->own_microtask_queue
156159
? options->own_microtask_queue.release()
157160
: nullptr) {
161+
CppgcMixin::Wrap(this, env, wrapper);
162+
158163
context_.Reset(env->isolate(), v8_context);
159164
// This should only be done after the initial initializations of the context
160165
// global object is finished.
161166
DCHECK_NULL(v8_context->GetAlignedPointerFromEmbedderData(
162167
ContextEmbedderIndex::kContextifyContext));
163168
v8_context->SetAlignedPointerInEmbedderData(
164169
ContextEmbedderIndex::kContextifyContext, this);
165-
// It's okay to make this reference weak - V8 would create an internal
166-
// reference to this context via the constructor of the wrapper.
167-
// As long as the wrapper is alive, it's constructor is alive, and so
168-
// is the context.
169-
context_.SetWeak();
170-
}
171-
172-
ContextifyContext::~ContextifyContext() {
173-
Isolate* isolate = env()->isolate();
174-
HandleScope scope(isolate);
175-
176-
env()->UnassignFromContext(PersistentToLocal::Weak(isolate, context_));
177-
context_.Reset();
178170
}
179171

180172
void ContextifyContext::InitializeGlobalTemplates(IsolateData* isolate_data) {
@@ -251,19 +243,18 @@ MaybeLocal<Context> ContextifyContext::CreateV8Context(
251243
return scope.Escape(ctx);
252244
}
253245

254-
BaseObjectPtr<ContextifyContext> ContextifyContext::New(
255-
Local<Context> v8_context,
256-
Environment* env,
257-
Local<Object> sandbox_obj,
258-
ContextOptions* options) {
246+
ContextifyContext* ContextifyContext::New(Local<Context> v8_context,
247+
Environment* env,
248+
Local<Object> sandbox_obj,
249+
ContextOptions* options) {
259250
HandleScope scope(env->isolate());
260251
CHECK_IMPLIES(sandbox_obj.IsEmpty(), options->vanilla);
261252
// This only initializes part of the context. The primordials are
262253
// only initialized when needed because even deserializing them slows
263254
// things down significantly and they are only needed in rare occasions
264255
// in the vm contexts.
265256
if (InitializeContextRuntime(v8_context).IsNothing()) {
266-
return BaseObjectPtr<ContextifyContext>();
257+
return nullptr;
267258
}
268259

269260
Local<Context> main_context = env->context();
@@ -300,7 +291,7 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
300291
info.origin = *origin_val;
301292
}
302293

303-
BaseObjectPtr<ContextifyContext> result;
294+
ContextifyContext* result;
304295
Local<Object> wrapper;
305296
{
306297
Context::Scope context_scope(v8_context);
@@ -315,7 +306,7 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
315306
ctor_name,
316307
static_cast<v8::PropertyAttribute>(v8::DontEnum))
317308
.IsNothing()) {
318-
return BaseObjectPtr<ContextifyContext>();
309+
return nullptr;
319310
}
320311
}
321312

@@ -328,21 +319,23 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
328319
env->host_defined_option_symbol(),
329320
options->host_defined_options_id)
330321
.IsNothing()) {
331-
return BaseObjectPtr<ContextifyContext>();
322+
return nullptr;
332323
}
333324

334325
env->AssignToContext(v8_context, nullptr, info);
335326

336327
if (!env->contextify_wrapper_template()
337328
->NewInstance(v8_context)
338329
.ToLocal(&wrapper)) {
339-
return BaseObjectPtr<ContextifyContext>();
330+
return nullptr;
340331
}
341332

342-
result =
343-
MakeBaseObject<ContextifyContext>(env, wrapper, v8_context, options);
344-
// The only strong reference to the wrapper will come from the sandbox.
345-
result->MakeWeak();
333+
result = cppgc::MakeGarbageCollected<ContextifyContext>(
334+
env->isolate()->GetCppHeap()->GetAllocationHandle(),
335+
env,
336+
wrapper,
337+
v8_context,
338+
options);
346339
}
347340

348341
Local<Object> wrapper_holder =
@@ -352,7 +345,7 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
352345
->SetPrivate(
353346
v8_context, env->contextify_context_private_symbol(), wrapper)
354347
.IsNothing()) {
355-
return BaseObjectPtr<ContextifyContext>();
348+
return nullptr;
356349
}
357350

358351
// Assign host_defined_options_id to the sandbox object or the global object
@@ -364,7 +357,7 @@ BaseObjectPtr<ContextifyContext> ContextifyContext::New(
364357
env->host_defined_option_symbol(),
365358
options->host_defined_options_id)
366359
.IsNothing()) {
367-
return BaseObjectPtr<ContextifyContext>();
360+
return nullptr;
368361
}
369362
return result;
370363
}
@@ -438,7 +431,7 @@ void ContextifyContext::MakeContext(const FunctionCallbackInfo<Value>& args) {
438431
options.host_defined_options_id = args[6].As<Symbol>();
439432

440433
TryCatchScope try_catch(env);
441-
BaseObjectPtr<ContextifyContext> context_ptr =
434+
ContextifyContext* context_ptr =
442435
ContextifyContext::New(env, sandbox, &options);
443436

444437
if (try_catch.HasCaught()) {
@@ -469,6 +462,10 @@ ContextifyContext* ContextifyContext::ContextFromContextifiedSandbox(
469462

470463
template <typename T>
471464
ContextifyContext* ContextifyContext::Get(const PropertyCallbackInfo<T>& args) {
465+
// TODO(joyeecheung): it should be fine to simply use
466+
// args.GetIsolate()->GetCurrentContext() and take the pointer at
467+
// ContextEmbedderIndex::kContextifyContext, as V8 is supposed to
468+
// push the creation context before invoking these callbacks.
472469
return Get(args.This());
473470
}
474471

src/node_contextify.h

+71-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
55

66
#include "base_object-inl.h"
7+
#include "cppgc/prefinalizer.h"
78
#include "cppgc_helpers.h"
89
#include "node_context_data.h"
910
#include "node_errors.h"
@@ -23,17 +24,73 @@ struct ContextOptions {
2324
bool vanilla = false;
2425
};
2526

26-
class ContextifyContext : public BaseObject {
27+
/**
28+
* The memory management of a vm context is as follows:
29+
*
30+
* user code
31+
* │
32+
* As global proxy or ▼
33+
* ┌──────────────┐ kSandboxObject embedder data ┌────────────────┐
34+
* ┌─► │ V8 Context │────────────────────────────────►│ Wrapper holder │
35+
* │ └──────────────┘ └───────┬────────┘
36+
* │ ▲ Object constructor/creation context │
37+
* │ │ │
38+
* │ ┌──────┴────────────┐ contextify_context_private_symbol │
39+
* │ │ ContextifyContext │◄────────────────────────────────────┘
40+
* │ │ JS Wrapper │◄──────────► ┌─────────────────────────┐
41+
* │ └───────────────────┘ cppgc │ node::ContextifyContext │
42+
* │ │ C++ Object │
43+
* └──────────────────────────────────► └─────────────────────────┘
44+
* v8::TracedReference / ContextEmbedderIndex::kContextifyContext
45+
*
46+
* There are two possibilities for the "wrapper holder":
47+
*
48+
* 1. When vm.constants.DONT_CONTEXTIFY is used, the wrapper holder is the V8
49+
* context's global proxy object
50+
* 2. Otherwise it's the arbitrary "sandbox object" that users pass into
51+
* vm.createContext() or a new empty object created internally if they pass
52+
* undefined.
53+
*
54+
* In 2, the global object of the new V8 context is created using
55+
* global_object_template with interceptors that perform any requested
56+
* operations on the global object in the context first on the sandbox object
57+
* living outside of the new context, then fall back to the global proxy of the
58+
* new context.
59+
*
60+
* It's critical for the user-accessible wrapper holder to keep the
61+
* ContextifyContext wrapper alive via contextify_context_private_symbol
62+
* so that the V8 context is always available to the user while they still
63+
* hold the vm "context" object alive.
64+
*
65+
* It's also critical for the V8 context to keep the wrapper holder
66+
* (specifically, the "sandbox object") as well as the node::ContextifyContext
67+
* C++ object alive, so that when the code runs inside the object and accesses
68+
* the global object, the interceptors can still access the "sandbox object"
69+
* as well as and perform operations
70+
* on them, even if users already relinquish access to the outer
71+
* "sandbox object".
72+
*
73+
* The v8::TracedReference and the ContextEmbedderIndex::kContextifyContext
74+
* slot in the context act as shortcuts from the node::ContextifyContext
75+
* C++ object to the V8 context.
76+
*/
77+
class ContextifyContext final : CPPGC_MIXIN(ContextifyContext) {
2778
public:
79+
SET_CPPGC_NAME(ContextifyContext)
80+
void Trace(cppgc::Visitor* visitor) const final;
81+
2882
ContextifyContext(Environment* env,
2983
v8::Local<v8::Object> wrapper,
3084
v8::Local<v8::Context> v8_context,
3185
ContextOptions* options);
32-
~ContextifyContext();
3386

34-
void MemoryInfo(MemoryTracker* tracker) const override;
35-
SET_MEMORY_INFO_NAME(ContextifyContext)
36-
SET_SELF_SIZE(ContextifyContext)
87+
// The destructors don't need to do anything because when the wrapper is
88+
// going away, the context is already going away or otherwise it would've
89+
// been holding the wrapper alive, so there's no need to reset the pointers
90+
// in the context. Also, any global handles to the context would've been
91+
// empty at this point, and the per-Environment context tracking code is
92+
// capable of dealing with empty handles from contexts purged elsewhere.
93+
~ContextifyContext() {}
3794

3895
static v8::MaybeLocal<v8::Context> CreateV8Context(
3996
v8::Isolate* isolate,
@@ -48,7 +105,7 @@ class ContextifyContext : public BaseObject {
48105
Environment* env, const v8::Local<v8::Object>& wrapper_holder);
49106

50107
inline v8::Local<v8::Context> context() const {
51-
return PersistentToLocal::Default(env()->isolate(), context_);
108+
return context_.Get(env()->isolate());
52109
}
53110

54111
inline v8::Local<v8::Object> global_proxy() const {
@@ -75,14 +132,14 @@ class ContextifyContext : public BaseObject {
75132
static void InitializeGlobalTemplates(IsolateData* isolate_data);
76133

77134
private:
78-
static BaseObjectPtr<ContextifyContext> New(Environment* env,
79-
v8::Local<v8::Object> sandbox_obj,
80-
ContextOptions* options);
135+
static ContextifyContext* New(Environment* env,
136+
v8::Local<v8::Object> sandbox_obj,
137+
ContextOptions* options);
81138
// Initialize a context created from CreateV8Context()
82-
static BaseObjectPtr<ContextifyContext> New(v8::Local<v8::Context> ctx,
83-
Environment* env,
84-
v8::Local<v8::Object> sandbox_obj,
85-
ContextOptions* options);
139+
static ContextifyContext* New(v8::Local<v8::Context> ctx,
140+
Environment* env,
141+
v8::Local<v8::Object> sandbox_obj,
142+
ContextOptions* options);
86143

87144
static bool IsStillInitializing(const ContextifyContext* ctx);
88145
static void MakeContext(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -140,7 +197,7 @@ class ContextifyContext : public BaseObject {
140197
static void IndexedPropertyEnumeratorCallback(
141198
const v8::PropertyCallbackInfo<v8::Array>& args);
142199

143-
v8::Global<v8::Context> context_;
200+
v8::TracedReference<v8::Context> context_;
144201
std::unique_ptr<v8::MicrotaskQueue> microtask_queue_;
145202
};
146203

0 commit comments

Comments
 (0)