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.
- checksums.yaml +7 -0
- data/CHANGELOG +351 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +687 -0
- data/ext/mini_racer_extension/extconf.rb +75 -0
- data/ext/mini_racer_extension/mini_racer_extension.c +2899 -0
- data/ext/mini_racer_extension/mini_racer_v8.cc +2692 -0
- data/ext/mini_racer_extension/mini_racer_v8.h +70 -0
- data/ext/mini_racer_extension/serde.c +782 -0
- data/ext/mini_racer_loader/extconf.rb +13 -0
- data/ext/mini_racer_loader/mini_racer_loader.c +123 -0
- data/lib/mini_racer/shared.rb +395 -0
- data/lib/mini_racer/truffleruby.rb +479 -0
- data/lib/mini_racer/version.rb +9 -0
- data/lib/mini_racer-csim.rb +4 -0
- data/lib/mini_racer.rb +117 -0
- metadata +168 -0
|
@@ -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
|
+
}
|