mini_racer-csim 0.21.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2692 @@
1
+ #include "v8.h"
2
+ #include "v8-profiler.h"
3
+ #include "libplatform/libplatform.h"
4
+ #include "mini_racer_v8.h"
5
+ #include <memory>
6
+ #include <string>
7
+ #include <unordered_map>
8
+ #include <unordered_set>
9
+ #include <utility>
10
+ #include <vector>
11
+ #include <cassert>
12
+ #include <cstdio>
13
+ #include <cstdint>
14
+ #include <cstdlib>
15
+ #include <cstring>
16
+ #include <vector>
17
+
18
+ // note: the filter function gets called inside the safe context,
19
+ // i.e., the context that has not been tampered with by user JS
20
+ // convention: $-prefixed identifiers signify objects from the
21
+ // user JS context and should be handled with special care
22
+ static const char safe_context_script_source[] = R"js(
23
+ ;(function($globalThis) {
24
+ const {Map: $Map, Set: $Set} = $globalThis
25
+ const sentinel = {}
26
+ return function filter(v) {
27
+ if (typeof v === "function")
28
+ return sentinel
29
+ if (typeof v !== "object" || v === null)
30
+ return v
31
+ if (v instanceof $Map) {
32
+ const m = new Map()
33
+ for (let [k, t] of Map.prototype.entries.call(v)) {
34
+ t = filter(t)
35
+ if (t !== sentinel)
36
+ m.set(k, t)
37
+ }
38
+ return m
39
+ } else if (v instanceof $Set) {
40
+ const s = new Set()
41
+ for (let t of Set.prototype.values.call(v)) {
42
+ t = filter(t)
43
+ if (t !== sentinel)
44
+ s.add(t)
45
+ }
46
+ return s
47
+ } else {
48
+ const o = Array.isArray(v) ? [] : {}
49
+ const pds = Object.getOwnPropertyDescriptors(v)
50
+ for (const [k, d] of Object.entries(pds)) {
51
+ if (!d.enumerable)
52
+ continue
53
+ let t = d.value
54
+ if (d.get) {
55
+ // *not* d.get.call(...), may have been tampered with
56
+ t = Function.prototype.call.call(d.get, v, k)
57
+ }
58
+ t = filter(t)
59
+ if (t !== sentinel)
60
+ Object.defineProperty(o, k, {value: t, enumerable: true})
61
+ }
62
+ return o
63
+ }
64
+ }
65
+ })
66
+ )js";
67
+
68
+ struct Callback
69
+ {
70
+ struct State *st;
71
+ int32_t id;
72
+ // The dotted global path the callback was attached at (e.g. "foo.bar").
73
+ // Retained so the JS shim function can be re-bound onto a fresh global
74
+ // after Context#reset_realm swaps the user realm.
75
+ std::string name;
76
+ };
77
+
78
+ // V8 doesn't expose ScriptOrigin's filename back from a v8::Module
79
+ // (UnboundModuleScript only exposes the //# sourceURL magic comment),
80
+ // so we cache the filename here at compile time. Used to populate the
81
+ // referrer URL passed to the Ruby resolver block and `import.meta.url`.
82
+ // v8::Global (not Persistent) so ~ModuleEntry releases the V8 handle
83
+ // eagerly — Persistent's default traits skip Reset() in the destructor.
84
+ struct ModuleEntry
85
+ {
86
+ v8::Global<v8::Module> handle;
87
+ std::string filename;
88
+ };
89
+
90
+ // Transient per-load resolution map, reachable from graph_resolve_callback
91
+ // (which has no embedder slot) via State::active_graph. The modules themselves
92
+ // live in the persistent URL registry (State::module_id_by_url); this only
93
+ // records how each import edge resolved during the walk.
94
+ struct GraphLoad
95
+ {
96
+ // referrer_url '\0' specifier -> resolved url ("" = embedder returned nil)
97
+ std::unordered_map<std::string, std::string> edges;
98
+ };
99
+
100
+ // NOTE: do *not* use thread_locals to store state. In single-threaded
101
+ // mode, V8 runs on the same thread as Ruby and the Ruby runtime clobbers
102
+ // thread-locals when it context-switches threads. Ruby 3.4.0 has a new
103
+ // API rb_thread_lock_native_thread() that pins the thread but I don't
104
+ // think we're quite ready yet to drop support for older versions, hence
105
+ // this inelegant "everything" struct.
106
+ struct State
107
+ {
108
+ v8::Isolate *isolate;
109
+ // declaring as Local is safe because we take special care
110
+ // to ensure it's rooted in a HandleScope before being used
111
+ v8::Local<v8::Context> context;
112
+ // extra context for when we need access to built-ins like Array
113
+ // and want to be sure they haven't been tampered with by JS code
114
+ v8::Local<v8::Context> safe_context;
115
+ v8::Local<v8::Function> safe_context_function;
116
+ // Canonical roots for the user/safe realm. The Local members above are
117
+ // re-derived from these on every request (see v8_threaded_enter /
118
+ // v8_single_threaded_enter), so swapping the realm is just a matter of
119
+ // Reset()-ing these in install_realm — the old realm then has no roots
120
+ // left and is collected.
121
+ v8::Persistent<v8::Context> persistent_context;
122
+ v8::Persistent<v8::Context> persistent_safe_context;
123
+ v8::Persistent<v8::Function> persistent_safe_context_function;
124
+ v8::Persistent<v8::Value> ruby_exception;
125
+ // The opt-in host namespace name (Context.new(host_namespace:)), retained
126
+ // so it can be re-installed on a fresh realm after reset_realm. Empty when
127
+ // the embedder did not opt in.
128
+ std::string host_namespace;
129
+ Context *ruby_context;
130
+ int64_t max_memory;
131
+ int err_reason;
132
+ bool verbose_exceptions;
133
+ std::vector<Callback*> callbacks;
134
+ // v8::Global (not Persistent): Global's destructor Reset()s the handle,
135
+ // so erase()/clear() actually release the compiled script eagerly.
136
+ // Default-traits Persistent has kResetInDestructor=false — destroying it
137
+ // is a no-op that leaks the global handle until isolate->Dispose(), which
138
+ // would silently defeat Script#dispose. Cleared in ~State() under the
139
+ // still-live isolate so each Global can Reset() before isolate->Dispose().
140
+ std::unordered_map<int32_t, v8::Global<v8::Script>> scripts;
141
+ int32_t next_script_id;
142
+ // ModuleEntry holds v8::Global<Module> + cached filename.
143
+ std::unordered_map<int32_t, std::unique_ptr<ModuleEntry>> modules;
144
+ int32_t next_module_id;
145
+ // Context-persistent "1 URL = 1 Module" registry: url -> id into `modules`.
146
+ // Populated by load_module_graph and by registry-backed dynamic import, so
147
+ // every load path that touches a URL shares one Module instance for the
148
+ // life of the realm. Cleared with `modules` on reset_realm / teardown.
149
+ std::unordered_map<std::string, int32_t> module_id_by_url;
150
+ // True once load_module_graph has run: routes dynamic import() through the
151
+ // URL registry + the persisted resolve/fetch_batch callbacks instead of the
152
+ // legacy per-import dynamic_import_resolver.
153
+ bool uses_graph_loader;
154
+ // Depth counter incremented while v8_api_callback is on the stack.
155
+ // CreateCodeCache walks live isolate state and corrupts the parser
156
+ // when invoked from within a JS->Ruby->JS frame; see compile()'s
157
+ // `produce_cache` handling.
158
+ int in_callback;
159
+ // Set for the duration of v8_load_module_graph's InstantiateModule call so
160
+ // graph_resolve_callback can resolve imports from the pre-walked graph
161
+ // (url->Module + edge map) with zero Ruby round-trips. Null otherwise.
162
+ struct GraphLoad *active_graph;
163
+ std::unique_ptr<v8::ArrayBuffer::Allocator> allocator;
164
+ inline ~State();
165
+ };
166
+
167
+ namespace {
168
+
169
+ // deliberately leaked on program exit,
170
+ // not safe to destroy after main() returns
171
+ v8::Platform *platform;
172
+
173
+ struct Serialized
174
+ {
175
+ uint8_t *data = nullptr;
176
+ size_t size = 0;
177
+
178
+ Serialized(State& st, v8::Local<v8::Value> v)
179
+ {
180
+ v8::ValueSerializer ser(st.isolate);
181
+ ser.WriteHeader();
182
+ if (!ser.WriteValue(st.context, v).FromMaybe(false)) return; // exception pending
183
+ auto pair = ser.Release();
184
+ data = pair.first;
185
+ size = pair.second;
186
+ }
187
+
188
+ ~Serialized()
189
+ {
190
+ free(data);
191
+ }
192
+ };
193
+
194
+ bool bubble_up_ruby_exception(State& st, v8::TryCatch *try_catch)
195
+ {
196
+ auto exception = try_catch->Exception();
197
+ if (exception.IsEmpty()) return false;
198
+ auto ruby_exception = v8::Local<v8::Value>::New(st.isolate, st.ruby_exception);
199
+ if (ruby_exception.IsEmpty()) return false;
200
+ if (!ruby_exception->SameValue(exception)) return false;
201
+ // signal that the ruby thread should reraise the exception
202
+ // that it caught earlier when executing a js->ruby callback
203
+ uint8_t c = 'e';
204
+ v8_reply(st.ruby_context, &c, 1);
205
+ return true;
206
+ }
207
+
208
+ // throws JS exception on serialization error
209
+ bool reply(State& st, v8::Local<v8::Value> v)
210
+ {
211
+ v8::TryCatch try_catch(st.isolate);
212
+ {
213
+ Serialized serialized(st, v);
214
+ if (serialized.data) {
215
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
216
+ return true;
217
+ }
218
+ }
219
+ if (!try_catch.CanContinue()) {
220
+ try_catch.ReThrow();
221
+ return false;
222
+ }
223
+ auto recv = v8::Undefined(st.isolate);
224
+ if (!st.safe_context_function->Call(st.safe_context, recv, 1, &v).ToLocal(&v)) {
225
+ try_catch.ReThrow();
226
+ return false;
227
+ }
228
+ Serialized serialized(st, v);
229
+ if (serialized.data)
230
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
231
+ return serialized.data != nullptr; // exception pending if false
232
+ }
233
+
234
+ bool reply(State& st, v8::Local<v8::Value> result, v8::Local<v8::Value> err)
235
+ {
236
+ v8::TryCatch try_catch(st.isolate);
237
+ try_catch.SetVerbose(st.verbose_exceptions);
238
+ v8::Local<v8::Array> response;
239
+ {
240
+ v8::Context::Scope context_scope(st.safe_context);
241
+ response = v8::Array::New(st.isolate, 2);
242
+ }
243
+ response->Set(st.context, 0, result).Check();
244
+ response->Set(st.context, 1, err).Check();
245
+ if (reply(st, response)) return true;
246
+ if (!try_catch.CanContinue()) { // termination exception?
247
+ try_catch.ReThrow();
248
+ return false;
249
+ }
250
+ v8::String::Utf8Value s(st.isolate, try_catch.Exception());
251
+ const char *message = *s ? *s : "unexpected failure";
252
+ // most serialization errors will be DataCloneErrors but not always
253
+ // DataCloneErrors are not directly detectable so use a heuristic
254
+ if (!strstr(message, "could not be cloned")) {
255
+ try_catch.ReThrow();
256
+ return false;
257
+ }
258
+ // return an {"error": "foo could not be cloned"} object
259
+ v8::Local<v8::Object> error;
260
+ {
261
+ v8::Context::Scope context_scope(st.safe_context);
262
+ error = v8::Object::New(st.isolate);
263
+ }
264
+ auto key = v8::String::NewFromUtf8Literal(st.isolate, "error");
265
+ v8::Local<v8::String> val;
266
+ if (!v8::String::NewFromUtf8(st.isolate, message).ToLocal(&val)) {
267
+ val = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
268
+ }
269
+ error->Set(st.context, key, val).Check();
270
+ response->Set(st.context, 0, error).Check();
271
+ if (!reply(st, response)) {
272
+ try_catch.ReThrow();
273
+ return false;
274
+ }
275
+ return true;
276
+ }
277
+
278
+ // for when a reply is not expected to fail because of serialization
279
+ // errors but can still fail when preempted by isolate termination;
280
+ // temporarily cancels the termination exception so it can send the reply
281
+ void reply_retry(State& st, v8::Local<v8::Value> response)
282
+ {
283
+ v8::TryCatch try_catch(st.isolate);
284
+ try_catch.SetVerbose(st.verbose_exceptions);
285
+ bool ok = reply(st, response);
286
+ while (!ok) {
287
+ assert(try_catch.HasCaught());
288
+ assert(try_catch.HasTerminated());
289
+ if (!try_catch.HasTerminated()) abort();
290
+ st.isolate->CancelTerminateExecution();
291
+ ok = reply(st, response);
292
+ st.isolate->TerminateExecution();
293
+ }
294
+ }
295
+
296
+ v8::Local<v8::Value> sanitize(State& st, v8::Local<v8::Value> v)
297
+ {
298
+ // punch through proxies
299
+ while (v->IsProxy()) v = v8::Proxy::Cast(*v)->GetTarget();
300
+ // V8's serializer doesn't accept symbols
301
+ if (v->IsSymbol()) return v8::Symbol::Cast(*v)->Description(st.isolate);
302
+ // TODO(bnoordhuis) replace this hack with something more principled
303
+ if (v->IsFunction()) {
304
+ auto type = v8::NewStringType::kNormal;
305
+ const size_t size = sizeof(js_function_marker) / sizeof(*js_function_marker);
306
+ return v8::String::NewFromTwoByte(st.isolate, js_function_marker, type, size).ToLocalChecked();
307
+ }
308
+ if (v->IsWeakMap() || v->IsWeakSet() || v->IsMapIterator() || v->IsSetIterator()) {
309
+ bool is_key_value;
310
+ v8::Local<v8::Array> array;
311
+ if (v8::Object::Cast(*v)->PreviewEntries(&is_key_value).ToLocal(&array)) {
312
+ return array;
313
+ }
314
+ }
315
+ return v;
316
+ }
317
+
318
+ v8::Local<v8::String> to_error(State& st, v8::TryCatch *try_catch, int cause)
319
+ {
320
+ v8::Local<v8::Value> t;
321
+ char buf[1024];
322
+
323
+ *buf = '\0';
324
+ if (cause == NO_ERROR) {
325
+ // nothing to do
326
+ } else if (cause == PARSE_ERROR) {
327
+ auto message = try_catch->Message();
328
+ v8::String::Utf8Value s(st.isolate, message->Get());
329
+ v8::String::Utf8Value name(st.isolate, message->GetScriptResourceName());
330
+ if (!*s || !*name) goto fallback;
331
+ auto line = message->GetLineNumber(st.context).FromMaybe(0);
332
+ auto column = message->GetStartColumn(st.context).FromMaybe(0);
333
+ snprintf(buf, sizeof(buf), "%c%s at %s:%d:%d", cause, *s, *name, line, column);
334
+ } else if (try_catch->StackTrace(st.context).ToLocal(&t)) {
335
+ v8::String::Utf8Value s(st.isolate, t);
336
+ if (!*s) goto fallback;
337
+ snprintf(buf, sizeof(buf), "%c%s", cause, *s);
338
+ } else {
339
+ fallback:
340
+ v8::String::Utf8Value s(st.isolate, try_catch->Exception());
341
+ const char *message = *s ? *s : "unexpected failure";
342
+ if (cause == MEMORY_ERROR) message = "out of memory";
343
+ if (cause == TERMINATED_ERROR) message = "terminated";
344
+ snprintf(buf, sizeof(buf), "%c%s", cause, message);
345
+ }
346
+ v8::Local<v8::String> s;
347
+ if (v8::String::NewFromUtf8(st.isolate, buf).ToLocal(&s)) return s;
348
+ return v8::String::Empty(st.isolate);
349
+ }
350
+
351
+ extern "C" void v8_global_init(void)
352
+ {
353
+ char *p;
354
+ size_t n;
355
+
356
+ v8_get_flags(&p, &n);
357
+ if (p) {
358
+ for (char *s = p; s < p+n; s += 1 + strlen(s)) {
359
+ v8::V8::SetFlagsFromString(s);
360
+ }
361
+ free(p);
362
+ }
363
+ v8::V8::InitializeICU();
364
+ if (single_threaded) {
365
+ platform = v8::platform::NewSingleThreadedDefaultPlatform().release();
366
+ } else {
367
+ platform = v8::platform::NewDefaultPlatform().release();
368
+ }
369
+ v8::V8::InitializePlatform(platform);
370
+ v8::V8::Initialize();
371
+ }
372
+
373
+ void v8_gc_callback(v8::Isolate*, v8::GCType, v8::GCCallbackFlags, void *data)
374
+ {
375
+ State& st = *static_cast<State*>(data);
376
+ v8::HeapStatistics s;
377
+ st.isolate->GetHeapStatistics(&s);
378
+ int64_t used_heap_size = static_cast<int64_t>(s.used_heap_size());
379
+ if (used_heap_size > st.max_memory) {
380
+ st.err_reason = MEMORY_ERROR;
381
+ st.isolate->TerminateExecution();
382
+ }
383
+ }
384
+
385
+ // Linear scan of st.modules to map a Local<Module> back to the filename
386
+ // captured at compile time. Returns empty string if the module isn't ours
387
+ // (shouldn't happen — all live modules come from v8_compile_module /
388
+ // load_module_graph). st.modules holds every module for the realm's lifetime
389
+ // (reset_realm/teardown is the only reclaim point), so this scan is O(N) in the
390
+ // realm's module count; fine for the per-visit-fresh-realm model (N is a single
391
+ // page's graph), but a reverse index would be needed for a long-lived realm that
392
+ // lazily imports many modules.
393
+ static const std::string& module_filename(State& st, v8::Local<v8::Module> mod)
394
+ {
395
+ static const std::string empty;
396
+ for (auto& kv : st.modules) {
397
+ auto stored = v8::Local<v8::Module>::New(st.isolate, kv.second->handle);
398
+ if (stored == mod) return kv.second->filename;
399
+ }
400
+ return empty;
401
+ }
402
+
403
+ // RAII marker that the V8 thread is suspended inside a host->Ruby->host
404
+ // roundtrip — a host-function call (v8_api_callback), a dynamic import, or a
405
+ // module-resolve. While in_callback is nonzero: compile() refuses
406
+ // CreateCodeCache (it corrupts V8's parser when run from such a frame), and
407
+ // reset_realm refuses to swap the realm out from under the suspended frame.
408
+ struct CallbackGuard {
409
+ State &st;
410
+ CallbackGuard(State &s) : st(s) { st.in_callback++; }
411
+ ~CallbackGuard() { st.in_callback--; }
412
+ };
413
+
414
+ // Forward declarations for the URL-registry module loader (defined below,
415
+ // alongside load_module_graph). Used by the registry-backed dynamic import path.
416
+ static v8::Local<v8::Module> registry_lookup(State& st, const std::string& url);
417
+ static void registry_rollback(State& st, const std::vector<std::string>& urls);
418
+ static bool graph_str(State& st, const std::string& s, v8::Local<v8::String>* out);
419
+ static bool graph_roundtrip(State& st, char marker, v8::Local<v8::Value> request,
420
+ v8::Local<v8::Value>* reply_out);
421
+ static bool walk_module_graph(State& st, const std::string& entry_url,
422
+ std::unordered_map<std::string, std::string>& edges,
423
+ std::vector<std::string>& new_urls,
424
+ std::unordered_map<std::string, bool>& rejected_by_url);
425
+ static v8::MaybeLocal<v8::Module> graph_resolve_callback(
426
+ v8::Local<v8::Context> context, v8::Local<v8::String> specifier,
427
+ v8::Local<v8::FixedArray> import_assertions, v8::Local<v8::Module> referrer);
428
+
429
+ // Opt-in (MINI_RACER_TRACE_MODULES env) stderr tracing of the dynamic-import and
430
+ // module-registry boundary — to correlate a real app's imports/registrations
431
+ // with leaked handles in a heap snapshot. Inert (one getenv) when unset.
432
+ static bool module_trace_on()
433
+ {
434
+ static const bool on = (getenv("MINI_RACER_TRACE_MODULES") != nullptr);
435
+ return on;
436
+ }
437
+
438
+ // V8 calls this for every JS `import(...)` expression. We rendezvous to
439
+ // Ruby (marker 'd'), expect a fully-instantiated MiniRacer::Module back,
440
+ // evaluate it if still pending, then resolve the returned Promise with
441
+ // its namespace. The contract requires the embedder to handle compile +
442
+ // instantiate + evaluate; Ruby's resolver is responsible for the first
443
+ // two, and we run Evaluate here so callers don't have to.
444
+ static v8::MaybeLocal<v8::Promise> host_import_module_dynamically_callback(
445
+ v8::Local<v8::Context> context,
446
+ v8::Local<v8::Data> /*host_defined_options*/,
447
+ v8::Local<v8::Value> resource_name,
448
+ v8::Local<v8::String> specifier,
449
+ v8::Local<v8::FixedArray> /*import_attributes*/)
450
+ {
451
+ auto isolate = context->GetIsolate();
452
+ State *pst = static_cast<State*>(isolate->GetData(0));
453
+ State& st = *pst;
454
+ // Suspended in a host->Ruby roundtrip for the whole resolver exchange.
455
+ CallbackGuard _guard(st);
456
+ v8::EscapableHandleScope handle_scope(isolate);
457
+
458
+ v8::Local<v8::Promise::Resolver> resolver;
459
+ if (!v8::Promise::Resolver::New(context).ToLocal(&resolver))
460
+ return v8::MaybeLocal<v8::Promise>();
461
+
462
+ // Single-exit helpers so every error path is one line.
463
+ auto escape = [&] { return handle_scope.Escape(resolver->GetPromise()); };
464
+ auto reject_with_value = [&](v8::Local<v8::Value> reason) {
465
+ (void)resolver->Reject(context, reason);
466
+ return escape();
467
+ };
468
+ // NewFromUtf8Literal returns a Local directly (no allocation Maybe),
469
+ // so error messages are safe under isolate OOM where NewFromUtf8 +
470
+ // ToLocalChecked would CHECK-fail.
471
+ auto reject_with_literal = [&](v8::Local<v8::String> msg) {
472
+ return reject_with_value(v8::Exception::Error(msg));
473
+ };
474
+
475
+ v8::Local<v8::Module> module;
476
+
477
+ if (module_trace_on()) {
478
+ v8::String::Utf8Value spec(st.isolate, specifier);
479
+ v8::String::Utf8Value ref(st.isolate, resource_name);
480
+ fprintf(stderr, "[mr.dynimport] specifier=%s referrer=%s path=%s\n",
481
+ *spec ? *spec : "?",
482
+ (resource_name->IsString() && *ref) ? *ref : "<none>",
483
+ st.uses_graph_loader ? "registry" : "legacy");
484
+ fflush(stderr);
485
+ }
486
+
487
+ if (st.uses_graph_loader) {
488
+ // Registry path: resolve the specifier to a URL via the persisted
489
+ // resolve callback, reuse the registry's Module if the URL was already
490
+ // loaded (the identity fix), else walk + instantiate its subgraph. A
491
+ // local TryCatch turns fetch/resolve/compile failures into a rejected
492
+ // import() promise instead of leaving an exception pending.
493
+ v8::TryCatch tc(st.isolate);
494
+ tc.SetVerbose(st.verbose_exceptions);
495
+ auto reject_pending = [&] {
496
+ v8::Local<v8::Value> reason = tc.HasCaught()
497
+ ? tc.Exception()
498
+ : v8::Local<v8::Value>::Cast(v8::Exception::Error(
499
+ v8::String::NewFromUtf8Literal(isolate, "dynamic import failed")));
500
+ // Clear so the captured Ruby error (if any) is reported via the
501
+ // promise, not re-raised in the enclosing eval frame.
502
+ st.ruby_exception.Reset();
503
+ return reject_with_value(reason);
504
+ };
505
+
506
+ std::string ref_url;
507
+ if (resource_name->IsString()) {
508
+ v8::String::Utf8Value ru(st.isolate, resource_name);
509
+ if (*ru) ref_url.assign(*ru, ru.length());
510
+ }
511
+ // Single-edge resolve batch: [[specifier, referrer_url]].
512
+ v8::Local<v8::Array> edges_arr, pr;
513
+ {
514
+ v8::Context::Scope cs(st.safe_context);
515
+ edges_arr = v8::Array::New(st.isolate, 1);
516
+ pr = v8::Array::New(st.isolate, 2);
517
+ }
518
+ v8::Local<v8::String> refs;
519
+ if (!graph_str(st, ref_url, &refs)) refs = v8::String::Empty(st.isolate);
520
+ pr->Set(context, 0, specifier).Check();
521
+ pr->Set(context, 1, refs).Check();
522
+ edges_arr->Set(context, 0, pr).Check();
523
+ v8::Local<v8::Value> resolved_v;
524
+ if (!graph_roundtrip(st, 'r', edges_arr, &resolved_v)) return reject_pending();
525
+ std::string url;
526
+ if (resolved_v->IsArray()) {
527
+ v8::Local<v8::Value> u;
528
+ if (resolved_v.As<v8::Array>()->Get(context, 0).ToLocal(&u) && u->IsString()) {
529
+ v8::String::Utf8Value uu(st.isolate, u);
530
+ if (*uu) url.assign(*uu, uu.length());
531
+ }
532
+ }
533
+ if (url.empty())
534
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
535
+ "dynamic import specifier could not be resolved to a URL"));
536
+
537
+ module = registry_lookup(st, url);
538
+ if (module_trace_on())
539
+ fprintf(stderr, "[mr.dynimport] resolved url=%s registry_%s\n",
540
+ url.c_str(), module.IsEmpty() ? "miss" : "hit"), fflush(stderr);
541
+ if (module.IsEmpty()) {
542
+ // Miss: load the not-yet-registered subgraph reachable from url,
543
+ // then instantiate (the shared tail below evaluates + resolves). On
544
+ // failure roll back what this walk registered so a retry recompiles
545
+ // cleanly. active_graph is save/restored (a dynamic import may itself
546
+ // fire inside an enclosing load's Evaluate).
547
+ GraphLoad graph;
548
+ std::vector<std::string> new_urls;
549
+ std::unordered_map<std::string, bool> rejected_by_url;
550
+ if (!walk_module_graph(st, url, graph.edges, new_urls, rejected_by_url)) {
551
+ registry_rollback(st, new_urls);
552
+ return reject_pending();
553
+ }
554
+ module = registry_lookup(st, url);
555
+ if (module.IsEmpty()) {
556
+ registry_rollback(st, new_urls);
557
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
558
+ "dynamic import target could not be fetched"));
559
+ }
560
+ GraphLoad *prev = st.active_graph;
561
+ st.active_graph = &graph;
562
+ v8::Maybe<bool> ok = module->InstantiateModule(st.context, graph_resolve_callback);
563
+ st.active_graph = prev;
564
+ if (ok.IsNothing() || !ok.FromJust()) {
565
+ registry_rollback(st, new_urls);
566
+ return reject_pending();
567
+ }
568
+ }
569
+ } else {
570
+ // Legacy path: the embedder's dynamic_import_resolver returns a
571
+ // fully-instantiated MiniRacer::Module (looked up by handle id).
572
+ v8::Local<v8::Array> request;
573
+ {
574
+ v8::Context::Scope context_scope(st.safe_context);
575
+ request = v8::Array::New(st.isolate, 2);
576
+ }
577
+ request->Set(context, 0, specifier).Check();
578
+ // resource_name is the referrer's filename for module-initiated imports,
579
+ // or the script filename for eval-initiated ones. May be Undefined for
580
+ // ad-hoc compilations; coerce to empty string in that case.
581
+ v8::Local<v8::Value> ref = resource_name->IsString()
582
+ ? resource_name
583
+ : v8::Local<v8::Value>::Cast(v8::String::Empty(st.isolate));
584
+ request->Set(context, 1, ref).Check();
585
+
586
+ {
587
+ Serialized serialized(st, request);
588
+ if (!serialized.data)
589
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
590
+ "could not serialize dynamic import request"));
591
+ uint8_t marker = 'd';
592
+ v8_reply(st.ruby_context, &marker, 1);
593
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
594
+ }
595
+
596
+ const uint8_t *p;
597
+ size_t n;
598
+ for (;;) {
599
+ v8_roundtrip(st.ruby_context, &p, &n);
600
+ if (*p == 'd') break;
601
+ if (*p == 'e') {
602
+ v8::Local<v8::String> message;
603
+ auto type = v8::NewStringType::kNormal;
604
+ if (!v8::String::NewFromOneByte(st.isolate, p+1, type, n-1).ToLocal(&message))
605
+ message = v8::String::NewFromUtf8Literal(st.isolate, "Ruby exception");
606
+ return reject_with_literal(message);
607
+ }
608
+ v8_dispatch(st.ruby_context);
609
+ }
610
+
611
+ v8::ValueDeserializer des(st.isolate, p+1, n-1);
612
+ des.ReadHeader(st.context).Check();
613
+ v8::Local<v8::Value> id_v;
614
+ int32_t id;
615
+ if (!des.ReadValue(st.context).ToLocal(&id_v) ||
616
+ !id_v->Int32Value(st.context).To(&id))
617
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
618
+ "dynamic import reply could not be decoded"));
619
+ auto it = st.modules.find(id);
620
+ if (it == st.modules.end())
621
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
622
+ "dynamic import resolver returned a handle unknown to this Context"));
623
+ module = v8::Local<v8::Module>::New(st.isolate, it->second->handle);
624
+ if (module_trace_on())
625
+ fprintf(stderr, "[mr.dynimport] legacy resolver returned id=%d url=%s\n",
626
+ id, it->second->filename.c_str()), fflush(stderr);
627
+ }
628
+
629
+ auto status = module->GetStatus();
630
+ // The Ruby resolver must hand back a Module that's at least instantiated;
631
+ // auto-instantiating here is impossible because there's no per-call
632
+ // resolver block to recurse through.
633
+ if (status < v8::Module::kInstantiated)
634
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
635
+ "dynamic import resolver returned an uninstantiated Module"));
636
+ if (status == v8::Module::kErrored)
637
+ return reject_with_value(module->GetException());
638
+ // kEvaluating means re-entry during cyclic dynamic import: V8 would
639
+ // give us a TDZ-laden namespace whose bindings throw ReferenceError.
640
+ // Spec-correct handling is to settle after the in-flight Evaluate
641
+ // completes, which requires TLA support; reject explicitly for now.
642
+ if (status == v8::Module::kEvaluating)
643
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
644
+ "dynamic import target is mid-evaluation (cyclic dynamic import)"));
645
+ if (status == v8::Module::kInstantiated) {
646
+ v8::TryCatch try_catch(st.isolate);
647
+ try_catch.SetVerbose(st.verbose_exceptions);
648
+ v8::Local<v8::Value> eval_result;
649
+ if (!module->Evaluate(context).ToLocal(&eval_result)) {
650
+ // Termination set the empty MaybeLocal without throwing — let
651
+ // the surrounding eval frame surface it instead of swallowing.
652
+ if (isolate->IsExecutionTerminating())
653
+ return v8::MaybeLocal<v8::Promise>();
654
+ return reject_with_value(try_catch.HasCaught()
655
+ ? try_catch.Exception()
656
+ : v8::Local<v8::Value>::Cast(v8::Undefined(isolate)));
657
+ }
658
+ // Drain so synchronously-scheduled microtasks (e.g. the dep body's
659
+ // own Promise.resolve().then) settle before we inspect promise state;
660
+ // matches v8_evaluate_module.
661
+ isolate->PerformMicrotaskCheckpoint();
662
+ if (eval_result->IsPromise()) {
663
+ auto promise = eval_result.As<v8::Promise>();
664
+ if (promise->State() == v8::Promise::kRejected)
665
+ return reject_with_value(promise->Result());
666
+ if (promise->State() == v8::Promise::kPending)
667
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
668
+ "dynamic import target has top-level await (not supported)"));
669
+ }
670
+ } else if (status == v8::Module::kEvaluated && module->IsGraphAsync()) {
671
+ // An already-evaluated module handed back by a registry hit (or the
672
+ // resolver). The kInstantiated branch above only confirmed settlement
673
+ // for the module it just evaluated; a previously-evaluated async module
674
+ // may still have a pending top-level await whose TDZ namespace would
675
+ // fatally abort the process when serialized. Refuse it.
676
+ return reject_with_literal(v8::String::NewFromUtf8Literal(isolate,
677
+ "dynamic import target uses top-level await (not supported)"));
678
+ }
679
+
680
+ (void)resolver->Resolve(context, module->GetModuleNamespace());
681
+ return escape();
682
+ }
683
+
684
+ // V8 calls this the first time JS reads `import.meta` for a module.
685
+ // Populate the `url` property with the filename passed to compile_module
686
+ // — needed for relative resolution helpers like `new URL(spec, import.meta.url)`.
687
+ // Graph/dynamic-import modules are registered in st.modules with filename=url,
688
+ // so module_filename resolves them too.
689
+ static void init_import_meta_object(v8::Local<v8::Context> context,
690
+ v8::Local<v8::Module> module,
691
+ v8::Local<v8::Object> meta)
692
+ {
693
+ auto isolate = context->GetIsolate();
694
+ // module_filename() materializes a Local<Module> per entry while scanning;
695
+ // give them a scope to reclaim instead of piling onto the caller's.
696
+ v8::HandleScope handle_scope(isolate);
697
+ State *pst = static_cast<State*>(isolate->GetData(0));
698
+ const std::string& filename = module_filename(*pst, module);
699
+ // Pass the byte length explicitly: filenames may contain embedded NULs,
700
+ // and NewFromUtf8 without a length argument truncates at the first NUL.
701
+ v8::Local<v8::String> name;
702
+ auto type = v8::NewStringType::kNormal;
703
+ if (!v8::String::NewFromUtf8(isolate, filename.data(), type,
704
+ static_cast<int>(filename.size())).ToLocal(&name))
705
+ return;
706
+ auto key = v8::String::NewFromUtf8Literal(isolate, "url");
707
+ // Do not Check() — a user-installed setter on Object.prototype.url
708
+ // would throw, and Check() would abort the process. Letting the
709
+ // Maybe<bool> drop surfaces the failure as a JS exception via the
710
+ // surrounding TryCatch frame.
711
+ (void)meta->Set(context, key, name);
712
+ }
713
+
714
+ // Native, rendezvous-free microtask checkpoint. When the embedder opts in via
715
+ // Context.new(host_namespace:), it is hung off the host namespace as
716
+ // <namespace>.drainMicrotasks(). Unlike Context#perform_microtask_checkpoint
717
+ // (dispatch tag 'M') this runs inline on the isolate thread and never
718
+ // round-trips through the Ruby<->V8 rendezvous, so JS can drain the queue
719
+ // mid-execution -- e.g. between synchronous dispatchEvent listeners -- for
720
+ // ~sub-microsecond cost. It mirrors v8_perform_microtask_checkpoint but
721
+ // without the reply, and deliberately leaves any termination active so the
722
+ // enclosing v8_call/v8_eval frame surfaces OOM (v8_gc_callback) or watchdog
723
+ // termination to Ruby.
724
+ void v8_drain_microtasks_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
725
+ {
726
+ auto ext = v8::External::Cast(*info.Data());
727
+ State& st = *static_cast<State*>(ext->Value());
728
+ // Do *not* take a v8::Locker here: in single-threaded mode V8 already holds
729
+ // the isolate on this (the Ruby) thread, so locking would deadlock.
730
+ //
731
+ // An uncaught exception thrown by a drained microtask is routed by V8 to
732
+ // its message/unhandled-rejection handlers, not propagated out of
733
+ // PerformCheckpoint, so this TryCatch normally catches nothing; it exists
734
+ // only to mirror v8_perform_microtask_checkpoint and honor verbose_exceptions.
735
+ // It must not (and does not) clear a pending termination.
736
+ v8::TryCatch try_catch(st.isolate);
737
+ try_catch.SetVerbose(st.verbose_exceptions);
738
+ v8::HandleScope handle_scope(st.isolate);
739
+ // PerformCheckpoint is a guarded no-op when the microtask depth is > 0, so
740
+ // it is safe to call mid-execution and never force-nests microtask runs.
741
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
742
+ info.GetReturnValue().SetUndefined();
743
+ }
744
+
745
+ // Builds a fresh user realm (plus the companion safe context), wires the
746
+ // safe-context marshalling helper against the new global, installs the opt-in
747
+ // host namespace, and re-binds any previously attached host functions. On
748
+ // success it atomically commits the new realm into st.persistent_* (releasing
749
+ // the previous one) and returns true. On any failure it touches none of the
750
+ // persistents — the previous realm stays intact — and returns false with an
751
+ // exception pending in the caller's TryCatch. Defined after v8_api_callback
752
+ // (which the re-bind needs). Assumes the isolate is entered by the caller.
753
+ static bool install_realm(State& st);
754
+
755
+ extern "C" State *v8_thread_init(Context *c, const uint8_t *snapshot_buf,
756
+ size_t snapshot_len, int64_t max_memory,
757
+ int verbose_exceptions,
758
+ const char *host_namespace)
759
+ {
760
+ State *pst = new State{};
761
+ State& st = *pst;
762
+ st.verbose_exceptions = (verbose_exceptions != 0);
763
+ st.ruby_context = c;
764
+ st.allocator.reset(v8::ArrayBuffer::Allocator::NewDefaultAllocator());
765
+ v8::StartupData blob{nullptr, 0};
766
+ v8::Isolate::CreateParams params;
767
+ params.array_buffer_allocator = st.allocator.get();
768
+ if (snapshot_len) {
769
+ blob.data = reinterpret_cast<const char*>(snapshot_buf);
770
+ blob.raw_size = snapshot_len;
771
+ params.snapshot_blob = &blob;
772
+ }
773
+ st.isolate = v8::Isolate::New(params);
774
+ // Slot 0 lets v8 callbacks that don't take embedder data (notably
775
+ // Module::InstantiateModule's ResolveCallback) recover State.
776
+ st.isolate->SetData(0, pst);
777
+ // Populate `import.meta.url` with the filename passed to compile_module.
778
+ st.isolate->SetHostInitializeImportMetaObjectCallback(init_import_meta_object);
779
+ // Dispatch JS `import(...)` expressions to Ruby via marker 'd'.
780
+ st.isolate->SetHostImportModuleDynamicallyCallback(
781
+ host_import_module_dynamically_callback);
782
+ st.max_memory = max_memory;
783
+ if (st.max_memory > 0)
784
+ st.isolate->AddGCEpilogueCallback(v8_gc_callback, pst);
785
+ {
786
+ v8::Locker locker(st.isolate);
787
+ v8::Isolate::Scope isolate_scope(st.isolate);
788
+ v8::HandleScope handle_scope(st.isolate);
789
+ st.host_namespace = host_namespace ? host_namespace : "";
790
+ // Build the user/safe realm and root it in st.persistent_*. The Local
791
+ // members are not kept alive past here; each request re-derives them
792
+ // from the persistents (see v8_threaded_enter / v8_single_threaded_enter).
793
+ // On a fresh isolate this only fails under catastrophic conditions (it
794
+ // used to be a hard CHECK), so treat boot failure as fatal.
795
+ if (!install_realm(st)) {
796
+ fprintf(stderr, "mini_racer: failed to initialize the V8 realm\n");
797
+ fflush(stderr);
798
+ abort();
799
+ }
800
+ if (single_threaded)
801
+ return pst; // intentionally returning early and keeping alive
802
+ v8_thread_main(c, pst);
803
+ }
804
+ delete pst;
805
+ return nullptr;
806
+ }
807
+
808
+ void v8_api_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
809
+ {
810
+ auto ext = v8::External::Cast(*info.Data());
811
+ Callback *cb = static_cast<Callback*>(ext->Value());
812
+ State& st = *cb->st;
813
+ // Suspended in a host->Ruby roundtrip for the whole callback exchange.
814
+ CallbackGuard _guard(st);
815
+ v8::Local<v8::Array> request;
816
+ {
817
+ v8::Context::Scope context_scope(st.safe_context);
818
+ request = v8::Array::New(st.isolate, 1 + info.Length());
819
+ }
820
+ for (int i = 0, n = info.Length(); i < n; i++) {
821
+ request->Set(st.context, i, sanitize(st, info[i])).Check();
822
+ }
823
+ auto id = v8::Int32::New(st.isolate, cb->id);
824
+ request->Set(st.context, info.Length(), id).Check(); // callback id
825
+ {
826
+ Serialized serialized(st, request);
827
+ if (!serialized.data) return; // exception pending
828
+ uint8_t marker = 'c'; // callback marker
829
+ v8_reply(st.ruby_context, &marker, 1);
830
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
831
+ }
832
+ const uint8_t *p;
833
+ size_t n;
834
+ for (;;) {
835
+ v8_roundtrip(st.ruby_context, &p, &n);
836
+ if (*p == 'c') // callback reply
837
+ break;
838
+ if (*p == 'e') { // ruby exception pending
839
+ v8::Local<v8::String> message;
840
+ auto type = v8::NewStringType::kNormal;
841
+ if (!v8::String::NewFromOneByte(st.isolate, p+1, type, n-1).ToLocal(&message)) {
842
+ message = v8::String::NewFromUtf8Literal(st.isolate, "Ruby exception");
843
+ }
844
+ auto exception = v8::Exception::Error(message);
845
+ st.ruby_exception.Reset(st.isolate, exception);
846
+ st.isolate->ThrowException(exception);
847
+ return;
848
+ }
849
+ v8_dispatch(st.ruby_context);
850
+ }
851
+ v8::ValueDeserializer des(st.isolate, p+1, n-1);
852
+ des.ReadHeader(st.context).Check();
853
+ v8::Local<v8::Value> result;
854
+ if (!des.ReadValue(st.context).ToLocal(&result)) return; // exception pending
855
+ info.GetReturnValue().Set(result);
856
+ }
857
+
858
+ // Binds cb's JS shim onto the current user global at cb->name, creating any
859
+ // intermediate objects for dotted "foo.bar.baz" paths. The Callback must
860
+ // already be registered in st.callbacks (it owns the External we hand to v8).
861
+ // Returns false with an exception pending in the caller's TryCatch on failure.
862
+ static bool bind_callback(State& st, Callback *cb)
863
+ {
864
+ auto ext = v8::External::New(st.isolate, cb);
865
+ v8::Local<v8::Function> function;
866
+ if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function))
867
+ return false;
868
+ auto type = v8::NewStringType::kNormal;
869
+ v8::Local<v8::Object> obj = st.context->Global();
870
+ v8::Local<v8::String> key;
871
+ for (const char *p = cb->name.c_str();;) {
872
+ size_t len = strcspn(p, ".");
873
+ if (!v8::String::NewFromUtf8(st.isolate, p, type, len).ToLocal(&key))
874
+ return false;
875
+ if (p[len] == '\0') break;
876
+ p += len + 1;
877
+ v8::Local<v8::Value> val;
878
+ if (!obj->Get(st.context, key).ToLocal(&val)) return false;
879
+ if (!val->IsObject() && !val->IsFunction()) {
880
+ val = v8::Object::New(st.isolate);
881
+ if (!obj->Set(st.context, key, val).FromMaybe(false)) return false;
882
+ }
883
+ obj = val.As<v8::Object>();
884
+ }
885
+ return obj->Set(st.context, key, function).FromMaybe(false);
886
+ }
887
+
888
+ // Re-derive the per-request realm Locals from the canonical persistents, and
889
+ // drop them again. Kept in one place so every entry point lists the same three
890
+ // members in the same order (v8_threaded_enter, v8_single_threaded_enter,
891
+ // v8_reset_realm).
892
+ static void restore_realm_locals(State& st)
893
+ {
894
+ st.safe_context_function = v8::Local<v8::Function>::New(st.isolate, st.persistent_safe_context_function);
895
+ st.safe_context = v8::Local<v8::Context>::New(st.isolate, st.persistent_safe_context);
896
+ st.context = v8::Local<v8::Context>::New(st.isolate, st.persistent_context);
897
+ }
898
+
899
+ static void clear_realm_locals(State& st)
900
+ {
901
+ st.context = v8::Local<v8::Context>();
902
+ st.safe_context = v8::Local<v8::Context>();
903
+ st.safe_context_function = v8::Local<v8::Function>();
904
+ }
905
+
906
+ // See the forward declaration above v8_thread_init for the contract. All
907
+ // build-time handles live in a private HandleScope. Nothing is committed until
908
+ // the realm is fully built (including every host-fn re-bind), so a failure
909
+ // midway — e.g. the isolate is terminating from a watchdog/OOM — leaves the
910
+ // previous realm untouched instead of CHECK-crashing the process.
911
+ static bool install_realm(State& st)
912
+ {
913
+ State *pst = &st;
914
+ v8::HandleScope handle_scope(st.isolate);
915
+ v8::Local<v8::Context> safe_context = v8::Context::New(st.isolate);
916
+ v8::Local<v8::Context> context = v8::Context::New(st.isolate);
917
+ if (safe_context.IsEmpty() || context.IsEmpty()) return false;
918
+ v8::Local<v8::Function> safe_context_function;
919
+ {
920
+ v8::Context::Scope safe_scope(safe_context);
921
+ auto source = v8::String::NewFromUtf8Literal(st.isolate, safe_context_script_source);
922
+ auto filename = v8::String::NewFromUtf8Literal(st.isolate, "safe_context_script.js");
923
+ v8::ScriptOrigin origin(filename);
924
+ v8::Local<v8::Script> script;
925
+ if (!v8::Script::Compile(safe_context, source, &origin).ToLocal(&script))
926
+ return false;
927
+ v8::Local<v8::Value> function_v;
928
+ if (!script->Run(safe_context).ToLocal(&function_v)) return false;
929
+ auto function = v8::Function::Cast(*function_v);
930
+ auto recv = v8::Undefined(st.isolate);
931
+ v8::Local<v8::Value> arg = context->Global();
932
+ // grant the safe context access to the user context's globalThis
933
+ safe_context->SetSecurityToken(context->GetSecurityToken());
934
+ v8::Local<v8::Value> ret;
935
+ bool ok = function->Call(safe_context, recv, 1, &arg).ToLocal(&ret);
936
+ // revoke access again now that the script did its one-time setup
937
+ safe_context->UseDefaultSecurityToken();
938
+ if (!ok) return false;
939
+ safe_context_function = v8::Local<v8::Function>::Cast(ret);
940
+ }
941
+ v8::Context::Scope context_scope(context);
942
+ // If the embedder opted in via Context.new(host_namespace:), install a
943
+ // single host-namespace object (in the spirit of Deno's `Deno` / Bun's
944
+ // `Bun`) under that global name and hang native helpers off it. The object
945
+ // closes over native code pointers so it cannot live in the (de)serialized
946
+ // snapshot; it is installed here on every fresh realm. The namespace is
947
+ // non-enumerable on globalThis so it stays out of Object.keys(globalThis)/
948
+ // for-in; its methods are ordinary enumerable properties so they remain
949
+ // discoverable on the object.
950
+ if (!st.host_namespace.empty()) {
951
+ v8::Local<v8::String> ns_name;
952
+ if (!v8::String::NewFromUtf8(st.isolate, st.host_namespace.c_str()).ToLocal(&ns_name))
953
+ return false;
954
+ auto ns = v8::Object::New(st.isolate);
955
+ auto data = v8::External::New(st.isolate, pst);
956
+ auto drain_name = v8::String::NewFromUtf8Literal(st.isolate, "drainMicrotasks");
957
+ v8::Local<v8::Function> drain;
958
+ if (!v8::Function::New(context, v8_drain_microtasks_callback, data).ToLocal(&drain))
959
+ return false;
960
+ if (!ns->Set(context, drain_name, drain).FromMaybe(false)) return false;
961
+ if (!context->Global()->DefineOwnProperty(context, ns_name, ns, v8::DontEnum).FromMaybe(false))
962
+ return false;
963
+ }
964
+ // Re-attach host functions onto the fresh global. Empty at boot; populated
965
+ // when install_realm runs from v8_reset_realm. bind_callback reads st.context,
966
+ // so point the members at the new realm for the duration of the loop, and
967
+ // make the whole rebuild atomic: if any function cannot be re-bound (e.g. a
968
+ // dotted path now collides with a snapshot global) the reset fails as a whole
969
+ // rather than silently dropping a host function.
970
+ st.context = context;
971
+ st.safe_context = safe_context;
972
+ st.safe_context_function = safe_context_function;
973
+ for (Callback *cb : st.callbacks) {
974
+ if (!bind_callback(st, cb)) {
975
+ clear_realm_locals(st);
976
+ return false;
977
+ }
978
+ }
979
+ // Commit: root the new realm and release the previous one (Reset replaces
980
+ // the old handle). The Local members dangle once handle_scope unwinds, so
981
+ // clear them; the next per-request restore re-derives them.
982
+ st.persistent_safe_context_function.Reset(st.isolate, safe_context_function);
983
+ st.persistent_safe_context.Reset(st.isolate, safe_context);
984
+ st.persistent_context.Reset(st.isolate, context);
985
+ clear_realm_locals(st);
986
+ return true;
987
+ }
988
+
989
+ // response is err or empty string
990
+ extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n)
991
+ {
992
+ State& st = *pst;
993
+ v8::TryCatch try_catch(st.isolate);
994
+ try_catch.SetVerbose(st.verbose_exceptions);
995
+ v8::HandleScope handle_scope(st.isolate);
996
+ v8::ValueDeserializer des(st.isolate, p, n);
997
+ des.ReadHeader(st.context).Check();
998
+ int cause = INTERNAL_ERROR;
999
+ {
1000
+ v8::Local<v8::Value> request_v;
1001
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
1002
+ v8::Local<v8::Object> request; // [name, id]
1003
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
1004
+ v8::Local<v8::Value> name_v;
1005
+ if (!request->Get(st.context, 0).ToLocal(&name_v)) goto fail;
1006
+ v8::Local<v8::Value> id_v;
1007
+ if (!request->Get(st.context, 1).ToLocal(&id_v)) goto fail;
1008
+ v8::Local<v8::String> name;
1009
+ if (!name_v->ToString(st.context).ToLocal(&name)) goto fail;
1010
+ int32_t id;
1011
+ if (!id_v->Int32Value(st.context).To(&id)) goto fail;
1012
+ // support foo.bar.baz paths
1013
+ v8::String::Utf8Value path(st.isolate, name);
1014
+ if (!*path) goto fail;
1015
+ // The Callback owns its name so reset_realm can re-bind it later. Only
1016
+ // register it in st.callbacks (which outlives realm swaps and drives the
1017
+ // re-bind) after the bind succeeds, so a failed attach is not resurrected
1018
+ // by a later reset_realm. Freed in ~State() once registered.
1019
+ Callback *cb = new Callback{pst, id, std::string(*path)};
1020
+ if (!bind_callback(st, cb)) {
1021
+ delete cb;
1022
+ goto fail;
1023
+ }
1024
+ st.callbacks.push_back(cb);
1025
+ }
1026
+ cause = NO_ERROR;
1027
+ fail:
1028
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1029
+ auto err = to_error(st, &try_catch, cause);
1030
+ reply_retry(st, err);
1031
+ }
1032
+
1033
+ // response is errback [result, err] array
1034
+ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
1035
+ {
1036
+ State& st = *pst;
1037
+ v8::TryCatch try_catch(st.isolate);
1038
+ try_catch.SetVerbose(st.verbose_exceptions);
1039
+ v8::HandleScope handle_scope(st.isolate);
1040
+ v8::ValueDeserializer des(st.isolate, p, n);
1041
+ std::vector<v8::Local<v8::Value>> args;
1042
+ des.ReadHeader(st.context).Check();
1043
+ v8::Local<v8::Value> result;
1044
+ int cause = INTERNAL_ERROR;
1045
+ {
1046
+ v8::Local<v8::Value> request_v;
1047
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
1048
+ v8::Local<v8::Object> request;
1049
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
1050
+ v8::Local<v8::Value> name_v;
1051
+ if (!request->Get(st.context, 0).ToLocal(&name_v)) goto fail;
1052
+ v8::Local<v8::String> name;
1053
+ if (!name_v->ToString(st.context).ToLocal(&name)) goto fail;
1054
+ cause = RUNTIME_ERROR;
1055
+ // support foo.bar.baz paths
1056
+ v8::String::Utf8Value path(st.isolate, name);
1057
+ if (!*path) goto fail;
1058
+ v8::Local<v8::Object> obj = st.context->Global();
1059
+ v8::Local<v8::String> key;
1060
+ for (const char *p = *path;;) {
1061
+ size_t n = strcspn(p, ".");
1062
+ auto type = v8::NewStringType::kNormal;
1063
+ if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail;
1064
+ if (p[n] == '\0') break;
1065
+ p += n + 1;
1066
+ v8::Local<v8::Value> val;
1067
+ if (!obj->Get(st.context, key).ToLocal(&val)) goto fail;
1068
+ if (!val->ToObject(st.context).ToLocal(&obj)) goto fail;
1069
+ }
1070
+ v8::Local<v8::Value> function_v;
1071
+ if (!obj->Get(st.context, key).ToLocal(&function_v)) goto fail;
1072
+ if (!function_v->IsFunction()) {
1073
+ // XXX it's technically possible for |function_v| to be a callable
1074
+ // object but those are effectively extinct; regexp objects used
1075
+ // to be callable but not anymore
1076
+ auto message = v8::String::NewFromUtf8Literal(st.isolate, "not a function");
1077
+ auto exception = v8::Exception::TypeError(message);
1078
+ st.isolate->ThrowException(exception);
1079
+ goto fail;
1080
+ }
1081
+ auto function = v8::Function::Cast(*function_v);
1082
+ assert(request->IsArray());
1083
+ int n = v8::Array::Cast(*request)->Length();
1084
+ for (int i = 1; i < n; i++) {
1085
+ v8::Local<v8::Value> val;
1086
+ if (!request->Get(st.context, i).ToLocal(&val)) goto fail;
1087
+ args.push_back(val);
1088
+ }
1089
+ auto maybe_result_v = function->Call(st.context, obj, args.size(), args.data());
1090
+ v8::Local<v8::Value> result_v;
1091
+ if (!maybe_result_v.ToLocal(&result_v)) goto fail;
1092
+ result = sanitize(st, result_v);
1093
+ }
1094
+ cause = NO_ERROR;
1095
+ fail:
1096
+ if (st.isolate->IsExecutionTerminating()) {
1097
+ st.isolate->CancelTerminateExecution();
1098
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1099
+ st.err_reason = NO_ERROR;
1100
+ }
1101
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1102
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1103
+ if (cause) result = v8::Undefined(st.isolate);
1104
+ auto err = to_error(st, &try_catch, cause);
1105
+ if (!reply(st, result, err)) {
1106
+ assert(try_catch.HasCaught());
1107
+ goto fail; // retry; can be termination exception
1108
+ }
1109
+ }
1110
+
1111
+ // response is errback [result, err] array
1112
+ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
1113
+ {
1114
+ State& st = *pst;
1115
+ v8::TryCatch try_catch(st.isolate);
1116
+ try_catch.SetVerbose(st.verbose_exceptions);
1117
+ v8::HandleScope handle_scope(st.isolate);
1118
+ v8::ValueDeserializer des(st.isolate, p, n);
1119
+ des.ReadHeader(st.context).Check();
1120
+ v8::Local<v8::Value> result;
1121
+ int cause = INTERNAL_ERROR;
1122
+ {
1123
+ v8::Local<v8::Value> request_v;
1124
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
1125
+ v8::Local<v8::Object> request; // [filename, source]
1126
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
1127
+ v8::Local<v8::Value> filename;
1128
+ if (!request->Get(st.context, 0).ToLocal(&filename)) goto fail;
1129
+ v8::Local<v8::Value> source_v;
1130
+ if (!request->Get(st.context, 1).ToLocal(&source_v)) goto fail;
1131
+ v8::Local<v8::String> source;
1132
+ if (!source_v->ToString(st.context).ToLocal(&source)) goto fail;
1133
+ v8::ScriptOrigin origin(filename);
1134
+ v8::Local<v8::Script> script;
1135
+ cause = PARSE_ERROR;
1136
+ if (!v8::Script::Compile(st.context, source, &origin).ToLocal(&script)) goto fail;
1137
+ v8::Local<v8::Value> result_v;
1138
+ cause = RUNTIME_ERROR;
1139
+ auto maybe_result_v = script->Run(st.context);
1140
+ if (!maybe_result_v.ToLocal(&result_v)) goto fail;
1141
+ result = sanitize(st, result_v);
1142
+ }
1143
+ cause = NO_ERROR;
1144
+ fail:
1145
+ if (st.isolate->IsExecutionTerminating()) {
1146
+ st.isolate->CancelTerminateExecution();
1147
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1148
+ st.err_reason = NO_ERROR;
1149
+ }
1150
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1151
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1152
+ if (cause) result = v8::Undefined(st.isolate);
1153
+ auto err = to_error(st, &try_catch, cause);
1154
+ if (!reply(st, result, err)) {
1155
+ assert(try_catch.HasCaught());
1156
+ goto fail; // retry; can be termination exception
1157
+ }
1158
+ }
1159
+
1160
+ // Pulls a Module handle id out of the request, looks it up in st.modules,
1161
+ // and stores the Local in *out. On miss, sets *cause = RUNTIME_ERROR and
1162
+ // throws a V8 exception; on deserialization failure, leaves *cause alone
1163
+ // and lets the standard fail-path handler take over. Returns false in
1164
+ // either failure case so callers can `goto fail` consistently.
1165
+ static bool module_from_request(State& st,
1166
+ v8::ValueDeserializer& des,
1167
+ v8::Local<v8::Module>* out,
1168
+ int* cause)
1169
+ {
1170
+ v8::Local<v8::Value> id_v;
1171
+ if (!des.ReadValue(st.context).ToLocal(&id_v)) return false;
1172
+ int32_t id;
1173
+ if (!id_v->Int32Value(st.context).To(&id)) return false;
1174
+ auto it = st.modules.find(id);
1175
+ if (it == st.modules.end()) {
1176
+ *cause = RUNTIME_ERROR;
1177
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate, "no such module handle");
1178
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1179
+ return false;
1180
+ }
1181
+ *out = v8::Local<v8::Module>::New(st.isolate, it->second->handle);
1182
+ return true;
1183
+ }
1184
+
1185
+ // request: [filename, source]
1186
+ // response: errback [handle_id:Int32, err]
1187
+ //
1188
+ // Parses |source| as an ES module. handle_id keys st.modules for later
1189
+ // v8_instantiate_module / v8_evaluate_module / v8_module_namespace /
1190
+ // v8_dispose_module. Imports declared by the module are not resolved here
1191
+ // — that happens in v8_instantiate_module via a Ruby-provided resolver.
1192
+ extern "C" void v8_compile_module(State *pst, const uint8_t *p, size_t n)
1193
+ {
1194
+ State& st = *pst;
1195
+ v8::TryCatch try_catch(st.isolate);
1196
+ try_catch.SetVerbose(st.verbose_exceptions);
1197
+ v8::HandleScope handle_scope(st.isolate);
1198
+ v8::ValueDeserializer des(st.isolate, p, n);
1199
+ des.ReadHeader(st.context).Check();
1200
+ v8::Local<v8::Array> result;
1201
+ int cause = INTERNAL_ERROR;
1202
+ {
1203
+ v8::Local<v8::Value> request_v;
1204
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
1205
+ v8::Local<v8::Object> request;
1206
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
1207
+ v8::Local<v8::Value> filename;
1208
+ if (!request->Get(st.context, 0).ToLocal(&filename)) goto fail;
1209
+ v8::Local<v8::Value> source_v;
1210
+ if (!request->Get(st.context, 1).ToLocal(&source_v)) goto fail;
1211
+ v8::Local<v8::Value> cached_v;
1212
+ if (!request->Get(st.context, 2).ToLocal(&cached_v)) goto fail;
1213
+ v8::Local<v8::Value> produce_v;
1214
+ if (!request->Get(st.context, 3).ToLocal(&produce_v)) goto fail;
1215
+ bool produce_cache = produce_v->BooleanValue(st.isolate);
1216
+ v8::Local<v8::String> source;
1217
+ if (!source_v->ToString(st.context).ToLocal(&source)) goto fail;
1218
+
1219
+ if (produce_cache && st.in_callback > 0) {
1220
+ cause = RUNTIME_ERROR;
1221
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1222
+ "produce_cache: true is unsafe inside a host-function callback "
1223
+ "(V8 CreateCodeCache corrupts parser state when re-entered); "
1224
+ "compile_module with produce_cache from the top level instead");
1225
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1226
+ goto fail;
1227
+ }
1228
+
1229
+ // BufferNotOwned: the Uint8Array bytes are pinned by the deserialized
1230
+ // request and stay valid through this v8_compile_module call; the
1231
+ // CachedData destructor (when source_obj falls out of scope) won't
1232
+ // free them.
1233
+ v8::ScriptCompiler::CachedData *cached_in = nullptr;
1234
+ if (cached_v->IsArrayBufferView()) {
1235
+ auto view = cached_v.As<v8::ArrayBufferView>();
1236
+ int len = static_cast<int>(view->ByteLength());
1237
+ if (len > 0) {
1238
+ auto store = view->Buffer()->GetBackingStore();
1239
+ auto bytes = static_cast<const uint8_t*>(store->Data()) + view->ByteOffset();
1240
+ cached_in = new v8::ScriptCompiler::CachedData(
1241
+ bytes, len, v8::ScriptCompiler::CachedData::BufferNotOwned);
1242
+ }
1243
+ }
1244
+
1245
+ // is_module must be true on the ScriptOrigin for V8 to accept
1246
+ // import/export syntax.
1247
+ v8::ScriptOrigin origin(filename,
1248
+ /*resource_line_offset=*/0,
1249
+ /*resource_column_offset=*/0,
1250
+ /*resource_is_shared_cross_origin=*/false,
1251
+ /*script_id=*/-1,
1252
+ /*source_map_url=*/v8::Local<v8::Value>(),
1253
+ /*resource_is_opaque=*/false,
1254
+ /*is_wasm=*/false,
1255
+ /*is_module=*/true);
1256
+ v8::ScriptCompiler::Source source_obj(source, origin, cached_in);
1257
+ auto options = cached_in ? v8::ScriptCompiler::kConsumeCodeCache
1258
+ : v8::ScriptCompiler::kNoCompileOptions;
1259
+ v8::Local<v8::Module> module;
1260
+ cause = PARSE_ERROR;
1261
+ if (!v8::ScriptCompiler::CompileModule(st.isolate, &source_obj, options)
1262
+ .ToLocal(&module)) goto fail;
1263
+ cause = INTERNAL_ERROR;
1264
+
1265
+ bool rejected = (cached_in && source_obj.GetCachedData()->rejected);
1266
+ v8::Local<v8::Value> cache_value = v8::Null(st.isolate);
1267
+ if (produce_cache && (!cached_in || rejected)) {
1268
+ std::unique_ptr<v8::ScriptCompiler::CachedData> blob(
1269
+ v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript()));
1270
+ if (blob && blob->length > 0) {
1271
+ auto backing = v8::ArrayBuffer::NewBackingStore(st.isolate, blob->length);
1272
+ memcpy(backing->Data(), blob->data, blob->length);
1273
+ cache_value = v8::ArrayBuffer::New(st.isolate, std::move(backing));
1274
+ }
1275
+ }
1276
+
1277
+ // Ids are monotonic and serialized as Int32 on the wire. Refuse to
1278
+ // wrap rather than invoke signed-overflow UB and risk aliasing a
1279
+ // still-live handle id (unreachable in practice — each live module
1280
+ // pins a Global handle, so the isolate OOMs long before 2^31).
1281
+ if (st.next_module_id == INT32_MAX) {
1282
+ cause = INTERNAL_ERROR;
1283
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1284
+ "module id space exhausted for this Context");
1285
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1286
+ goto fail;
1287
+ }
1288
+ int32_t id = ++st.next_module_id;
1289
+ auto entry = std::make_unique<ModuleEntry>();
1290
+ entry->handle.Reset(st.isolate, module);
1291
+ v8::String::Utf8Value fname(st.isolate, filename);
1292
+ if (*fname) entry->filename.assign(*fname, fname.length());
1293
+
1294
+ {
1295
+ v8::Context::Scope context_scope(st.safe_context);
1296
+ result = v8::Array::New(st.isolate, 3);
1297
+ }
1298
+ // Populate via the goto-fail idiom, not .Check(): compile_module runs
1299
+ // under the watchdog (tag 'O' -> v8_timedwait), so a timeout can leave
1300
+ // the isolate terminating here, making Set() return Nothing — .Check()
1301
+ // would abort the process. The fail path replies a proper
1302
+ // TERMINATED_ERROR instead. (mirrors v8_compile)
1303
+ if (!result->Set(st.context, 0, v8::Int32::New(st.isolate, id)).FromMaybe(false)) goto fail;
1304
+ if (!result->Set(st.context, 1, cache_value).FromMaybe(false)) goto fail;
1305
+ if (!result->Set(st.context, 2, v8::Boolean::New(st.isolate, rejected)).FromMaybe(false)) goto fail;
1306
+
1307
+ // Register the module only after the reply array is fully built. If a
1308
+ // Set above bailed (e.g. watchdog termination), the Ruby side gets an
1309
+ // error and never learns the id, so it could never erase the entry —
1310
+ // inserting earlier would orphan an undisposable handle until teardown.
1311
+ if (module_trace_on())
1312
+ fprintf(stderr, "[mr.register] url=%s id=%d (compile_module)\n",
1313
+ *fname ? *fname : "?", id), fflush(stderr);
1314
+ st.modules[id] = std::move(entry);
1315
+ }
1316
+ cause = NO_ERROR;
1317
+ fail:
1318
+ if (st.isolate->IsExecutionTerminating()) {
1319
+ st.isolate->CancelTerminateExecution();
1320
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1321
+ st.err_reason = NO_ERROR;
1322
+ }
1323
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1324
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1325
+ v8::Local<v8::Value> result_v = result.IsEmpty()
1326
+ ? static_cast<v8::Local<v8::Value>>(v8::Undefined(st.isolate))
1327
+ : static_cast<v8::Local<v8::Value>>(result);
1328
+ auto err = to_error(st, &try_catch, cause);
1329
+ if (!reply(st, result_v, err)) {
1330
+ assert(try_catch.HasCaught());
1331
+ goto fail;
1332
+ }
1333
+ }
1334
+
1335
+ // V8 invokes this for each static import while InstantiateModule walks
1336
+ // the import graph. It has no embedder slot, so State is recovered via
1337
+ // isolate->GetData(0). We round-trip to Ruby with marker 'm', expect an
1338
+ // int32 handle id back, and look it up in st.modules.
1339
+ //
1340
+ // The Ruby resolver block can re-enter the v8 thread via other dispatch
1341
+ // tags (e.g. compile_module the requested module on demand) — that flows
1342
+ // through v8_dispatch inside the wait loop, like v8_api_callback does.
1343
+ static v8::MaybeLocal<v8::Module> resolve_module_callback(
1344
+ v8::Local<v8::Context> context,
1345
+ v8::Local<v8::String> specifier,
1346
+ v8::Local<v8::FixedArray> /*import_assertions*/,
1347
+ v8::Local<v8::Module> referrer)
1348
+ {
1349
+ v8::Isolate *isolate = context->GetIsolate();
1350
+ State *pst = static_cast<State*>(isolate->GetData(0));
1351
+ State& st = *pst;
1352
+ // Suspended in a host->Ruby roundtrip for the whole resolve exchange.
1353
+ CallbackGuard _guard(st);
1354
+
1355
+ // InstantiateModule walks the entire import graph in one call; without
1356
+ // an explicit scope, every Local allocated per import (request, dispatch
1357
+ // buffers, transitive compile_module Locals) would pile into whatever
1358
+ // outer scope the embedder installed. EscapableHandleScope so the
1359
+ // returned Local<Module> survives the scope's destruction.
1360
+ v8::EscapableHandleScope handle_scope(isolate);
1361
+
1362
+ v8::Local<v8::Array> request;
1363
+ {
1364
+ v8::Context::Scope context_scope(st.safe_context);
1365
+ request = v8::Array::New(st.isolate, 2);
1366
+ }
1367
+ // Use the callback's |context| (matches what V8 walked the graph in)
1368
+ // rather than st.context. In mini_racer's single-context-per-isolate
1369
+ // model they're the same handle, but this is defensive in case that
1370
+ // ever changes.
1371
+ request->Set(context, 0, specifier).Check();
1372
+ // Referrer URL — the filename passed to compile_module's filename:
1373
+ // kwarg. Lets the Ruby resolver resolve relative specifiers
1374
+ // (`./foo`, `../bar`) against the importing module. Falls back to
1375
+ // an empty string if we can't materialize the v8::String (OOM).
1376
+ // Pass length explicitly so embedded NULs in the filename survive.
1377
+ v8::Local<v8::Value> referrer_name;
1378
+ v8::Local<v8::String> s;
1379
+ const std::string& ref_fn = module_filename(st, referrer);
1380
+ auto type = v8::NewStringType::kNormal;
1381
+ if (v8::String::NewFromUtf8(st.isolate, ref_fn.data(), type,
1382
+ static_cast<int>(ref_fn.size())).ToLocal(&s)) {
1383
+ referrer_name = s;
1384
+ } else {
1385
+ referrer_name = v8::String::Empty(st.isolate);
1386
+ }
1387
+ request->Set(context, 1, referrer_name).Check();
1388
+ {
1389
+ Serialized serialized(st, request);
1390
+ if (!serialized.data) return v8::MaybeLocal<v8::Module>();
1391
+ uint8_t marker = 'm';
1392
+ v8_reply(st.ruby_context, &marker, 1);
1393
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
1394
+ }
1395
+ const uint8_t *p;
1396
+ size_t n;
1397
+ for (;;) {
1398
+ v8_roundtrip(st.ruby_context, &p, &n);
1399
+ if (*p == 'm') break;
1400
+ if (*p == 'e') {
1401
+ v8::Local<v8::String> message;
1402
+ auto type = v8::NewStringType::kNormal;
1403
+ if (!v8::String::NewFromOneByte(st.isolate, p+1, type, n-1).ToLocal(&message)) {
1404
+ message = v8::String::NewFromUtf8Literal(st.isolate, "Ruby exception");
1405
+ }
1406
+ auto exception = v8::Exception::Error(message);
1407
+ st.ruby_exception.Reset(st.isolate, exception);
1408
+ st.isolate->ThrowException(exception);
1409
+ return v8::MaybeLocal<v8::Module>();
1410
+ }
1411
+ v8_dispatch(st.ruby_context);
1412
+ }
1413
+ v8::ValueDeserializer des(st.isolate, p+1, n-1);
1414
+ des.ReadHeader(st.context).Check();
1415
+ v8::Local<v8::Value> id_v;
1416
+ if (!des.ReadValue(st.context).ToLocal(&id_v)) return v8::MaybeLocal<v8::Module>();
1417
+ int32_t id;
1418
+ if (!id_v->Int32Value(st.context).To(&id)) return v8::MaybeLocal<v8::Module>();
1419
+ auto it = st.modules.find(id);
1420
+ if (it == st.modules.end()) {
1421
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1422
+ "module resolver returned a handle unknown to this Context");
1423
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1424
+ return v8::MaybeLocal<v8::Module>();
1425
+ }
1426
+ return handle_scope.Escape(v8::Local<v8::Module>::New(st.isolate,
1427
+ it->second->handle));
1428
+ }
1429
+
1430
+ // request: [handle_id:Int32]
1431
+ // response: errback [undefined, err]
1432
+ extern "C" void v8_instantiate_module(State *pst, const uint8_t *p, size_t n)
1433
+ {
1434
+ State& st = *pst;
1435
+ v8::TryCatch try_catch(st.isolate);
1436
+ try_catch.SetVerbose(st.verbose_exceptions);
1437
+ v8::HandleScope handle_scope(st.isolate);
1438
+ v8::ValueDeserializer des(st.isolate, p, n);
1439
+ des.ReadHeader(st.context).Check();
1440
+ v8::Local<v8::Value> result;
1441
+ int cause = INTERNAL_ERROR;
1442
+ {
1443
+ v8::Local<v8::Module> module;
1444
+ if (!module_from_request(st, des, &module, &cause)) goto fail;
1445
+ cause = RUNTIME_ERROR;
1446
+ v8::Maybe<bool> ok = module->InstantiateModule(st.context, resolve_module_callback);
1447
+ if (ok.IsNothing() || !ok.FromJust()) goto fail;
1448
+ result = v8::Undefined(st.isolate);
1449
+ }
1450
+ cause = NO_ERROR;
1451
+ fail:
1452
+ if (st.isolate->IsExecutionTerminating()) {
1453
+ st.isolate->CancelTerminateExecution();
1454
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1455
+ st.err_reason = NO_ERROR;
1456
+ }
1457
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1458
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1459
+ if (result.IsEmpty()) result = v8::Undefined(st.isolate);
1460
+ auto err = to_error(st, &try_catch, cause);
1461
+ if (!reply(st, result, err)) {
1462
+ assert(try_catch.HasCaught());
1463
+ goto fail;
1464
+ }
1465
+ }
1466
+
1467
+ // request: [handle_id:Int32]
1468
+ // response: errback [evaluation_result, err]
1469
+ //
1470
+ // V8 wraps every module evaluation in a Promise (settles synchronously for
1471
+ // non-TLA modules). We drain microtasks once, then unwrap. Pending after
1472
+ // the drain means the module has top-level await still in flight — not
1473
+ // supported in this round; the user gets a clear error.
1474
+ extern "C" void v8_evaluate_module(State *pst, const uint8_t *p, size_t n)
1475
+ {
1476
+ State& st = *pst;
1477
+ v8::TryCatch try_catch(st.isolate);
1478
+ try_catch.SetVerbose(st.verbose_exceptions);
1479
+ v8::HandleScope handle_scope(st.isolate);
1480
+ v8::ValueDeserializer des(st.isolate, p, n);
1481
+ des.ReadHeader(st.context).Check();
1482
+ v8::Local<v8::Value> result;
1483
+ int cause = INTERNAL_ERROR;
1484
+ {
1485
+ v8::Local<v8::Module> module;
1486
+ if (!module_from_request(st, des, &module, &cause)) goto fail;
1487
+ // V8 requires status >= kInstantiated for Evaluate; calling on an
1488
+ // uninstantiated module hits a CHECK and aborts the process.
1489
+ if (module->GetStatus() < v8::Module::kInstantiated) {
1490
+ cause = RUNTIME_ERROR;
1491
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1492
+ "module must be instantiated before it can be evaluated");
1493
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1494
+ goto fail;
1495
+ }
1496
+ cause = RUNTIME_ERROR;
1497
+ v8::Local<v8::Value> eval_result;
1498
+ if (!module->Evaluate(st.context).ToLocal(&eval_result)) goto fail;
1499
+ st.isolate->PerformMicrotaskCheckpoint();
1500
+ if (!eval_result->IsPromise()) {
1501
+ // older V8 / unusual configurations may return a plain value
1502
+ result = sanitize(st, eval_result);
1503
+ } else {
1504
+ auto promise = eval_result.As<v8::Promise>();
1505
+ if (promise->State() == v8::Promise::kFulfilled) {
1506
+ result = sanitize(st, promise->Result());
1507
+ } else if (promise->State() == v8::Promise::kRejected) {
1508
+ st.isolate->ThrowException(promise->Result());
1509
+ goto fail;
1510
+ } else {
1511
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1512
+ "module evaluation is still pending "
1513
+ "(top-level await is not yet supported)");
1514
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1515
+ goto fail;
1516
+ }
1517
+ }
1518
+ }
1519
+ cause = NO_ERROR;
1520
+ fail:
1521
+ if (st.isolate->IsExecutionTerminating()) {
1522
+ st.isolate->CancelTerminateExecution();
1523
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1524
+ st.err_reason = NO_ERROR;
1525
+ }
1526
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1527
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1528
+ if (result.IsEmpty()) result = v8::Undefined(st.isolate);
1529
+ auto err = to_error(st, &try_catch, cause);
1530
+ if (!reply(st, result, err)) {
1531
+ assert(try_catch.HasCaught());
1532
+ goto fail;
1533
+ }
1534
+ }
1535
+
1536
+ // ---- Context#load_module_graph: batched, mostly Ruby-free ESM graph load ----
1537
+
1538
+ static std::string edge_key(const std::string& referrer, const std::string& specifier)
1539
+ {
1540
+ std::string k;
1541
+ k.reserve(referrer.size() + 1 + specifier.size());
1542
+ k.append(referrer);
1543
+ k.push_back('\0');
1544
+ k.append(specifier);
1545
+ return k;
1546
+ }
1547
+
1548
+ // URL registry: one Module instance per URL for the realm's lifetime.
1549
+ static v8::Local<v8::Module> registry_lookup(State& st, const std::string& url)
1550
+ {
1551
+ auto it = st.module_id_by_url.find(url);
1552
+ if (it == st.module_id_by_url.end()) return v8::Local<v8::Module>();
1553
+ auto m = st.modules.find(it->second);
1554
+ if (m == st.modules.end()) return v8::Local<v8::Module>();
1555
+ return v8::Local<v8::Module>::New(st.isolate, m->second->handle);
1556
+ }
1557
+
1558
+ // Register a freshly compiled module under |url| (filename=url so module_filename
1559
+ // and import.meta.url resolve it). Caller must have confirmed a registry miss.
1560
+ static void registry_register(State& st, const std::string& url, v8::Local<v8::Module> module)
1561
+ {
1562
+ int32_t id = ++st.next_module_id;
1563
+ auto entry = std::make_unique<ModuleEntry>();
1564
+ entry->handle.Reset(st.isolate, module);
1565
+ entry->filename = url;
1566
+ st.modules[id] = std::move(entry);
1567
+ st.module_id_by_url[url] = id;
1568
+ if (module_trace_on())
1569
+ fprintf(stderr, "[mr.register] url=%s id=%d (registry)\n", url.c_str(), id), fflush(stderr);
1570
+ }
1571
+
1572
+ // Undo registry_register for |urls| — used to roll back a load that registered
1573
+ // modules but then failed to instantiate/evaluate, so those URLs aren't left in
1574
+ // the registry as half-loaded (uninstantiated) modules that future imports would
1575
+ // reuse and reject. Only the modules a failed load itself compiled are passed in;
1576
+ // reused modules from earlier successful loads are untouched.
1577
+ static void registry_rollback(State& st, const std::vector<std::string>& urls)
1578
+ {
1579
+ for (const std::string& url : urls) {
1580
+ auto it = st.module_id_by_url.find(url);
1581
+ if (it == st.module_id_by_url.end()) continue;
1582
+ st.modules.erase(it->second);
1583
+ st.module_id_by_url.erase(it);
1584
+ }
1585
+ }
1586
+
1587
+ // InstantiateModule resolver for a graph/dynamic load. Every edge was resolved
1588
+ // during the walk and its target is in the URL registry, so this is a pure map
1589
+ // lookup — no Ruby round-trip per import. Returns empty (throwing) for edges the
1590
+ // embedder left unresolved (resolve -> nil) or whose target failed to fetch
1591
+ // (404); InstantiateModule then fails on that import (ESM-correct for a missing
1592
+ // static dependency).
1593
+ static v8::MaybeLocal<v8::Module> graph_resolve_callback(
1594
+ v8::Local<v8::Context> /*context*/,
1595
+ v8::Local<v8::String> specifier,
1596
+ v8::Local<v8::FixedArray> /*import_assertions*/,
1597
+ v8::Local<v8::Module> referrer)
1598
+ {
1599
+ State& st = *static_cast<State*>(v8::Isolate::GetCurrent()->GetData(0));
1600
+ auto isolate = st.isolate;
1601
+ v8::String::Utf8Value spec(isolate, specifier);
1602
+ std::string spec_s(*spec ? *spec : "", *spec ? spec.length() : 0);
1603
+ if (st.active_graph) {
1604
+ const std::string& ref_url = module_filename(st, referrer);
1605
+ auto e = st.active_graph->edges.find(edge_key(ref_url, spec_s));
1606
+ if (e != st.active_graph->edges.end() && !e->second.empty()) {
1607
+ auto m = registry_lookup(st, e->second);
1608
+ if (!m.IsEmpty()) return m;
1609
+ }
1610
+ }
1611
+ std::string msg = "could not resolve import \"";
1612
+ msg.append(spec_s).append("\"");
1613
+ v8::Local<v8::String> m;
1614
+ if (!v8::String::NewFromUtf8(isolate, msg.c_str()).ToLocal(&m))
1615
+ m = v8::String::NewFromUtf8Literal(isolate, "could not resolve import");
1616
+ isolate->ThrowException(v8::Exception::Error(m));
1617
+ return v8::MaybeLocal<v8::Module>();
1618
+ }
1619
+
1620
+ // Send |request| to Ruby under |marker|, pump the rendezvous wait loop (the Ruby
1621
+ // handler may re-enter via v8_dispatch), and deserialize the reply (returned
1622
+ // under the same marker). Returns false with an exception pending if Ruby raised
1623
+ // ('e') or (de)serialization failed.
1624
+ static bool graph_roundtrip(State& st, char marker, v8::Local<v8::Value> request,
1625
+ v8::Local<v8::Value>* reply_out)
1626
+ {
1627
+ // Suspended in a host->Ruby roundtrip: the fetch/resolve block runs while
1628
+ // this v8_load_module_graph frame (and its st.active_graph) sits on the C++
1629
+ // stack. Mark in_callback so reset_realm refuses and compile() refuses
1630
+ // CreateCodeCache — re-entering either from here would corrupt the realm or
1631
+ // V8's parser.
1632
+ CallbackGuard _guard(st);
1633
+ {
1634
+ Serialized serialized(st, request);
1635
+ if (!serialized.data) return false; // exception pending
1636
+ uint8_t m = static_cast<uint8_t>(marker);
1637
+ v8_reply(st.ruby_context, &m, 1);
1638
+ v8_reply(st.ruby_context, serialized.data, serialized.size);
1639
+ }
1640
+ const uint8_t *p;
1641
+ size_t n;
1642
+ for (;;) {
1643
+ v8_roundtrip(st.ruby_context, &p, &n);
1644
+ if (*p == static_cast<uint8_t>(marker)) break;
1645
+ if (*p == 'e') {
1646
+ v8::Local<v8::String> message;
1647
+ auto type = v8::NewStringType::kNormal;
1648
+ if (!v8::String::NewFromOneByte(st.isolate, p+1, type, n-1).ToLocal(&message))
1649
+ message = v8::String::NewFromUtf8Literal(st.isolate, "Ruby exception");
1650
+ auto exception = v8::Exception::Error(message);
1651
+ st.ruby_exception.Reset(st.isolate, exception);
1652
+ st.isolate->ThrowException(exception);
1653
+ return false;
1654
+ }
1655
+ v8_dispatch(st.ruby_context);
1656
+ }
1657
+ v8::ValueDeserializer des(st.isolate, p+1, n-1);
1658
+ des.ReadHeader(st.context).Check();
1659
+ return des.ReadValue(st.context).ToLocal(reply_out);
1660
+ }
1661
+
1662
+ // Make a JS string from a std::string, preserving length (embedded NULs/UTF-8).
1663
+ static bool graph_str(State& st, const std::string& s, v8::Local<v8::String>* out)
1664
+ {
1665
+ return v8::String::NewFromUtf8(st.isolate, s.data(), v8::NewStringType::kNormal,
1666
+ static_cast<int>(s.size())).ToLocal(out);
1667
+ }
1668
+
1669
+ // Compile one module for the walk; sets *rejected if a supplied code cache was
1670
+ // rejected by V8. Mirrors v8_compile_module's compile path (is_module origin,
1671
+ // BufferNotOwned cached_data) but does not produce caches.
1672
+ static v8::MaybeLocal<v8::Module> graph_compile(State& st, v8::Local<v8::String> filename,
1673
+ v8::Local<v8::String> source,
1674
+ v8::Local<v8::Value> cached_v, bool* rejected)
1675
+ {
1676
+ *rejected = false;
1677
+ v8::ScriptCompiler::CachedData *cached_in = nullptr;
1678
+ if (cached_v->IsArrayBufferView()) {
1679
+ auto view = cached_v.As<v8::ArrayBufferView>();
1680
+ int len = static_cast<int>(view->ByteLength());
1681
+ if (len > 0) {
1682
+ auto store = view->Buffer()->GetBackingStore();
1683
+ auto bytes = static_cast<const uint8_t*>(store->Data()) + view->ByteOffset();
1684
+ cached_in = new v8::ScriptCompiler::CachedData(
1685
+ bytes, len, v8::ScriptCompiler::CachedData::BufferNotOwned);
1686
+ }
1687
+ }
1688
+ v8::ScriptOrigin origin(filename, 0, 0, false, -1, v8::Local<v8::Value>(),
1689
+ false, false, /*is_module=*/true);
1690
+ v8::ScriptCompiler::Source source_obj(source, origin, cached_in);
1691
+ auto options = cached_in ? v8::ScriptCompiler::kConsumeCodeCache
1692
+ : v8::ScriptCompiler::kNoCompileOptions;
1693
+ v8::Local<v8::Module> module;
1694
+ if (!v8::ScriptCompiler::CompileModule(st.isolate, &source_obj, options).ToLocal(&module))
1695
+ return v8::MaybeLocal<v8::Module>();
1696
+ if (cached_in) *rejected = source_obj.GetCachedData()->rejected;
1697
+ return module;
1698
+ }
1699
+
1700
+ // Loads the modules reachable from |entry_url| that aren't already in the URL
1701
+ // registry, using the Context's persisted resolve/fetch_batch callbacks. Walks
1702
+ // level by level: fetch a batch ('f'), compile + register each module with its
1703
+ // cached_data, collect its imports, batch-resolve them ('r'), recurse into the
1704
+ // not-yet-registered targets. Records every resolved edge into |edges| and
1705
+ // appends newly compiled URLs to |new_urls| (with per-URL cache_rejected).
1706
+ // Already-registered URLs are reused — never re-fetched or re-compiled, so
1707
+ // dynamic import() of a URL the entry graph already pulled in gets the same
1708
+ // Module instance. Returns false with an exception pending on error.
1709
+ static bool walk_module_graph(State& st, const std::string& entry_url,
1710
+ std::unordered_map<std::string, std::string>& edges,
1711
+ std::vector<std::string>& new_urls,
1712
+ std::unordered_map<std::string, bool>& rejected_by_url)
1713
+ {
1714
+ std::unordered_set<std::string> seen;
1715
+ std::vector<std::string> to_fetch;
1716
+ if (registry_lookup(st, entry_url).IsEmpty()) {
1717
+ to_fetch.push_back(entry_url);
1718
+ seen.insert(entry_url);
1719
+ }
1720
+ while (!to_fetch.empty()) {
1721
+ // ---- FETCH batch ----
1722
+ v8::Local<v8::Array> urls_arr;
1723
+ { v8::Context::Scope cs(st.safe_context); urls_arr = v8::Array::New(st.isolate, (int)to_fetch.size()); }
1724
+ for (size_t i = 0; i < to_fetch.size(); i++) {
1725
+ v8::Local<v8::String> u;
1726
+ if (!graph_str(st, to_fetch[i], &u)) return false;
1727
+ urls_arr->Set(st.context, (uint32_t)i, u).Check();
1728
+ }
1729
+ v8::Local<v8::Value> fetched_v;
1730
+ if (!graph_roundtrip(st, 'f', urls_arr, &fetched_v)) return false;
1731
+ if (!fetched_v->IsArray()) return false;
1732
+ auto fetched = fetched_v.As<v8::Array>();
1733
+
1734
+ // ---- compile + register this level, collect edges ----
1735
+ std::vector<std::pair<std::string, std::string>> level_edges; // (specifier, referrer_url)
1736
+ std::unordered_set<std::string> edge_seen; // dedup (ref,spec)
1737
+ for (size_t i = 0; i < to_fetch.size(); i++) {
1738
+ const std::string url = to_fetch[i];
1739
+ v8::Local<v8::Value> entry;
1740
+ if (!fetched->Get(st.context, (uint32_t)i).ToLocal(&entry)) return false;
1741
+ // nil / non-pair => fetch failed (404). Leave it uncompiled: any
1742
+ // static import of it then fails at instantiate (ESM-correct for a
1743
+ // missing dependency). Not added to new_urls (it was not loaded).
1744
+ if (!entry->IsArray()) continue;
1745
+ auto pair = entry.As<v8::Array>();
1746
+ v8::Local<v8::Value> source_v, cached_v;
1747
+ if (!pair->Get(st.context, 0).ToLocal(&source_v)) return false;
1748
+ if (!pair->Get(st.context, 1).ToLocal(&cached_v)) return false;
1749
+ v8::Local<v8::String> source, fname;
1750
+ if (!source_v->ToString(st.context).ToLocal(&source)) return false;
1751
+ if (!graph_str(st, url, &fname)) return false;
1752
+ bool rej = false;
1753
+ v8::Local<v8::Module> module;
1754
+ if (!graph_compile(st, fname, source, cached_v, &rej).ToLocal(&module)) return false;
1755
+ registry_register(st, url, module);
1756
+ rejected_by_url[url] = rej;
1757
+ new_urls.push_back(url);
1758
+ // Collect imports for the resolve batch (deduped: `import a from "x";
1759
+ // import b from "x"` is one edge).
1760
+ auto requests = module->GetModuleRequests();
1761
+ for (int r = 0; r < requests->Length(); r++) {
1762
+ auto mr = requests->Get(st.context, r).As<v8::ModuleRequest>();
1763
+ v8::String::Utf8Value spec(st.isolate, mr->GetSpecifier());
1764
+ if (!*spec) continue;
1765
+ std::string spec_s(*spec, spec.length());
1766
+ if (edge_seen.insert(edge_key(url, spec_s)).second)
1767
+ level_edges.emplace_back(spec_s, url);
1768
+ }
1769
+ }
1770
+
1771
+ // ---- RESOLVE batch ----
1772
+ to_fetch.clear();
1773
+ if (level_edges.empty()) continue;
1774
+ v8::Local<v8::Array> edges_arr;
1775
+ { v8::Context::Scope cs(st.safe_context); edges_arr = v8::Array::New(st.isolate, (int)level_edges.size()); }
1776
+ for (size_t i = 0; i < level_edges.size(); i++) {
1777
+ v8::Local<v8::Array> pr;
1778
+ { v8::Context::Scope cs(st.safe_context); pr = v8::Array::New(st.isolate, 2); }
1779
+ v8::Local<v8::String> spec, ref;
1780
+ if (!graph_str(st, level_edges[i].first, &spec)) return false;
1781
+ if (!graph_str(st, level_edges[i].second, &ref)) return false;
1782
+ pr->Set(st.context, 0, spec).Check();
1783
+ pr->Set(st.context, 1, ref).Check();
1784
+ edges_arr->Set(st.context, (uint32_t)i, pr).Check();
1785
+ }
1786
+ v8::Local<v8::Value> resolved_v;
1787
+ if (!graph_roundtrip(st, 'r', edges_arr, &resolved_v)) return false;
1788
+ if (!resolved_v->IsArray()) return false;
1789
+ auto resolved = resolved_v.As<v8::Array>();
1790
+ for (size_t i = 0; i < level_edges.size(); i++) {
1791
+ v8::Local<v8::Value> u;
1792
+ if (!resolved->Get(st.context, (uint32_t)i).ToLocal(&u)) return false;
1793
+ std::string turl;
1794
+ if (u->IsString()) {
1795
+ v8::String::Utf8Value uu(st.isolate, u);
1796
+ if (*uu) turl.assign(*uu, uu.length());
1797
+ }
1798
+ edges[edge_key(level_edges[i].second, level_edges[i].first)] = turl;
1799
+ if (!turl.empty() && registry_lookup(st, turl).IsEmpty() && seen.find(turl) == seen.end()) {
1800
+ seen.insert(turl);
1801
+ to_fetch.push_back(turl);
1802
+ }
1803
+ }
1804
+ }
1805
+ return true;
1806
+ }
1807
+
1808
+ // Instantiate (native resolver) + Evaluate |entry_module| under |graph|'s edges,
1809
+ // draining microtasks and rejecting on top-level await. Writes the evaluation
1810
+ // value to *out. Returns false with an exception pending on failure. active_graph
1811
+ // is save/restored (not blindly nulled) so a nested dynamic import() fired during
1812
+ // this Evaluate doesn't clobber an enclosing load's graph.
1813
+ static bool instantiate_and_evaluate(State& st, GraphLoad& graph,
1814
+ v8::Local<v8::Module> entry_module,
1815
+ v8::Local<v8::Value>* out)
1816
+ {
1817
+ GraphLoad *prev = st.active_graph;
1818
+ st.active_graph = &graph;
1819
+ v8::Maybe<bool> ok = entry_module->InstantiateModule(st.context, graph_resolve_callback);
1820
+ if (ok.IsNothing() || !ok.FromJust()) { st.active_graph = prev; return false; }
1821
+ v8::Local<v8::Value> eval_result;
1822
+ if (!entry_module->Evaluate(st.context).ToLocal(&eval_result)) { st.active_graph = prev; return false; }
1823
+ st.isolate->PerformMicrotaskCheckpoint();
1824
+ st.active_graph = prev;
1825
+ if (!eval_result->IsPromise()) { *out = sanitize(st, eval_result); return true; }
1826
+ auto promise = eval_result.As<v8::Promise>();
1827
+ if (promise->State() == v8::Promise::kFulfilled) { *out = sanitize(st, promise->Result()); return true; }
1828
+ if (promise->State() == v8::Promise::kRejected) { st.isolate->ThrowException(promise->Result()); return false; }
1829
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1830
+ "module evaluation is still pending (top-level await is not yet supported)");
1831
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1832
+ return false;
1833
+ }
1834
+
1835
+ // request: [entry_url:String]
1836
+ // response: errback [[value, [[url, cache_rejected:Bool], ...]], err]
1837
+ //
1838
+ // Walks the static import graph on the V8 thread (see walk_module_graph), then
1839
+ // instantiates with a native resolver and evaluates. Collapses the per-module
1840
+ // compile_module/instantiate round-trips (~2*N) down to ~2 per graph level, and
1841
+ // registers every module in the URL registry so later dynamic import() reuses
1842
+ // the same instances. `modules` lists only modules newly compiled by this call.
1843
+ extern "C" void v8_load_module_graph(State *pst, const uint8_t *p, size_t n)
1844
+ {
1845
+ State& st = *pst;
1846
+ // Route dynamic import() through the registry for the rest of this load
1847
+ // (the entry's own top-level import() must reuse it) and, on success, for
1848
+ // the Context's life. Reverted below if this load fails, so a failed first
1849
+ // load doesn't permanently disable the legacy dynamic_import_resolver.
1850
+ bool prev_uses_graph_loader = st.uses_graph_loader;
1851
+ st.uses_graph_loader = true;
1852
+ v8::TryCatch try_catch(st.isolate);
1853
+ try_catch.SetVerbose(st.verbose_exceptions);
1854
+ v8::HandleScope handle_scope(st.isolate);
1855
+ v8::ValueDeserializer des(st.isolate, p, n);
1856
+ des.ReadHeader(st.context).Check();
1857
+ v8::Local<v8::Value> result;
1858
+ int cause = INTERNAL_ERROR;
1859
+ GraphLoad graph;
1860
+ std::vector<std::string> new_urls;
1861
+ std::unordered_map<std::string, bool> rejected_by_url;
1862
+ {
1863
+ v8::Local<v8::Value> req_v;
1864
+ if (!des.ReadValue(st.context).ToLocal(&req_v)) goto fail;
1865
+ v8::Local<v8::Object> req;
1866
+ if (!req_v->ToObject(st.context).ToLocal(&req)) goto fail;
1867
+ v8::Local<v8::Value> entry_v;
1868
+ if (!req->Get(st.context, 0).ToLocal(&entry_v)) goto fail;
1869
+ v8::String::Utf8Value entry_u(st.isolate, entry_v);
1870
+ if (!*entry_u) goto fail;
1871
+ std::string entry_url(*entry_u, entry_u.length());
1872
+
1873
+ cause = RUNTIME_ERROR;
1874
+ if (!walk_module_graph(st, entry_url, graph.edges, new_urls, rejected_by_url)) goto fail;
1875
+
1876
+ v8::Local<v8::Module> entry_module = registry_lookup(st, entry_url);
1877
+ if (entry_module.IsEmpty()) {
1878
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1879
+ "load_module_graph: entry module could not be fetched");
1880
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1881
+ goto fail;
1882
+ }
1883
+
1884
+ v8::Local<v8::Value> value;
1885
+ if (!instantiate_and_evaluate(st, graph, entry_module, &value)) goto fail;
1886
+
1887
+ // ---- build [value, [[url, rejected], ...]] for newly compiled modules ----
1888
+ cause = INTERNAL_ERROR;
1889
+ v8::Local<v8::Array> mods;
1890
+ { v8::Context::Scope cs(st.safe_context); mods = v8::Array::New(st.isolate, (int)new_urls.size()); }
1891
+ for (size_t i = 0; i < new_urls.size(); i++) {
1892
+ v8::Local<v8::Array> row;
1893
+ { v8::Context::Scope cs(st.safe_context); row = v8::Array::New(st.isolate, 2); }
1894
+ v8::Local<v8::String> u;
1895
+ if (!graph_str(st, new_urls[i], &u)) goto fail;
1896
+ row->Set(st.context, 0, u).Check();
1897
+ row->Set(st.context, 1, v8::Boolean::New(st.isolate, rejected_by_url[new_urls[i]])).Check();
1898
+ mods->Set(st.context, (uint32_t)i, row).Check();
1899
+ }
1900
+ v8::Local<v8::Array> out;
1901
+ { v8::Context::Scope cs(st.safe_context); out = v8::Array::New(st.isolate, 2); }
1902
+ out->Set(st.context, 0, value).Check();
1903
+ out->Set(st.context, 1, mods).Check();
1904
+ result = out;
1905
+ }
1906
+ cause = NO_ERROR;
1907
+ fail:
1908
+ st.active_graph = nullptr;
1909
+ // On failure (every goto-fail sets a nonzero cause; success and the
1910
+ // reply-retry path leave it NO_ERROR), undo this load's registrations so it
1911
+ // leaves no half-loaded modules behind, and revert the loader latch so a
1912
+ // failed load doesn't disable the legacy resolver.
1913
+ if (cause != NO_ERROR) {
1914
+ registry_rollback(st, new_urls);
1915
+ st.uses_graph_loader = prev_uses_graph_loader;
1916
+ }
1917
+ if (st.isolate->IsExecutionTerminating()) {
1918
+ st.isolate->CancelTerminateExecution();
1919
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1920
+ st.err_reason = NO_ERROR;
1921
+ }
1922
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1923
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1924
+ v8::Local<v8::Value> result_v = result.IsEmpty()
1925
+ ? static_cast<v8::Local<v8::Value>>(v8::Undefined(st.isolate))
1926
+ : result;
1927
+ auto err = to_error(st, &try_catch, cause);
1928
+ if (!reply(st, result_v, err)) {
1929
+ assert(try_catch.HasCaught());
1930
+ goto fail;
1931
+ }
1932
+ }
1933
+
1934
+ // request: [handle_id:Int32]
1935
+ // response: errback [namespace_value, err]
1936
+ //
1937
+ // GetModuleNamespace requires the module to be at least instantiated
1938
+ // (V8 will fatal otherwise). Plain-data exports come back as Hash
1939
+ // entries via the regular sanitize path; function exports are filtered
1940
+ // out by the safe-context wrapper, same as other Object returns.
1941
+ extern "C" void v8_module_namespace(State *pst, const uint8_t *p, size_t n)
1942
+ {
1943
+ State& st = *pst;
1944
+ v8::TryCatch try_catch(st.isolate);
1945
+ try_catch.SetVerbose(st.verbose_exceptions);
1946
+ v8::HandleScope handle_scope(st.isolate);
1947
+ v8::ValueDeserializer des(st.isolate, p, n);
1948
+ des.ReadHeader(st.context).Check();
1949
+ v8::Local<v8::Value> result;
1950
+ int cause = INTERNAL_ERROR;
1951
+ {
1952
+ v8::Local<v8::Module> module;
1953
+ if (!module_from_request(st, des, &module, &cause)) goto fail;
1954
+ // Only a fully evaluated, non-async module has a safe-to-read namespace.
1955
+ // Reading bindings still in the temporal dead zone (not yet evaluated,
1956
+ // or a top-level-await module whose promise never settled) makes the
1957
+ // serializer hit a throwing accessor on every property, which V8 turns
1958
+ // into an unrecoverable FatalProcessOutOfMemory (process abort), not a
1959
+ // catchable exception. Require kEvaluated AND !IsGraphAsync; surface an
1960
+ // errored module's own exception; reject every other state.
1961
+ auto status = module->GetStatus();
1962
+ if (status == v8::Module::kErrored) {
1963
+ cause = RUNTIME_ERROR;
1964
+ st.isolate->ThrowException(module->GetException());
1965
+ goto fail;
1966
+ }
1967
+ if (status != v8::Module::kEvaluated || module->IsGraphAsync()) {
1968
+ cause = RUNTIME_ERROR;
1969
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
1970
+ "module must be evaluated (and not use top-level await) before "
1971
+ "its namespace can be read");
1972
+ st.isolate->ThrowException(v8::Exception::Error(msg));
1973
+ goto fail;
1974
+ }
1975
+ cause = RUNTIME_ERROR;
1976
+ result = sanitize(st, module->GetModuleNamespace());
1977
+ }
1978
+ cause = NO_ERROR;
1979
+ fail:
1980
+ if (st.isolate->IsExecutionTerminating()) {
1981
+ st.isolate->CancelTerminateExecution();
1982
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
1983
+ st.err_reason = NO_ERROR;
1984
+ }
1985
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
1986
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
1987
+ if (result.IsEmpty()) result = v8::Undefined(st.isolate);
1988
+ auto err = to_error(st, &try_catch, cause);
1989
+ if (!reply(st, result, err)) {
1990
+ assert(try_catch.HasCaught());
1991
+ goto fail;
1992
+ }
1993
+ }
1994
+
1995
+ // response: errback [status_name:String, err]
1996
+ // status_name is one of the v8::Module::Status enum names in lowercase
1997
+ // ("uninstantiated", "instantiating", "instantiated", "evaluating",
1998
+ // "evaluated", "errored"). Ruby side converts to a symbol.
1999
+ extern "C" void v8_module_status(State *pst, const uint8_t *p, size_t n)
2000
+ {
2001
+ State& st = *pst;
2002
+ v8::TryCatch try_catch(st.isolate);
2003
+ try_catch.SetVerbose(st.verbose_exceptions);
2004
+ v8::HandleScope handle_scope(st.isolate);
2005
+ v8::ValueDeserializer des(st.isolate, p, n);
2006
+ des.ReadHeader(st.context).Check();
2007
+ v8::Local<v8::Value> result;
2008
+ int cause = INTERNAL_ERROR;
2009
+ {
2010
+ v8::Local<v8::Module> module;
2011
+ if (!module_from_request(st, des, &module, &cause)) goto fail;
2012
+ const char *name;
2013
+ switch (module->GetStatus()) {
2014
+ case v8::Module::kUninstantiated: name = "uninstantiated"; break;
2015
+ case v8::Module::kInstantiating: name = "instantiating"; break;
2016
+ case v8::Module::kInstantiated: name = "instantiated"; break;
2017
+ case v8::Module::kEvaluating: name = "evaluating"; break;
2018
+ case v8::Module::kEvaluated: name = "evaluated"; break;
2019
+ case v8::Module::kErrored: name = "errored"; break;
2020
+ default: name = "unknown"; break;
2021
+ }
2022
+ v8::Local<v8::String> s;
2023
+ if (!v8::String::NewFromUtf8(st.isolate, name).ToLocal(&s)) goto fail;
2024
+ result = s;
2025
+ }
2026
+ cause = NO_ERROR;
2027
+ fail:
2028
+ if (st.isolate->IsExecutionTerminating()) {
2029
+ st.isolate->CancelTerminateExecution();
2030
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2031
+ st.err_reason = NO_ERROR;
2032
+ }
2033
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
2034
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
2035
+ if (result.IsEmpty()) result = v8::Undefined(st.isolate);
2036
+ auto err = to_error(st, &try_catch, cause);
2037
+ if (!reply(st, result, err)) {
2038
+ assert(try_catch.HasCaught());
2039
+ goto fail;
2040
+ }
2041
+ }
2042
+
2043
+ // Unknown ids are silently ignored — Ruby-side Module#dispose is idempotent.
2044
+ extern "C" void v8_dispose_module(State *pst, const uint8_t *p, size_t n)
2045
+ {
2046
+ State& st = *pst;
2047
+ v8::HandleScope handle_scope(st.isolate);
2048
+ v8::ValueDeserializer des(st.isolate, p, n);
2049
+ des.ReadHeader(st.context).Check();
2050
+ v8::Local<v8::Value> id_v;
2051
+ if (des.ReadValue(st.context).ToLocal(&id_v)) {
2052
+ int32_t id;
2053
+ if (id_v->Int32Value(st.context).To(&id))
2054
+ st.modules.erase(id);
2055
+ }
2056
+ reply_retry(st, v8::String::Empty(st.isolate));
2057
+ }
2058
+
2059
+ // request: [filename, source, cached_data|null, produce_cache:Bool]
2060
+ // response: errback [[handle_id:Int32, cached_data:ArrayBuffer|null, rejected:Bool], err]
2061
+ //
2062
+ // CreateCodeCache walks live isolate state in a way that corrupts the parser
2063
+ // when called from inside a v8_api_callback frame (re-entrant compile from
2064
+ // host fn). Callers must opt in via produce_cache and only do so from the
2065
+ // top level; we raise from re-entrant context rather than silently skipping
2066
+ // so misuse is caught immediately.
2067
+ extern "C" void v8_compile(State *pst, const uint8_t *p, size_t n)
2068
+ {
2069
+ State& st = *pst;
2070
+ v8::TryCatch try_catch(st.isolate);
2071
+ try_catch.SetVerbose(st.verbose_exceptions);
2072
+ v8::HandleScope handle_scope(st.isolate);
2073
+ v8::ValueDeserializer des(st.isolate, p, n);
2074
+ des.ReadHeader(st.context).Check();
2075
+ v8::Local<v8::Array> result;
2076
+ int cause = INTERNAL_ERROR;
2077
+ {
2078
+ v8::Local<v8::Value> request_v;
2079
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
2080
+ v8::Local<v8::Object> request;
2081
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
2082
+ v8::Local<v8::Value> filename;
2083
+ if (!request->Get(st.context, 0).ToLocal(&filename)) goto fail;
2084
+ v8::Local<v8::Value> source_v;
2085
+ if (!request->Get(st.context, 1).ToLocal(&source_v)) goto fail;
2086
+ v8::Local<v8::Value> cached_v;
2087
+ if (!request->Get(st.context, 2).ToLocal(&cached_v)) goto fail;
2088
+ v8::Local<v8::Value> produce_v;
2089
+ if (!request->Get(st.context, 3).ToLocal(&produce_v)) goto fail;
2090
+ bool produce_cache = produce_v->BooleanValue(st.isolate);
2091
+ v8::Local<v8::String> source;
2092
+ if (!source_v->ToString(st.context).ToLocal(&source)) goto fail;
2093
+
2094
+ if (produce_cache && st.in_callback > 0) {
2095
+ cause = RUNTIME_ERROR;
2096
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
2097
+ "produce_cache: true is unsafe inside a host-function callback "
2098
+ "(V8 CreateCodeCache corrupts parser state when re-entered); "
2099
+ "compile with produce_cache from the top level instead");
2100
+ st.isolate->ThrowException(v8::Exception::Error(msg));
2101
+ goto fail;
2102
+ }
2103
+
2104
+ // ser_uint8array on the Ruby side wraps the bytes in an ArrayBuffer +
2105
+ // Uint8Array view. The view's backing bytes are valid for the whole
2106
+ // v8_compile call, so BufferNotOwned avoids a copy — the CachedData
2107
+ // destructor (run when source_obj goes out of scope) leaves them alone.
2108
+ v8::ScriptCompiler::CachedData *cached_in = nullptr;
2109
+ if (cached_v->IsArrayBufferView()) {
2110
+ auto view = cached_v.As<v8::ArrayBufferView>();
2111
+ int len = static_cast<int>(view->ByteLength());
2112
+ if (len > 0) {
2113
+ auto store = view->Buffer()->GetBackingStore();
2114
+ auto bytes = static_cast<const uint8_t*>(store->Data()) + view->ByteOffset();
2115
+ cached_in = new v8::ScriptCompiler::CachedData(
2116
+ bytes, len, v8::ScriptCompiler::CachedData::BufferNotOwned);
2117
+ }
2118
+ }
2119
+
2120
+ v8::ScriptOrigin origin(filename);
2121
+ v8::ScriptCompiler::Source source_obj(source, origin, cached_in);
2122
+ auto options = cached_in ? v8::ScriptCompiler::kConsumeCodeCache
2123
+ : v8::ScriptCompiler::kNoCompileOptions;
2124
+ v8::Local<v8::Script> script;
2125
+ cause = PARSE_ERROR;
2126
+ if (!v8::ScriptCompiler::Compile(st.context, &source_obj, options)
2127
+ .ToLocal(&script)) goto fail;
2128
+ cause = INTERNAL_ERROR;
2129
+
2130
+ bool rejected = (cached_in && source_obj.GetCachedData()->rejected);
2131
+ v8::Local<v8::Value> cache_value = v8::Null(st.isolate);
2132
+ if (produce_cache && (!cached_in || rejected)) {
2133
+ std::unique_ptr<v8::ScriptCompiler::CachedData> blob(
2134
+ v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
2135
+ if (blob && blob->length > 0) {
2136
+ auto backing = v8::ArrayBuffer::NewBackingStore(st.isolate, blob->length);
2137
+ memcpy(backing->Data(), blob->data, blob->length);
2138
+ cache_value = v8::ArrayBuffer::New(st.isolate, std::move(backing));
2139
+ }
2140
+ }
2141
+
2142
+ // Ids are monotonic and serialized as Int32 on the wire. Refuse to
2143
+ // wrap at INT32_MAX rather than invoke signed-overflow UB / risk
2144
+ // aliasing a still-live id (unreachable in practice — each undisposed
2145
+ // script pins a handle, so the isolate OOMs long before 2^31).
2146
+ if (st.next_script_id == INT32_MAX) {
2147
+ cause = INTERNAL_ERROR;
2148
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate,
2149
+ "script id space exhausted for this Context");
2150
+ st.isolate->ThrowException(v8::Exception::Error(msg));
2151
+ goto fail;
2152
+ }
2153
+ int32_t id = ++st.next_script_id;
2154
+
2155
+ {
2156
+ v8::Context::Scope context_scope(st.safe_context);
2157
+ result = v8::Array::New(st.isolate, 3);
2158
+ }
2159
+ // Populate via the goto-fail idiom, not .Check(): v8_compile runs under
2160
+ // the watchdog ('K' -> v8_timedwait), so a timeout can leave the isolate
2161
+ // terminating here, making Set() return Nothing — .Check() would abort
2162
+ // the process. The fail path replies a proper TERMINATED_ERROR instead.
2163
+ if (!result->Set(st.context, 0, v8::Int32::New(st.isolate, id)).FromMaybe(false)) goto fail;
2164
+ if (!result->Set(st.context, 1, cache_value).FromMaybe(false)) goto fail;
2165
+ if (!result->Set(st.context, 2, v8::Boolean::New(st.isolate, rejected)).FromMaybe(false)) goto fail;
2166
+
2167
+ // Register the handle only after the reply array is fully built. If a
2168
+ // Set above bailed (e.g. watchdog termination), the Ruby side gets an
2169
+ // error and never learns the id, so it could never erase the entry —
2170
+ // inserting earlier would orphan an undisposable handle until teardown.
2171
+ st.scripts[id].Reset(st.isolate, script);
2172
+ }
2173
+ cause = NO_ERROR;
2174
+ fail:
2175
+ if (st.isolate->IsExecutionTerminating()) {
2176
+ st.isolate->CancelTerminateExecution();
2177
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2178
+ st.err_reason = NO_ERROR;
2179
+ }
2180
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
2181
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
2182
+ v8::Local<v8::Value> result_v = result.IsEmpty()
2183
+ ? static_cast<v8::Local<v8::Value>>(v8::Undefined(st.isolate))
2184
+ : static_cast<v8::Local<v8::Value>>(result);
2185
+ auto err = to_error(st, &try_catch, cause);
2186
+ if (!reply(st, result_v, err)) {
2187
+ assert(try_catch.HasCaught());
2188
+ goto fail;
2189
+ }
2190
+ }
2191
+
2192
+ extern "C" void v8_run(State *pst, const uint8_t *p, size_t n)
2193
+ {
2194
+ State& st = *pst;
2195
+ v8::TryCatch try_catch(st.isolate);
2196
+ try_catch.SetVerbose(st.verbose_exceptions);
2197
+ v8::HandleScope handle_scope(st.isolate);
2198
+ v8::ValueDeserializer des(st.isolate, p, n);
2199
+ des.ReadHeader(st.context).Check();
2200
+ v8::Local<v8::Value> result;
2201
+ int cause = INTERNAL_ERROR;
2202
+ {
2203
+ v8::Local<v8::Value> id_v;
2204
+ if (!des.ReadValue(st.context).ToLocal(&id_v)) goto fail;
2205
+ int32_t id;
2206
+ if (!id_v->Int32Value(st.context).To(&id)) goto fail;
2207
+ auto it = st.scripts.find(id);
2208
+ if (it == st.scripts.end()) {
2209
+ cause = RUNTIME_ERROR;
2210
+ auto msg = v8::String::NewFromUtf8Literal(st.isolate, "no such script handle");
2211
+ st.isolate->ThrowException(v8::Exception::Error(msg));
2212
+ goto fail;
2213
+ }
2214
+ auto script = v8::Local<v8::Script>::New(st.isolate, it->second);
2215
+ v8::Local<v8::Value> result_v;
2216
+ cause = RUNTIME_ERROR;
2217
+ if (!script->Run(st.context).ToLocal(&result_v)) goto fail;
2218
+ result = sanitize(st, result_v);
2219
+ }
2220
+ cause = NO_ERROR;
2221
+ fail:
2222
+ if (st.isolate->IsExecutionTerminating()) {
2223
+ st.isolate->CancelTerminateExecution();
2224
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2225
+ st.err_reason = NO_ERROR;
2226
+ }
2227
+ if (bubble_up_ruby_exception(st, &try_catch)) return;
2228
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
2229
+ if (cause) result = v8::Undefined(st.isolate);
2230
+ auto err = to_error(st, &try_catch, cause);
2231
+ if (!reply(st, result, err)) {
2232
+ assert(try_catch.HasCaught());
2233
+ goto fail;
2234
+ }
2235
+ }
2236
+
2237
+ // Unknown ids are silently ignored — Ruby-side Script#dispose is idempotent.
2238
+ extern "C" void v8_dispose_script(State *pst, const uint8_t *p, size_t n)
2239
+ {
2240
+ State& st = *pst;
2241
+ v8::HandleScope handle_scope(st.isolate);
2242
+ v8::ValueDeserializer des(st.isolate, p, n);
2243
+ des.ReadHeader(st.context).Check();
2244
+ v8::Local<v8::Value> id_v;
2245
+ if (des.ReadValue(st.context).ToLocal(&id_v)) {
2246
+ int32_t id;
2247
+ if (id_v->Int32Value(st.context).To(&id))
2248
+ st.scripts.erase(id);
2249
+ }
2250
+ reply_retry(st, v8::String::Empty(st.isolate));
2251
+ }
2252
+
2253
+ extern "C" void v8_heap_stats(State *pst)
2254
+ {
2255
+ State& st = *pst;
2256
+ v8::HandleScope handle_scope(st.isolate);
2257
+ v8::HeapStatistics s;
2258
+ st.isolate->GetHeapStatistics(&s);
2259
+ v8::Local<v8::Object> response = v8::Object::New(st.isolate);
2260
+ #define PROP(name) \
2261
+ do { \
2262
+ auto key = v8::String::NewFromUtf8Literal(st.isolate, #name); \
2263
+ auto val = v8::Number::New(st.isolate, s.name()); \
2264
+ response->Set(st.context, key, val).Check(); \
2265
+ } while (0)
2266
+ PROP(total_heap_size);
2267
+ PROP(total_heap_size);
2268
+ PROP(total_heap_size_executable);
2269
+ PROP(total_physical_size);
2270
+ PROP(total_available_size);
2271
+ PROP(total_global_handles_size);
2272
+ PROP(used_global_handles_size);
2273
+ PROP(used_heap_size);
2274
+ PROP(heap_size_limit);
2275
+ PROP(malloced_memory);
2276
+ PROP(external_memory);
2277
+ PROP(peak_malloced_memory);
2278
+ PROP(number_of_native_contexts);
2279
+ PROP(number_of_detached_contexts);
2280
+ #undef PROP
2281
+ reply_retry(st, response);
2282
+ }
2283
+
2284
+ struct OutputStream : public v8::OutputStream
2285
+ {
2286
+ std::vector<uint8_t> buf;
2287
+
2288
+ void EndOfStream() final {}
2289
+ int GetChunkSize() final { return 65536; }
2290
+
2291
+ WriteResult WriteAsciiChunk(char* data, int size)
2292
+ {
2293
+ const uint8_t *p = reinterpret_cast<uint8_t*>(data);
2294
+ buf.insert(buf.end(), p, p+size);
2295
+ return WriteResult::kContinue;
2296
+ }
2297
+ };
2298
+
2299
+ extern "C" void v8_heap_snapshot(State *pst)
2300
+ {
2301
+ State& st = *pst;
2302
+ v8::HandleScope handle_scope(st.isolate);
2303
+ auto snapshot = st.isolate->GetHeapProfiler()->TakeHeapSnapshot();
2304
+ OutputStream os;
2305
+ snapshot->Serialize(&os, v8::HeapSnapshot::kJSON);
2306
+ v8_reply(st.ruby_context, os.buf.data(), os.buf.size()); // not serialized because big
2307
+ }
2308
+
2309
+ extern "C" void v8_perform_microtask_checkpoint(State *pst)
2310
+ {
2311
+ // Leave any termination active so the enclosing v8_call/v8_eval frame
2312
+ // surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
2313
+ State& st = *pst;
2314
+ v8::TryCatch try_catch(st.isolate);
2315
+ try_catch.SetVerbose(st.verbose_exceptions);
2316
+ v8::HandleScope handle_scope(st.isolate);
2317
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
2318
+ reply_retry(st, v8::Undefined(st.isolate));
2319
+ }
2320
+
2321
+ extern "C" void v8_pump_message_loop(State *pst)
2322
+ {
2323
+ State& st = *pst;
2324
+ v8::TryCatch try_catch(st.isolate);
2325
+ try_catch.SetVerbose(st.verbose_exceptions);
2326
+ v8::HandleScope handle_scope(st.isolate);
2327
+ bool ran_task = v8::platform::PumpMessageLoop(platform, st.isolate);
2328
+ if (st.isolate->IsExecutionTerminating()) goto fail;
2329
+ if (try_catch.HasCaught()) goto fail;
2330
+ if (ran_task) v8::MicrotasksScope::PerformCheckpoint(st.isolate);
2331
+ if (st.isolate->IsExecutionTerminating()) goto fail;
2332
+ if (platform->IdleTasksEnabled(st.isolate)) {
2333
+ double idle_time_in_seconds = 1.0 / 50;
2334
+ v8::platform::RunIdleTasks(platform, st.isolate, idle_time_in_seconds);
2335
+ if (st.isolate->IsExecutionTerminating()) goto fail;
2336
+ if (try_catch.HasCaught()) goto fail;
2337
+ }
2338
+ fail:
2339
+ if (st.isolate->IsExecutionTerminating()) {
2340
+ st.isolate->CancelTerminateExecution();
2341
+ st.err_reason = NO_ERROR;
2342
+ }
2343
+ auto result = v8::Boolean::New(st.isolate, ran_task);
2344
+ reply_retry(st, result);
2345
+ }
2346
+
2347
+ int snapshot(bool is_warmup, bool verbose_exceptions,
2348
+ const v8::String::Utf8Value& code,
2349
+ v8::StartupData blob, v8::StartupData *result,
2350
+ char (*errbuf)[512])
2351
+ {
2352
+ // SnapshotCreator takes ownership of isolate
2353
+ v8::Isolate *isolate = v8::Isolate::Allocate();
2354
+ v8::StartupData *existing_blob = is_warmup ? &blob : nullptr;
2355
+ v8::SnapshotCreator snapshot_creator(isolate, nullptr, existing_blob);
2356
+ v8::Isolate::Scope isolate_scope(isolate);
2357
+ v8::HandleScope handle_scope(isolate);
2358
+ v8::TryCatch try_catch(isolate);
2359
+ try_catch.SetVerbose(verbose_exceptions);
2360
+ auto filename = is_warmup
2361
+ ? v8::String::NewFromUtf8Literal(isolate, "<warmup>")
2362
+ : v8::String::NewFromUtf8Literal(isolate, "<snapshot>");
2363
+ auto mode = is_warmup
2364
+ ? v8::SnapshotCreator::FunctionCodeHandling::kKeep
2365
+ : v8::SnapshotCreator::FunctionCodeHandling::kClear;
2366
+ int cause = INTERNAL_ERROR;
2367
+ {
2368
+ auto context = v8::Context::New(isolate);
2369
+ v8::Context::Scope context_scope(context);
2370
+ v8::Local<v8::String> source;
2371
+ auto type = v8::NewStringType::kNormal;
2372
+ if (!v8::String::NewFromUtf8(isolate, *code, type, code.length()).ToLocal(&source)) {
2373
+ v8::String::Utf8Value s(isolate, try_catch.Exception());
2374
+ if (*s) snprintf(*errbuf, sizeof(*errbuf), "%c%s", cause, *s);
2375
+ goto fail;
2376
+ }
2377
+ v8::ScriptOrigin origin(filename);
2378
+ v8::Local<v8::Script> script;
2379
+ cause = PARSE_ERROR;
2380
+ if (!v8::Script::Compile(context, source, &origin).ToLocal(&script)) {
2381
+ goto err;
2382
+ }
2383
+ cause = RUNTIME_ERROR;
2384
+ if (script->Run(context).IsEmpty()) {
2385
+ err:
2386
+ auto m = try_catch.Message();
2387
+ v8::String::Utf8Value s(isolate, m->Get());
2388
+ v8::String::Utf8Value name(isolate, m->GetScriptResourceName());
2389
+ auto line = m->GetLineNumber(context).FromMaybe(0);
2390
+ auto column = m->GetStartColumn(context).FromMaybe(0);
2391
+ snprintf(*errbuf, sizeof(*errbuf), "%c%s\n%s:%d:%d",
2392
+ cause, *s, *name, line, column);
2393
+ goto fail;
2394
+ }
2395
+ cause = INTERNAL_ERROR;
2396
+ if (!is_warmup) snapshot_creator.SetDefaultContext(context);
2397
+ }
2398
+ if (is_warmup) {
2399
+ isolate->ContextDisposedNotification(false);
2400
+ auto context = v8::Context::New(isolate);
2401
+ snapshot_creator.SetDefaultContext(context);
2402
+ }
2403
+ *result = snapshot_creator.CreateBlob(mode);
2404
+ cause = NO_ERROR;
2405
+ fail:
2406
+ return cause;
2407
+ }
2408
+
2409
+ // response is errback [result, err] array
2410
+ // note: currently needs --stress_snapshot in V8 debug builds
2411
+ // to work around a buggy check in the snapshot deserializer
2412
+ extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n)
2413
+ {
2414
+ State& st = *pst;
2415
+ v8::TryCatch try_catch(st.isolate);
2416
+ try_catch.SetVerbose(st.verbose_exceptions);
2417
+ v8::HandleScope handle_scope(st.isolate);
2418
+ v8::ValueDeserializer des(st.isolate, p, n);
2419
+ des.ReadHeader(st.context).Check();
2420
+ v8::Local<v8::Value> result;
2421
+ v8::StartupData blob{nullptr, 0};
2422
+ int cause = INTERNAL_ERROR;
2423
+ char errbuf[512] = {0};
2424
+ {
2425
+ v8::Local<v8::Value> code_v;
2426
+ if (!des.ReadValue(st.context).ToLocal(&code_v)) goto fail;
2427
+ v8::String::Utf8Value code(st.isolate, code_v);
2428
+ if (!*code) goto fail;
2429
+ v8::StartupData init{nullptr, 0};
2430
+ cause = snapshot(/*is_warmup*/false, st.verbose_exceptions, code, init, &blob, &errbuf);
2431
+ if (cause) goto fail;
2432
+ }
2433
+ if (blob.data) {
2434
+ auto data = reinterpret_cast<const uint8_t*>(blob.data);
2435
+ auto type = v8::NewStringType::kNormal;
2436
+ bool ok = v8::String::NewFromOneByte(st.isolate, data, type,
2437
+ blob.raw_size).ToLocal(&result);
2438
+ delete[] blob.data;
2439
+ blob = v8::StartupData{nullptr, 0};
2440
+ if (!ok) goto fail;
2441
+ }
2442
+ cause = NO_ERROR;
2443
+ fail:
2444
+ if (st.isolate->IsExecutionTerminating()) {
2445
+ st.isolate->CancelTerminateExecution();
2446
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2447
+ st.err_reason = NO_ERROR;
2448
+ }
2449
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
2450
+ if (cause) result = v8::Undefined(st.isolate);
2451
+ v8::Local<v8::Value> err;
2452
+ if (*errbuf) {
2453
+ if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
2454
+ err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
2455
+ }
2456
+ } else {
2457
+ err = to_error(st, &try_catch, cause);
2458
+ }
2459
+ if (!reply(st, result, err)) {
2460
+ assert(try_catch.HasCaught());
2461
+ goto fail; // retry; can be termination exception
2462
+ }
2463
+ }
2464
+
2465
+ extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n)
2466
+ {
2467
+ State& st = *pst;
2468
+ v8::TryCatch try_catch(st.isolate);
2469
+ try_catch.SetVerbose(st.verbose_exceptions);
2470
+ v8::HandleScope handle_scope(st.isolate);
2471
+ std::vector<uint8_t> storage;
2472
+ v8::ValueDeserializer des(st.isolate, p, n);
2473
+ des.ReadHeader(st.context).Check();
2474
+ v8::Local<v8::Value> result;
2475
+ v8::StartupData blob{nullptr, 0};
2476
+ int cause = INTERNAL_ERROR;
2477
+ char errbuf[512] = {0};
2478
+ {
2479
+ v8::Local<v8::Value> request_v;
2480
+ if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
2481
+ v8::Local<v8::Object> request; // [snapshot, warmup_code]
2482
+ if (!request_v->ToObject(st.context).ToLocal(&request)) goto fail;
2483
+ v8::Local<v8::Value> blob_data_v;
2484
+ if (!request->Get(st.context, 0).ToLocal(&blob_data_v)) goto fail;
2485
+ v8::Local<v8::String> blob_data;
2486
+ if (!blob_data_v->ToString(st.context).ToLocal(&blob_data)) goto fail;
2487
+ assert(blob_data->IsOneByte());
2488
+ assert(blob_data->ContainsOnlyOneByte());
2489
+ if (const size_t len = blob_data->Length()) {
2490
+ auto flags = v8::String::NO_NULL_TERMINATION
2491
+ | v8::String::PRESERVE_ONE_BYTE_NULL;
2492
+ storage.resize(len);
2493
+ blob_data->WriteOneByte(st.isolate, storage.data(), 0, len, flags);
2494
+ }
2495
+ v8::Local<v8::Value> code_v;
2496
+ if (!request->Get(st.context, 1).ToLocal(&code_v)) goto fail;
2497
+ v8::String::Utf8Value code(st.isolate, code_v);
2498
+ if (!*code) goto fail;
2499
+ auto data = reinterpret_cast<const char*>(storage.data());
2500
+ auto size = static_cast<int>(storage.size());
2501
+ v8::StartupData init{data, size};
2502
+ cause = snapshot(/*is_warmup*/true, st.verbose_exceptions, code, init, &blob, &errbuf);
2503
+ if (cause) goto fail;
2504
+ }
2505
+ if (blob.data) {
2506
+ auto data = reinterpret_cast<const uint8_t*>(blob.data);
2507
+ auto type = v8::NewStringType::kNormal;
2508
+ bool ok = v8::String::NewFromOneByte(st.isolate, data, type,
2509
+ blob.raw_size).ToLocal(&result);
2510
+ delete[] blob.data;
2511
+ blob = v8::StartupData{nullptr, 0};
2512
+ if (!ok) goto fail;
2513
+ }
2514
+ cause = NO_ERROR;
2515
+ fail:
2516
+ if (st.isolate->IsExecutionTerminating()) {
2517
+ st.isolate->CancelTerminateExecution();
2518
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2519
+ st.err_reason = NO_ERROR;
2520
+ }
2521
+ if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
2522
+ if (cause) result = v8::Undefined(st.isolate);
2523
+ v8::Local<v8::Value> err;
2524
+ if (*errbuf) {
2525
+ if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
2526
+ err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
2527
+ }
2528
+ } else {
2529
+ err = to_error(st, &try_catch, cause);
2530
+ }
2531
+ if (!reply(st, result, err)) {
2532
+ assert(try_catch.HasCaught());
2533
+ goto fail; // retry; can be termination exception
2534
+ }
2535
+ }
2536
+
2537
+ extern "C" void v8_low_memory_notification(State *pst)
2538
+ {
2539
+ pst->isolate->LowMemoryNotification();
2540
+ }
2541
+
2542
+ // called from ruby thread
2543
+ extern "C" void v8_terminate_execution(State *pst)
2544
+ {
2545
+ pst->isolate->TerminateExecution();
2546
+ }
2547
+
2548
+ // Per-request realm restore for the dedicated V8 thread. v8_thread_init holds
2549
+ // the Locker + Isolate::Scope for the thread's whole life, so unlike
2550
+ // v8_single_threaded_enter this only needs a HandleScope. Re-deriving the
2551
+ // Locals from the persistents on every request is what lets v8_reset_realm
2552
+ // swap the realm underneath the running loop.
2553
+ extern "C" void v8_threaded_enter(State *pst, Context *c, void (*f)(Context *c))
2554
+ {
2555
+ State& st = *pst;
2556
+ v8::HandleScope handle_scope(st.isolate);
2557
+ restore_realm_locals(st);
2558
+ {
2559
+ v8::Context::Scope context_scope(st.context);
2560
+ f(c);
2561
+ }
2562
+ clear_realm_locals(st);
2563
+ }
2564
+
2565
+ // Drops the current user realm and installs a fresh one from the (snapshot)
2566
+ // default context, keeping the isolate — and its warm compilation cache —
2567
+ // alive. The opt-in host namespace and any attached host functions are
2568
+ // re-applied. Once install_realm commits the new realm and the old realm's
2569
+ // remaining roots are released below, the previous globalThis (and everything
2570
+ // hung off it) is unreachable and gets collected.
2571
+ extern "C" void v8_reset_realm(State *pst)
2572
+ {
2573
+ State& st = *pst;
2574
+ v8::TryCatch try_catch(st.isolate);
2575
+ try_catch.SetVerbose(st.verbose_exceptions);
2576
+ v8::HandleScope handle_scope(st.isolate);
2577
+
2578
+ // Refuse to swap the realm out from under a JS->Ruby callback. When a host
2579
+ // function's Ruby code calls reset_realm, an outer v8_api_callback frame is
2580
+ // suspended mid-roundtrip with the old context entered and would resume
2581
+ // against the swapped realm — corrupting it. (in_callback is the same signal
2582
+ // that makes compile() refuse CreateCodeCache mid-callback.)
2583
+ if (st.in_callback > 0) {
2584
+ char buf[128];
2585
+ snprintf(buf, sizeof(buf), "%c%s", RUNTIME_ERROR,
2586
+ "reset_realm cannot be called from within a host function callback");
2587
+ v8::Local<v8::String> err;
2588
+ if (!v8::String::NewFromUtf8(st.isolate, buf).ToLocal(&err))
2589
+ err = v8::String::Empty(st.isolate);
2590
+ reply_retry(st, err);
2591
+ return;
2592
+ }
2593
+
2594
+ // install_realm is all-or-nothing: on failure the previous realm is left
2595
+ // intact in the persistents. Surface the cause, and if a watchdog/OOM
2596
+ // terminated execution mid-rebuild, clear it here so it does not poison the
2597
+ // next unrelated request (mirrors v8_eval/v8_call's termination handling).
2598
+ if (!install_realm(st)) {
2599
+ int cause;
2600
+ if (st.isolate->IsExecutionTerminating()) {
2601
+ st.isolate->CancelTerminateExecution();
2602
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
2603
+ st.err_reason = NO_ERROR;
2604
+ } else {
2605
+ cause = try_catch.HasCaught() ? RUNTIME_ERROR : INTERNAL_ERROR;
2606
+ }
2607
+ // Restore Locals (the rebuild may have cleared them) so the error reply
2608
+ // can be serialized against the surviving realm.
2609
+ restore_realm_locals(st);
2610
+ reply_retry(st, to_error(st, &try_catch, cause));
2611
+ return;
2612
+ }
2613
+
2614
+ // New realm committed. Release the remaining roots into the old realm:
2615
+ // a pending ruby-exception handle and the scripts/modules compiled against
2616
+ // it (their handles are realm-bound, so they would both pin the old realm
2617
+ // and, if run after the swap, execute against the wrong globalThis — they
2618
+ // are invalidated here).
2619
+ if (module_trace_on())
2620
+ fprintf(stderr, "[mr.reset] clearing modules=%zu scripts=%zu url_index=%zu\n",
2621
+ st.modules.size(), st.scripts.size(), st.module_id_by_url.size()), fflush(stderr);
2622
+ st.ruby_exception.Reset();
2623
+ // clear() destroys each v8::Global, and ~Global() Resets the handle under
2624
+ // the still-live isolate — no explicit Reset loop needed (it would be for
2625
+ // the old default-traits Persistent, whose destructor is a no-op).
2626
+ st.scripts.clear();
2627
+ st.modules.clear();
2628
+ st.module_id_by_url.clear();
2629
+ // The fresh realm has no registered modules; fall back to the legacy dynamic
2630
+ // import resolver until load_module_graph runs again and re-latches this.
2631
+ st.uses_graph_loader = false;
2632
+
2633
+ // Tell V8 the old realm is gone — the same primitive a browser uses on
2634
+ // navigation / iframe teardown. Unlike low_memory_notification (a full GC
2635
+ // that also flushes the compilation cache, throwing away the warm bytecode
2636
+ // that makes realm reuse worthwhile), this nudges V8's MemoryReducer to
2637
+ // reclaim the now-detached realm incrementally while KEEPING the compilation
2638
+ // cache. Without it, detached realms accrue until heap pressure forces a GC,
2639
+ // which on a heavy app (e.g. Discourse) can overshoot the limit before it
2640
+ // fires. The `false` (no dependent context) argument is what triggers the
2641
+ // proactive reduction — mirrors the snapshot-warmup call site.
2642
+ st.isolate->ContextDisposedNotification(false);
2643
+
2644
+ // Restore Locals from the new persistents and enter the new context so the
2645
+ // reply is serialized against it (install_realm leaves the members empty).
2646
+ restore_realm_locals(st);
2647
+ v8::Context::Scope context_scope(st.context);
2648
+ reply_retry(st, to_error(st, &try_catch, NO_ERROR));
2649
+ }
2650
+
2651
+ extern "C" void v8_single_threaded_enter(State *pst, Context *c, void (*f)(Context *c))
2652
+ {
2653
+ State& st = *pst;
2654
+ v8::Locker locker(st.isolate);
2655
+ v8::Isolate::Scope isolate_scope(st.isolate);
2656
+ v8::HandleScope handle_scope(st.isolate);
2657
+ {
2658
+ restore_realm_locals(st);
2659
+ v8::Context::Scope context_scope(st.context);
2660
+ f(c);
2661
+ clear_realm_locals(st);
2662
+ }
2663
+ }
2664
+
2665
+ extern "C" void v8_single_threaded_dispose(struct State *pst)
2666
+ {
2667
+ delete pst; // see State::~State() below
2668
+ }
2669
+
2670
+ extern "C" uint32_t v8_cached_data_version_tag(void)
2671
+ {
2672
+ return v8::ScriptCompiler::CachedDataVersionTag();
2673
+ }
2674
+
2675
+ } // namespace anonymous
2676
+
2677
+ State::~State()
2678
+ {
2679
+ {
2680
+ v8::Locker locker(isolate);
2681
+ v8::Isolate::Scope isolate_scope(isolate);
2682
+ modules.clear();
2683
+ scripts.clear();
2684
+ persistent_safe_context_function.Reset();
2685
+ persistent_safe_context.Reset();
2686
+ persistent_context.Reset();
2687
+ ruby_exception.Reset();
2688
+ }
2689
+ isolate->Dispose();
2690
+ for (Callback *cb : callbacks)
2691
+ delete cb;
2692
+ }