rusty_racer 0.1.1 → 0.1.2
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 +4 -4
- data/README.md +50 -0
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +23 -2228
- data/ext/rusty_racer/src/marshal.rs +798 -0
- data/ext/rusty_racer/src/ops.rs +1061 -0
- data/ext/rusty_racer/src/stack.rs +292 -0
- data/ext/rusty_racer/src/watchdog.rs +226 -0
- data/lib/rusty_racer/execjs.rb +118 -0
- data/lib/rusty_racer/version.rb +1 -1
- metadata +13 -4
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
// The request/dispatch layer: the Request and VmReply value types, the
|
|
2
|
+
// service_request -> dispatch_one fan-out, every per-op handler (op_*), and the
|
|
3
|
+
// three op helpers run_source / call_function / compile_source. Extracted from
|
|
4
|
+
// lib.rs verbatim.
|
|
5
|
+
//
|
|
6
|
+
// Request, VmReply and Compiled are pub(crate) (the magnus method impls and
|
|
7
|
+
// Core/Isolate/Context/Module/Script wiring still in lib.rs build and consume
|
|
8
|
+
// them, and read Compiled's fields). service_request is pub(crate) (Core::run
|
|
9
|
+
// calls it) and run_source too (Snapshot warmup calls it). request_realm,
|
|
10
|
+
// dispatch_one, the op_* handlers, call_function and compile_source are used
|
|
11
|
+
// only here and stay private.
|
|
12
|
+
//
|
|
13
|
+
// ops.rs reaches the crate root's (private) helpers, structs and the istate!
|
|
14
|
+
// macro through the imports below; the marshal/watchdog symbols come from their
|
|
15
|
+
// own modules.
|
|
16
|
+
|
|
17
|
+
use crate::istate;
|
|
18
|
+
use crate::marshal::{js_to_jsval, jsval_to_js, JsVal};
|
|
19
|
+
use crate::*;
|
|
20
|
+
|
|
21
|
+
// One VM operation, built by a magnus method and run inline by Core::run ->
|
|
22
|
+
// service_request -> dispatch_one. |context_id| selects which realm the op runs
|
|
23
|
+
// in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
|
|
24
|
+
// N >= 1 = an extra realm made by create_context.
|
|
25
|
+
pub(crate) enum Request {
|
|
26
|
+
Eval {
|
|
27
|
+
context_id: i32,
|
|
28
|
+
source: String,
|
|
29
|
+
filename: String,
|
|
30
|
+
timeout_ms: u64,
|
|
31
|
+
},
|
|
32
|
+
// Resolve a dotted function path on globalThis and invoke it with marshalled
|
|
33
|
+
// args (v8::Function::call), preserving the holder as `this`. Distinct from
|
|
34
|
+
// Eval so args keep full type/identity fidelity instead of a JSON literal.
|
|
35
|
+
Call {
|
|
36
|
+
context_id: i32,
|
|
37
|
+
name: String,
|
|
38
|
+
args: Vec<JsVal>,
|
|
39
|
+
// void = don't marshal the return (fire-and-forget): the called fn may
|
|
40
|
+
// return a huge/cyclic JS object the caller never reads.
|
|
41
|
+
void: bool,
|
|
42
|
+
timeout_ms: u64,
|
|
43
|
+
},
|
|
44
|
+
// Drain the isolate's microtask queue once (no auto event loop).
|
|
45
|
+
DrainMicrotasks {
|
|
46
|
+
timeout_ms: u64,
|
|
47
|
+
},
|
|
48
|
+
Attach {
|
|
49
|
+
context_id: i32,
|
|
50
|
+
name: String,
|
|
51
|
+
host_fn_id: usize,
|
|
52
|
+
timeout_ms: u64,
|
|
53
|
+
},
|
|
54
|
+
// Batch attach: install many (name, host_fn_id) host fns in one round-trip
|
|
55
|
+
// (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
|
|
56
|
+
AttachMany {
|
|
57
|
+
context_id: i32,
|
|
58
|
+
entries: Vec<(String, usize)>,
|
|
59
|
+
timeout_ms: u64,
|
|
60
|
+
},
|
|
61
|
+
// reset: swap globalThis for a fresh v8::Context, reusing the same warm
|
|
62
|
+
// isolate — csim's per-visit reset. Applies to the named context.
|
|
63
|
+
Reset {
|
|
64
|
+
context_id: i32,
|
|
65
|
+
},
|
|
66
|
+
// create_context: build a fresh, persistent v8::Context in the isolate and
|
|
67
|
+
// return its id (the multi-realm model). DisposeContext frees one.
|
|
68
|
+
CreateContext,
|
|
69
|
+
DisposeContext {
|
|
70
|
+
context_id: i32,
|
|
71
|
+
},
|
|
72
|
+
// Thin ES-module primitives (V8's raw compile/instantiate/evaluate). The
|
|
73
|
+
// embedder owns the url->Module registry and the resolve policy; the binding
|
|
74
|
+
// just exposes the steps. A compiled module is addressed by an id (like a
|
|
75
|
+
// realm) since a v8::Local handle can't outlive the op's scope.
|
|
76
|
+
CompileModule {
|
|
77
|
+
// The context to compile the module in (modules are realm-bound).
|
|
78
|
+
context_id: i32,
|
|
79
|
+
source: String,
|
|
80
|
+
filename: String,
|
|
81
|
+
// Bytecode cache to consume (skip reparse); None compiles fresh.
|
|
82
|
+
cached_data: Option<Vec<u8>>,
|
|
83
|
+
// Produce a fresh bytecode cache to hand back (Module#cached_data).
|
|
84
|
+
produce_cache: bool,
|
|
85
|
+
// Eager-compile every function up front (CompileOptions::EagerCompile)
|
|
86
|
+
// instead of V8's default lazy top-level-only compile. Ignored when
|
|
87
|
+
// cached_data is set (V8 forbids ConsumeCodeCache + EagerCompile).
|
|
88
|
+
eager: bool,
|
|
89
|
+
},
|
|
90
|
+
// instantiate: V8 walks imports, calling back to the Ruby resolve block
|
|
91
|
+
// (parked in the slot for the op) per edge via resolve_imported.
|
|
92
|
+
InstantiateModule {
|
|
93
|
+
module_id: i32,
|
|
94
|
+
},
|
|
95
|
+
EvaluateModule {
|
|
96
|
+
module_id: i32,
|
|
97
|
+
timeout_ms: u64,
|
|
98
|
+
},
|
|
99
|
+
ModuleNamespace {
|
|
100
|
+
module_id: i32,
|
|
101
|
+
},
|
|
102
|
+
// The module's v8::Module::Status, as a lowercase name ("uninstantiated",
|
|
103
|
+
// "instantiated", ...) the Ruby wrapper symbolizes.
|
|
104
|
+
ModuleStatus {
|
|
105
|
+
module_id: i32,
|
|
106
|
+
},
|
|
107
|
+
DisposeModule {
|
|
108
|
+
module_id: i32,
|
|
109
|
+
},
|
|
110
|
+
// Classic <script> primitives (V8 ScriptCompiler::CompileUnboundScript): an
|
|
111
|
+
// unbound script, compiled in a context, runnable repeatedly, with the same
|
|
112
|
+
// bytecode-cache options as modules. Addressed by id like a module.
|
|
113
|
+
CompileScript {
|
|
114
|
+
context_id: i32,
|
|
115
|
+
source: String,
|
|
116
|
+
filename: String,
|
|
117
|
+
cached_data: Option<Vec<u8>>,
|
|
118
|
+
produce_cache: bool,
|
|
119
|
+
eager: bool,
|
|
120
|
+
},
|
|
121
|
+
// Bind the script to its context and run it; returns the completion value.
|
|
122
|
+
RunScript {
|
|
123
|
+
script_id: i32,
|
|
124
|
+
timeout_ms: u64,
|
|
125
|
+
},
|
|
126
|
+
DisposeScript {
|
|
127
|
+
script_id: i32,
|
|
128
|
+
},
|
|
129
|
+
// Serialize a bytecode cache from a compiled handle's CURRENT compile state
|
|
130
|
+
// (Script#create_code_cache / Module#create_code_cache). Called after run/
|
|
131
|
+
// evaluate, it captures the inner functions V8 lazily compiled while running
|
|
132
|
+
// — the only way (as of V8-150) to get inner-function bytecode into a cache,
|
|
133
|
+
// since create_code_cache at compile time only sees the top level.
|
|
134
|
+
ScriptCodeCache {
|
|
135
|
+
script_id: i32,
|
|
136
|
+
},
|
|
137
|
+
ModuleCodeCache {
|
|
138
|
+
module_id: i32,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// compile_module result: the module's id plus any produced bytecode cache and
|
|
143
|
+
// whether a supplied cache was rejected.
|
|
144
|
+
pub(crate) struct Compiled {
|
|
145
|
+
pub(crate) id: i32,
|
|
146
|
+
pub(crate) cached_data: Option<Vec<u8>>,
|
|
147
|
+
pub(crate) cache_rejected: bool,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The terminal reply of an op: service_request returns it straight up to
|
|
151
|
+
// Core::run (no channel). Host callbacks and module resolvers don't round-trip
|
|
152
|
+
// through here — they run inline (with_gvl).
|
|
153
|
+
pub(crate) enum VmReply {
|
|
154
|
+
Done(Result<JsVal, VmError>),
|
|
155
|
+
// compile_module / compile's richer reply (id + produced cache + rejected).
|
|
156
|
+
ModuleCompiled(Result<Compiled, VmError>),
|
|
157
|
+
ScriptCompiled(Result<Compiled, VmError>),
|
|
158
|
+
// Script#/Module#create_code_cache: the serialized bytes, or None when V8
|
|
159
|
+
// can't produce a cache (or the handle's realm is gone).
|
|
160
|
+
CodeCache(Result<Option<Vec<u8>>, VmError>),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pub(crate) fn run_source(scope: &mut v8::PinScope<'_, '_>, source: &str, filename: &str) -> Result<JsVal, VmError> {
|
|
164
|
+
v8::tc_scope!(let tc, scope);
|
|
165
|
+
// Compile and run as distinct phases so a compile failure maps to
|
|
166
|
+
// ParseError and a thrown exception to RuntimeError (csim rescues both).
|
|
167
|
+
let Some(code) = v8::String::new(tc, source) else {
|
|
168
|
+
return Err(VmError::Parse("source too large".into()));
|
|
169
|
+
};
|
|
170
|
+
let origin = script_origin(tc, filename);
|
|
171
|
+
let script = match v8::Script::compile(tc, code, Some(&origin)) {
|
|
172
|
+
Some(script) => script,
|
|
173
|
+
None if tc.has_terminated() => return Err(VmError::Terminated),
|
|
174
|
+
None => {
|
|
175
|
+
let msg = tc
|
|
176
|
+
.exception()
|
|
177
|
+
.map(|e| e.to_rust_string_lossy(tc))
|
|
178
|
+
.unwrap_or_else(|| "parse error".to_string());
|
|
179
|
+
// Append the location V8 recorded; always name the file, add the
|
|
180
|
+
// line when V8 reports one.
|
|
181
|
+
let message = tc.message();
|
|
182
|
+
let res = message
|
|
183
|
+
.and_then(|m| m.get_script_resource_name(tc))
|
|
184
|
+
.filter(|v| v.is_string())
|
|
185
|
+
.map(|v| v.to_rust_string_lossy(tc))
|
|
186
|
+
.unwrap_or_else(|| filename.to_string());
|
|
187
|
+
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
188
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
189
|
+
None => format!(" at {res}"),
|
|
190
|
+
};
|
|
191
|
+
return Err(VmError::Parse(format!("{msg}{loc}")));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
match script.run(tc) {
|
|
195
|
+
Some(value) => Ok(js_to_jsval(tc, value)),
|
|
196
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
197
|
+
None => {
|
|
198
|
+
let exc = tc.exception();
|
|
199
|
+
let stack = tc.stack_trace();
|
|
200
|
+
Err(capture_js_error(tc, exc, stack))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Resolve a dotted property path on globalThis to a function and invoke it via
|
|
206
|
+
// v8::Function::call, with the property's holder as `this` (so `a.b.f` gets the
|
|
207
|
+
// right receiver). Args/result marshal through the ref-preserving paths.
|
|
208
|
+
fn call_function(
|
|
209
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
210
|
+
name: &str,
|
|
211
|
+
args: Vec<JsVal>,
|
|
212
|
+
void: bool,
|
|
213
|
+
) -> Result<JsVal, VmError> {
|
|
214
|
+
v8::tc_scope!(let tc, scope);
|
|
215
|
+
let context = tc.get_current_context();
|
|
216
|
+
let global = context.global(tc);
|
|
217
|
+
let mut recv: v8::Local<v8::Value> = global.into();
|
|
218
|
+
let mut target: v8::Local<v8::Value> = global.into();
|
|
219
|
+
for part in name.split('.') {
|
|
220
|
+
let Some(obj) = target.to_object(tc) else {
|
|
221
|
+
// The holder of `part` (a preceding segment) was null/undefined, so
|
|
222
|
+
// there's nothing to read `part` from — name the holder, not `part`.
|
|
223
|
+
return Err(VmError::Runtime(format!(
|
|
224
|
+
"`{name}`: cannot read `{part}` (a preceding path segment is not an object)"
|
|
225
|
+
)));
|
|
226
|
+
};
|
|
227
|
+
let Some(key) = v8::String::new(tc, part) else {
|
|
228
|
+
return Err(VmError::Runtime("property name too large".into()));
|
|
229
|
+
};
|
|
230
|
+
let Some(next) = obj.get(tc, key.into()) else {
|
|
231
|
+
if tc.has_terminated() {
|
|
232
|
+
return Err(VmError::Terminated);
|
|
233
|
+
}
|
|
234
|
+
let msg = tc
|
|
235
|
+
.exception()
|
|
236
|
+
.map(|e| e.to_rust_string_lossy(tc))
|
|
237
|
+
.unwrap_or_else(|| format!("cannot read `{part}` of `{name}`"));
|
|
238
|
+
return Err(VmError::Runtime(msg));
|
|
239
|
+
};
|
|
240
|
+
recv = target;
|
|
241
|
+
target = next;
|
|
242
|
+
}
|
|
243
|
+
let Ok(func) = v8::Local::<v8::Function>::try_from(target) else {
|
|
244
|
+
return Err(VmError::Runtime(format!("`{name}` is not a function")));
|
|
245
|
+
};
|
|
246
|
+
let argv: Vec<v8::Local<v8::Value>> = args.into_iter().map(|a| jsval_to_js(tc, a)).collect();
|
|
247
|
+
match func.call(tc, recv, &argv) {
|
|
248
|
+
// void: skip marshalling the return so a huge/cyclic result is never walked.
|
|
249
|
+
Some(_) if void => Ok(JsVal::Undefined),
|
|
250
|
+
Some(value) => Ok(js_to_jsval(tc, value)),
|
|
251
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
252
|
+
None => {
|
|
253
|
+
let exc = tc.exception();
|
|
254
|
+
let stack = tc.stack_trace();
|
|
255
|
+
Err(capture_js_error(tc, exc, stack))
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The (Source, CompileOptions) pair shared by the module and script compile
|
|
261
|
+
// handlers: consume a supplied bytecode cache (skip reparse), else eager-compile
|
|
262
|
+
// every function up front, else compile lazily (V8's default — only the top
|
|
263
|
+
// level). A supplied cache wins over `eager`: V8's CompileOptionsIsValid forbids
|
|
264
|
+
// ConsumeCodeCache + EagerCompile together, so `eager` is ignored on the consume
|
|
265
|
+
// path. (Source is an owned struct — V8 copies the origin in — so returning it
|
|
266
|
+
// across this fn boundary keeps the same handle-lifetime contract as inlining.)
|
|
267
|
+
fn compile_source<'s>(
|
|
268
|
+
code: v8::Local<'s, v8::String>,
|
|
269
|
+
origin: &v8::ScriptOrigin<'s>,
|
|
270
|
+
cached_data: &Option<Vec<u8>>,
|
|
271
|
+
eager: bool,
|
|
272
|
+
) -> (v8::script_compiler::Source, v8::script_compiler::CompileOptions) {
|
|
273
|
+
use v8::script_compiler::{CompileOptions, Source};
|
|
274
|
+
match cached_data {
|
|
275
|
+
Some(bytes) => (
|
|
276
|
+
Source::new_with_cached_data(code, Some(origin), v8::script_compiler::CachedData::new(bytes)),
|
|
277
|
+
CompileOptions::ConsumeCodeCache,
|
|
278
|
+
),
|
|
279
|
+
None if eager => (Source::new(code, Some(origin)), CompileOptions::EagerCompile),
|
|
280
|
+
None => (Source::new(code, Some(origin)), CompileOptions::NoCompileOptions),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Service ONE request inline on the owner thread and RETURN its terminal reply.
|
|
285
|
+
// This is the single dispatcher for BOTH a top-level op and a re-entrant one (a
|
|
286
|
+
// host proc / module resolver that issues another op), so EVERY op — not just
|
|
287
|
+
// eval/call — works re-entrantly. `outermost` (depth == 0, computed by Core::run
|
|
288
|
+
// before it bumped the depth) owns the terminate-flag cleanup; a nested op
|
|
289
|
+
// passes false.
|
|
290
|
+
pub(crate) fn service_request(scope: &mut v8::PinScope<'_, '_, ()>, request: Request, outermost: bool) -> VmReply {
|
|
291
|
+
// Clear any terminate left over from BEFORE this request. An
|
|
292
|
+
// Isolate#terminate fired while no JS was running arms the isolate-global
|
|
293
|
+
// flag but no watchdog_fired, so the end-of-request sweep would miss it and
|
|
294
|
+
// the next eval would abort spuriously — and an idle terminate isn't even
|
|
295
|
+
// observable via is_execution_terminating() yet, so cancel unconditionally.
|
|
296
|
+
// Only at the outermost frame: a terminate aimed at a SUSPENDED outer frame
|
|
297
|
+
// must survive a nested request.
|
|
298
|
+
if outermost {
|
|
299
|
+
scope.cancel_terminate_execution();
|
|
300
|
+
}
|
|
301
|
+
// Mark the realm this request runs in active while it is on the stack, so
|
|
302
|
+
// Reset/DisposeContext can refuse to pull a live realm out from under a
|
|
303
|
+
// suspended frame.
|
|
304
|
+
let realm = request_realm(istate!(scope), &request);
|
|
305
|
+
if let Some(id) = realm {
|
|
306
|
+
istate!(scope).active_realms.push(id);
|
|
307
|
+
}
|
|
308
|
+
let reply = dispatch_one(scope, request, outermost);
|
|
309
|
+
if realm.is_some() {
|
|
310
|
+
istate!(scope).active_realms.pop();
|
|
311
|
+
}
|
|
312
|
+
// Sweep a leftover terminate flag once the whole request stack has
|
|
313
|
+
// unwound (see watchdog_fired for why nested frames must not cancel).
|
|
314
|
+
if outermost && istate!(scope).watchdog_fired {
|
|
315
|
+
istate!(scope).watchdog_fired = false;
|
|
316
|
+
scope.cancel_terminate_execution();
|
|
317
|
+
}
|
|
318
|
+
reply
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The realm a request will run in (None for realm-independent ops); feeds
|
|
322
|
+
// ACTIVE_REALMS above.
|
|
323
|
+
fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
|
|
324
|
+
match request {
|
|
325
|
+
Request::Eval { context_id, .. }
|
|
326
|
+
| Request::Call { context_id, .. }
|
|
327
|
+
| Request::Attach { context_id, .. }
|
|
328
|
+
| Request::AttachMany { context_id, .. }
|
|
329
|
+
| Request::CompileModule { context_id, .. }
|
|
330
|
+
| Request::CompileScript { context_id, .. } => Some(*context_id),
|
|
331
|
+
Request::DrainMicrotasks { .. } => Some(0),
|
|
332
|
+
Request::InstantiateModule { module_id, .. }
|
|
333
|
+
| Request::EvaluateModule { module_id, .. }
|
|
334
|
+
| Request::ModuleNamespace { module_id, .. } => {
|
|
335
|
+
module_handle(state, *module_id).map(|(_, cid)| cid)
|
|
336
|
+
}
|
|
337
|
+
Request::RunScript { script_id, .. } => script_handle(state, *script_id).map(|(_, cid)| cid),
|
|
338
|
+
Request::Reset { .. }
|
|
339
|
+
| Request::CreateContext
|
|
340
|
+
| Request::DisposeContext { .. }
|
|
341
|
+
| Request::ModuleStatus { .. }
|
|
342
|
+
| Request::DisposeModule { .. }
|
|
343
|
+
| Request::DisposeScript { .. }
|
|
344
|
+
| Request::ScriptCodeCache { .. }
|
|
345
|
+
| Request::ModuleCodeCache { .. } => None,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn dispatch_one(scope: &mut v8::PinScope<'_, '_, ()>, request: Request, outermost: bool) -> VmReply {
|
|
350
|
+
// A request-scoped handle scope, so handles created while servicing a
|
|
351
|
+
// nested request don't pile up in the suspended callback's scope.
|
|
352
|
+
v8::scope!(let scope, &mut *scope);
|
|
353
|
+
match request {
|
|
354
|
+
Request::Eval {
|
|
355
|
+
context_id,
|
|
356
|
+
source,
|
|
357
|
+
filename,
|
|
358
|
+
timeout_ms,
|
|
359
|
+
} => op_eval(scope, context_id, source, filename, timeout_ms, outermost),
|
|
360
|
+
Request::Call {
|
|
361
|
+
context_id,
|
|
362
|
+
name,
|
|
363
|
+
args,
|
|
364
|
+
void,
|
|
365
|
+
timeout_ms,
|
|
366
|
+
} => op_call(scope, context_id, name, args, void, timeout_ms, outermost),
|
|
367
|
+
Request::DrainMicrotasks { timeout_ms } => op_drain_microtasks(scope, timeout_ms),
|
|
368
|
+
Request::Attach {
|
|
369
|
+
context_id,
|
|
370
|
+
name,
|
|
371
|
+
host_fn_id,
|
|
372
|
+
timeout_ms,
|
|
373
|
+
} => op_attach(scope, context_id, name, host_fn_id, timeout_ms, outermost),
|
|
374
|
+
Request::AttachMany {
|
|
375
|
+
context_id,
|
|
376
|
+
entries,
|
|
377
|
+
timeout_ms,
|
|
378
|
+
} => op_attach_many(scope, context_id, entries, timeout_ms, outermost),
|
|
379
|
+
Request::Reset { context_id } => op_reset(scope, context_id),
|
|
380
|
+
Request::CreateContext => op_create_context(scope),
|
|
381
|
+
Request::DisposeContext { context_id } => op_dispose_context(scope, context_id),
|
|
382
|
+
Request::CompileModule {
|
|
383
|
+
context_id,
|
|
384
|
+
source,
|
|
385
|
+
filename,
|
|
386
|
+
cached_data,
|
|
387
|
+
produce_cache,
|
|
388
|
+
eager,
|
|
389
|
+
} => op_compile_module(scope, context_id, source, filename, cached_data, produce_cache, eager),
|
|
390
|
+
Request::InstantiateModule { module_id } => op_instantiate_module(scope, module_id),
|
|
391
|
+
Request::EvaluateModule { module_id, timeout_ms } => op_evaluate_module(scope, module_id, timeout_ms, outermost),
|
|
392
|
+
Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
|
|
393
|
+
Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
|
|
394
|
+
Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
|
|
395
|
+
Request::CompileScript {
|
|
396
|
+
context_id,
|
|
397
|
+
source,
|
|
398
|
+
filename,
|
|
399
|
+
cached_data,
|
|
400
|
+
produce_cache,
|
|
401
|
+
eager,
|
|
402
|
+
} => op_compile_script(scope, context_id, source, filename, cached_data, produce_cache, eager),
|
|
403
|
+
Request::RunScript {
|
|
404
|
+
script_id,
|
|
405
|
+
timeout_ms,
|
|
406
|
+
} => op_run_script(scope, script_id, timeout_ms, outermost),
|
|
407
|
+
Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
|
|
408
|
+
// Serialize the script's CURRENT compile state. The stored handle is
|
|
409
|
+
// the UnboundScript, which V8 fills in with inner-function bytecode as
|
|
410
|
+
// run() lazily compiles them — so calling this after run() captures
|
|
411
|
+
// the functions that actually executed (a warm cache). None when V8
|
|
412
|
+
// can't serialize, or when the realm was reset/disposed out from under
|
|
413
|
+
// the script (its handle is gone): produce nil, not an error.
|
|
414
|
+
Request::ScriptCodeCache { script_id } => op_script_code_cache(scope, script_id),
|
|
415
|
+
// Same, for a module: get_unbound_module_script gives the shared
|
|
416
|
+
// compiled script, which evaluate() fills with inner-function bytecode.
|
|
417
|
+
// It needs the module's context entered (unlike UnboundScript), so
|
|
418
|
+
// a gone realm yields nil.
|
|
419
|
+
Request::ModuleCodeCache { module_id } => op_module_code_cache(scope, module_id),
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fn op_eval(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
424
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
|
|
425
|
+
let realm = context_for(istate!(scope), context_id);
|
|
426
|
+
match realm {
|
|
427
|
+
Some(ctx) => {
|
|
428
|
+
let context = v8::Local::new(scope, &ctx);
|
|
429
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
430
|
+
let out = run_source(scope, &source, &filename);
|
|
431
|
+
auto_drain(scope, outermost);
|
|
432
|
+
(true, out)
|
|
433
|
+
}
|
|
434
|
+
None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
VmReply::Done(outcome)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#[allow(clippy::too_many_arguments)]
|
|
441
|
+
fn op_call(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, name: String, args: Vec<JsVal>, void: bool, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
442
|
+
// A host fn invoked by the called function runs inline
|
|
443
|
+
// (host_fn_callback, with_gvl) — no routing setup needed.
|
|
444
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "call", |scope, outermost| {
|
|
445
|
+
let realm = context_for(istate!(scope), context_id);
|
|
446
|
+
match realm {
|
|
447
|
+
Some(ctx) => {
|
|
448
|
+
let context = v8::Local::new(scope, &ctx);
|
|
449
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
450
|
+
let out = call_function(scope, &name, args, void);
|
|
451
|
+
auto_drain(scope, outermost);
|
|
452
|
+
(true, out)
|
|
453
|
+
}
|
|
454
|
+
None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
VmReply::Done(outcome)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
fn op_drain_microtasks(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
|
|
461
|
+
// A microtask may call an attached host fn (a Promise .then ->
|
|
462
|
+
// ruby), which runs inline via host_fn_callback — no routing
|
|
463
|
+
// setup needed any more.
|
|
464
|
+
let watchdog = arm_watchdog(scope, timeout_ms);
|
|
465
|
+
let main = context_for(istate!(scope), 0);
|
|
466
|
+
if let Some(ctx) = main {
|
|
467
|
+
let context = v8::Local::new(scope, &ctx);
|
|
468
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
469
|
+
checkpoint_draining(scope);
|
|
470
|
+
}
|
|
471
|
+
let fired = disarm_watchdog(scope, watchdog);
|
|
472
|
+
if fired {
|
|
473
|
+
istate!(scope).watchdog_fired = true;
|
|
474
|
+
}
|
|
475
|
+
let outcome = if fired {
|
|
476
|
+
Err(VmError::Terminated)
|
|
477
|
+
} else {
|
|
478
|
+
Ok(JsVal::Undefined)
|
|
479
|
+
};
|
|
480
|
+
VmReply::Done(outcome)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
fn op_attach(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, name: String, host_fn_id: usize, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
484
|
+
// attach_at_path writes onto globalThis (and walks a dotted
|
|
485
|
+
// path), which can fire a user-defined accessor or Proxy trap —
|
|
486
|
+
// arbitrary JS. So it goes through the same bracket as Eval: a
|
|
487
|
+
// host fn the trap calls routes back, and a looping trap is
|
|
488
|
+
// time-capped.
|
|
489
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "attach", |scope, outermost| {
|
|
490
|
+
let realm = context_for(istate!(scope), context_id);
|
|
491
|
+
match realm {
|
|
492
|
+
Some(ctx) => {
|
|
493
|
+
let context = v8::Local::new(scope, &ctx);
|
|
494
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
495
|
+
let external = v8::External::new(scope, host_fn_id as *mut c_void);
|
|
496
|
+
let out = match v8::Function::builder(host_fn_callback)
|
|
497
|
+
.data(external.into())
|
|
498
|
+
.build(scope)
|
|
499
|
+
{
|
|
500
|
+
// A dotted name (e.g. "MiniRacer.foo") attaches
|
|
501
|
+
// under a namespace object, creating missing
|
|
502
|
+
// intermediates, so host fns needn't pollute the
|
|
503
|
+
// bare global.
|
|
504
|
+
Some(function) => attach_at_path(scope, context, &name, function),
|
|
505
|
+
None => Err(VmError::Runtime("failed to build function".into())),
|
|
506
|
+
};
|
|
507
|
+
auto_drain(scope, outermost);
|
|
508
|
+
(true, out)
|
|
509
|
+
}
|
|
510
|
+
None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
VmReply::Done(outcome)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn op_attach_many(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, entries: Vec<(String, usize)>, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
517
|
+
// Same as Attach (arbitrary JS via accessors/Proxy traps), but
|
|
518
|
+
// installs every entry under one bracket/drain. Applied in order;
|
|
519
|
+
// stops at the first failure and reports its (name-tagged) error.
|
|
520
|
+
// NOT transactional: entries before the failure stay attached —
|
|
521
|
+
// the realm is not rolled back (matches single Attach, which also
|
|
522
|
+
// commits its one write or fails it).
|
|
523
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "attach_many", |scope, outermost| {
|
|
524
|
+
let realm = context_for(istate!(scope), context_id);
|
|
525
|
+
match realm {
|
|
526
|
+
Some(ctx) => {
|
|
527
|
+
let context = v8::Local::new(scope, &ctx);
|
|
528
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
529
|
+
let mut out = Ok(JsVal::Undefined);
|
|
530
|
+
for (name, host_fn_id) in &entries {
|
|
531
|
+
let external = v8::External::new(scope, *host_fn_id as *mut c_void);
|
|
532
|
+
out = match v8::Function::builder(host_fn_callback)
|
|
533
|
+
.data(external.into())
|
|
534
|
+
.build(scope)
|
|
535
|
+
{
|
|
536
|
+
Some(function) => attach_at_path(scope, context, name, function),
|
|
537
|
+
None => Err(VmError::Runtime(format!(
|
|
538
|
+
"failed to build function for `{name}`"
|
|
539
|
+
))),
|
|
540
|
+
};
|
|
541
|
+
if out.is_err() {
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
auto_drain(scope, outermost);
|
|
546
|
+
(true, out)
|
|
547
|
+
}
|
|
548
|
+
None => (false, Err(VmError::Runtime("realm disposed or unknown".into()))),
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
VmReply::Done(outcome)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
fn op_reset(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
|
|
555
|
+
let known =
|
|
556
|
+
context_id == 0 || istate!(scope).realms.contexts.contains_key(&context_id);
|
|
557
|
+
if istate!(scope).draining {
|
|
558
|
+
// A microtask from ANY realm may be mid-flight on the stack;
|
|
559
|
+
// swapping a v8::Context out from under it corrupts state.
|
|
560
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
561
|
+
"cannot reset a realm during a microtask checkpoint".into(),
|
|
562
|
+
)))
|
|
563
|
+
} else if !known {
|
|
564
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
565
|
+
"context disposed or unknown".into(),
|
|
566
|
+
)))
|
|
567
|
+
} else if istate!(scope).active_realms.contains(&context_id) {
|
|
568
|
+
// Swapping the v8::Context behind a suspended frame would
|
|
569
|
+
// drop its in-flight modules/scripts and let the realm id
|
|
570
|
+
// refer to a different context than the one on the stack
|
|
571
|
+
// (defeating the cross-context import guards).
|
|
572
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
573
|
+
"cannot reset a realm while a request for it is suspended on the V8 stack"
|
|
574
|
+
.into(),
|
|
575
|
+
)))
|
|
576
|
+
} else {
|
|
577
|
+
let fresh = new_realm(scope, context_id);
|
|
578
|
+
{
|
|
579
|
+
let realms = &mut istate!(scope).realms;
|
|
580
|
+
if context_id == 0 {
|
|
581
|
+
realms.main_context = Some(fresh);
|
|
582
|
+
} else {
|
|
583
|
+
realms.contexts.insert(context_id, fresh);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Drop modules bound to this context — their realm just changed.
|
|
587
|
+
drop_context_artifacts(istate!(scope), context_id);
|
|
588
|
+
VmReply::Done(Ok(JsVal::Undefined))
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fn op_create_context(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
|
|
593
|
+
let id = {
|
|
594
|
+
let realms = &mut istate!(scope).realms;
|
|
595
|
+
let id = realms.next_context_id;
|
|
596
|
+
realms.next_context_id += 1;
|
|
597
|
+
id
|
|
598
|
+
};
|
|
599
|
+
let fresh = new_realm(scope, id);
|
|
600
|
+
istate!(scope).realms.contexts.insert(id, fresh);
|
|
601
|
+
VmReply::Done(Ok(JsVal::Int(id as i64)))
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
fn op_dispose_context(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32) -> VmReply {
|
|
605
|
+
if istate!(scope).draining {
|
|
606
|
+
// Same hazard as Reset: a microtask from any realm may be live.
|
|
607
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
608
|
+
"cannot dispose a realm during a microtask checkpoint".into(),
|
|
609
|
+
)))
|
|
610
|
+
} else if istate!(scope).active_realms.contains(&context_id) {
|
|
611
|
+
// Same hazard as Reset: a suspended frame still runs in it.
|
|
612
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
613
|
+
"cannot dispose a realm while a request for it is suspended on the V8 stack"
|
|
614
|
+
.into(),
|
|
615
|
+
)))
|
|
616
|
+
} else {
|
|
617
|
+
// Dropping the Global lets V8 collect the context. id 0 is the
|
|
618
|
+
// default context and never disposed independently.
|
|
619
|
+
istate!(scope).realms.contexts.remove(&context_id);
|
|
620
|
+
// Reclaim the modules compiled in it (else they leak until
|
|
621
|
+
// isolate teardown).
|
|
622
|
+
drop_context_artifacts(istate!(scope), context_id);
|
|
623
|
+
VmReply::Done(Ok(JsVal::Undefined))
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
#[allow(clippy::too_many_arguments)]
|
|
628
|
+
fn op_compile_module(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, cached_data: Option<Vec<u8>>, produce_cache: bool, eager: bool) -> VmReply {
|
|
629
|
+
let ctx = context_for(istate!(scope), context_id);
|
|
630
|
+
let outcome = match ctx {
|
|
631
|
+
None => Err(VmError::Runtime("context disposed or unknown".into())),
|
|
632
|
+
Some(cx) => {
|
|
633
|
+
let context = v8::Local::new(scope, &cx);
|
|
634
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
635
|
+
v8::tc_scope!(let tc, scope);
|
|
636
|
+
match v8::String::new(tc, &source) {
|
|
637
|
+
None => Err(VmError::Runtime("module source too large".into())),
|
|
638
|
+
Some(code) => {
|
|
639
|
+
let origin = module_origin(tc, &filename);
|
|
640
|
+
// Consume a supplied bytecode cache (skip reparse),
|
|
641
|
+
// eager-compile every function, or compile fresh
|
|
642
|
+
// (lazy). cached_data wins: V8 forbids combining
|
|
643
|
+
// ConsumeCodeCache with EagerCompile.
|
|
644
|
+
let (mut src, opts) = compile_source(code, &origin, &cached_data, eager);
|
|
645
|
+
let compiled = v8::script_compiler::compile_module2(
|
|
646
|
+
tc,
|
|
647
|
+
&mut src,
|
|
648
|
+
opts,
|
|
649
|
+
v8::script_compiler::NoCacheReason::NoReason,
|
|
650
|
+
);
|
|
651
|
+
match compiled {
|
|
652
|
+
Some(module) => {
|
|
653
|
+
// V8 marks a stale/incompatible supplied cache
|
|
654
|
+
// rejected; the embedder recompiles & re-caches.
|
|
655
|
+
let cache_rejected = cached_data.is_some()
|
|
656
|
+
&& src.get_cached_data().is_some_and(|c| c.rejected());
|
|
657
|
+
// Produce a fresh cache from the unbound script.
|
|
658
|
+
let produced = if produce_cache {
|
|
659
|
+
module
|
|
660
|
+
.get_unbound_module_script(tc)
|
|
661
|
+
.create_code_cache()
|
|
662
|
+
.map(|c| c.to_vec())
|
|
663
|
+
} else {
|
|
664
|
+
None
|
|
665
|
+
};
|
|
666
|
+
let hash = module.get_identity_hash().get();
|
|
667
|
+
let g = v8::Global::new(tc, module);
|
|
668
|
+
let id = {
|
|
669
|
+
let m = &mut istate!(tc).modules;
|
|
670
|
+
let id = m.next_id;
|
|
671
|
+
m.next_id += 1;
|
|
672
|
+
m.by_id
|
|
673
|
+
.insert(id, (g.clone(), filename.clone(), context_id));
|
|
674
|
+
m.by_hash.entry(hash).or_default().push((g, id));
|
|
675
|
+
id
|
|
676
|
+
};
|
|
677
|
+
Ok(Compiled {
|
|
678
|
+
id,
|
|
679
|
+
cached_data: produced,
|
|
680
|
+
cache_rejected,
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
684
|
+
// A module compile failure is a parse error
|
|
685
|
+
// (compile-time), not a thrown exception.
|
|
686
|
+
None => {
|
|
687
|
+
let msg = tc
|
|
688
|
+
.exception()
|
|
689
|
+
.map(|e| e.to_rust_string_lossy(tc))
|
|
690
|
+
.unwrap_or_else(|| "module parse error".to_string());
|
|
691
|
+
let message = tc.message();
|
|
692
|
+
let res = message
|
|
693
|
+
.and_then(|m| m.get_script_resource_name(tc))
|
|
694
|
+
.filter(|v| v.is_string())
|
|
695
|
+
.map(|v| v.to_rust_string_lossy(tc))
|
|
696
|
+
.unwrap_or_else(|| filename.clone());
|
|
697
|
+
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
698
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
699
|
+
None => format!(" at {res}"),
|
|
700
|
+
};
|
|
701
|
+
Err(VmError::Parse(format!("{msg}{loc}")))
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
VmReply::ModuleCompiled(outcome)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
712
|
+
// V8's module instantiation is NOT re-entrant: a nested
|
|
713
|
+
// instantiate issued from a resolve block walks the outer,
|
|
714
|
+
// half-built module graph and SEGVs the process. Refuse it
|
|
715
|
+
// cleanly — a resolve block may COMPILE dependencies lazily
|
|
716
|
+
// and return them; the outer instantiate links them.
|
|
717
|
+
if istate!(scope).instantiating {
|
|
718
|
+
VmReply::Done(Err(VmError::Runtime(
|
|
719
|
+
"instantiate is not re-entrant: another module is currently \
|
|
720
|
+
instantiating (compile the dependency and return it; the outer \
|
|
721
|
+
instantiate links it)"
|
|
722
|
+
.into(),
|
|
723
|
+
)))
|
|
724
|
+
} else {
|
|
725
|
+
istate!(scope).instantiating = true;
|
|
726
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
727
|
+
let outcome = match handle {
|
|
728
|
+
None => Err(VmError::Runtime("unknown module".into())),
|
|
729
|
+
Some((g, cid)) => match context_for(istate!(scope), cid) {
|
|
730
|
+
None => Err(VmError::Runtime("module's context is gone".into())),
|
|
731
|
+
Some(cx) => {
|
|
732
|
+
let context = v8::Local::new(scope, &cx);
|
|
733
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
734
|
+
let module = v8::Local::new(scope, &g);
|
|
735
|
+
match module.get_status() {
|
|
736
|
+
// Already linked (or further along): a no-op,
|
|
737
|
+
// not an error — instantiate is idempotent.
|
|
738
|
+
v8::ModuleStatus::Instantiated
|
|
739
|
+
| v8::ModuleStatus::Evaluating
|
|
740
|
+
| v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
741
|
+
// V8 CHECK-aborts on instantiating an errored
|
|
742
|
+
// module; surface its exception instead.
|
|
743
|
+
v8::ModuleStatus::Errored => Err(VmError::JsError {
|
|
744
|
+
message: module
|
|
745
|
+
.get_exception()
|
|
746
|
+
.to_rust_string_lossy(scope),
|
|
747
|
+
backtrace: vec![],
|
|
748
|
+
}),
|
|
749
|
+
_ => {
|
|
750
|
+
v8::tc_scope!(let tc, scope);
|
|
751
|
+
match module.instantiate_module(tc, resolve_imported) {
|
|
752
|
+
Some(true) => Ok(JsVal::Undefined),
|
|
753
|
+
_ if tc.has_terminated() => Err(VmError::Terminated),
|
|
754
|
+
// A resolver that RAISED is re-raised with its
|
|
755
|
+
// real class by instantiate_module (via the
|
|
756
|
+
// stashed exception); this generic link error
|
|
757
|
+
// is only used when no resolver exception was
|
|
758
|
+
// stashed.
|
|
759
|
+
_ => {
|
|
760
|
+
let exc = tc.exception();
|
|
761
|
+
let stack = tc.stack_trace();
|
|
762
|
+
Err(capture_js_error(tc, exc, stack))
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
istate!(scope).instantiating = false;
|
|
771
|
+
VmReply::Done(outcome)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn op_evaluate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
776
|
+
// Top-level module code (and, under :auto, the microtasks its
|
|
777
|
+
// TLA continuation drains) can loop, so it runs in the same
|
|
778
|
+
// watchdog bracket as Eval/Call/RunScript.
|
|
779
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "evaluate_module", |scope, outermost| {
|
|
780
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
781
|
+
match handle {
|
|
782
|
+
None => (false, Err(VmError::Runtime("unknown module".into()))),
|
|
783
|
+
Some((g, cid)) => match context_for(istate!(scope), cid) {
|
|
784
|
+
None => (false, Err(VmError::Runtime("module's context is gone".into()))),
|
|
785
|
+
Some(cx) => {
|
|
786
|
+
let context = v8::Local::new(scope, &cx);
|
|
787
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
788
|
+
let module = v8::Local::new(scope, &g);
|
|
789
|
+
// A top-level-await module's evaluate() returns a
|
|
790
|
+
// PENDING promise that only settles once the drain
|
|
791
|
+
// runs its continuation — remember it so we can read
|
|
792
|
+
// its post-drain state instead of reporting a stale Ok.
|
|
793
|
+
let mut eval_promise: Option<v8::Global<v8::Promise>> = None;
|
|
794
|
+
// ran_js is true ONLY for the Instantiated arm that
|
|
795
|
+
// actually calls evaluate(); the Errored/Evaluated/
|
|
796
|
+
// non-instantiated arms run no JS, so a raced watchdog
|
|
797
|
+
// must not override their real outcome to Terminated.
|
|
798
|
+
let mut did_eval = false;
|
|
799
|
+
// V8 CHECK-aborts the process if evaluate runs on a
|
|
800
|
+
// module that isn't exactly Instantiated, so guard
|
|
801
|
+
// status explicitly rather than crash.
|
|
802
|
+
let out = match module.get_status() {
|
|
803
|
+
v8::ModuleStatus::Errored => {
|
|
804
|
+
Err(VmError::JsError {
|
|
805
|
+
message: module
|
|
806
|
+
.get_exception()
|
|
807
|
+
.to_rust_string_lossy(scope),
|
|
808
|
+
backtrace: vec![],
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
812
|
+
v8::ModuleStatus::Instantiated => {
|
|
813
|
+
did_eval = true;
|
|
814
|
+
v8::tc_scope!(let tc, scope);
|
|
815
|
+
match module.evaluate(tc) {
|
|
816
|
+
// A synchronous top-level throw yields a
|
|
817
|
+
// *rejected* promise (not None); a pending
|
|
818
|
+
// (TLA) or fulfilled one is remembered and
|
|
819
|
+
// re-checked after the drain.
|
|
820
|
+
Some(value) => match v8::Local::<v8::Promise>::try_from(value) {
|
|
821
|
+
Ok(p) if p.state() == v8::PromiseState::Rejected => {
|
|
822
|
+
let reason = p.result(tc);
|
|
823
|
+
Err(VmError::JsError {
|
|
824
|
+
message: reason.to_rust_string_lossy(tc),
|
|
825
|
+
backtrace: vec![],
|
|
826
|
+
})
|
|
827
|
+
}
|
|
828
|
+
Ok(p) => {
|
|
829
|
+
eval_promise = Some(v8::Global::new(tc, p));
|
|
830
|
+
Ok(JsVal::Undefined)
|
|
831
|
+
}
|
|
832
|
+
_ => Ok(JsVal::Undefined),
|
|
833
|
+
},
|
|
834
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
835
|
+
None => {
|
|
836
|
+
let exc = tc.exception();
|
|
837
|
+
let stack = tc.stack_trace();
|
|
838
|
+
Err(capture_js_error(tc, exc, stack))
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
_ => Err(VmError::Runtime(
|
|
843
|
+
"module must be instantiated before evaluate".into(),
|
|
844
|
+
)),
|
|
845
|
+
};
|
|
846
|
+
auto_drain(scope, outermost);
|
|
847
|
+
// The drain may have settled a TLA module's promise to
|
|
848
|
+
// rejected — surface that instead of the provisional Ok.
|
|
849
|
+
let result = if let (true, Some(g)) = (out.is_ok(), eval_promise) {
|
|
850
|
+
let p = v8::Local::new(scope, &g);
|
|
851
|
+
if p.state() == v8::PromiseState::Rejected {
|
|
852
|
+
let reason = p.result(scope);
|
|
853
|
+
Err(VmError::JsError {
|
|
854
|
+
message: reason.to_rust_string_lossy(scope),
|
|
855
|
+
backtrace: vec![],
|
|
856
|
+
})
|
|
857
|
+
} else {
|
|
858
|
+
out
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
out
|
|
862
|
+
};
|
|
863
|
+
(did_eval, result)
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
VmReply::Done(outcome)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
872
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
873
|
+
let outcome = match handle {
|
|
874
|
+
None => Err(VmError::Runtime("unknown module".into())),
|
|
875
|
+
Some((g, cid)) => match context_for(istate!(scope), cid) {
|
|
876
|
+
None => Err(VmError::Runtime("module's context is gone".into())),
|
|
877
|
+
Some(cx) => {
|
|
878
|
+
let context = v8::Local::new(scope, &cx);
|
|
879
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
880
|
+
let module = v8::Local::new(scope, &g);
|
|
881
|
+
// get_module_namespace CHECK-aborts unless the module
|
|
882
|
+
// is at least Instantiated.
|
|
883
|
+
match module.get_status() {
|
|
884
|
+
v8::ModuleStatus::Uninstantiated
|
|
885
|
+
| v8::ModuleStatus::Instantiating => Err(VmError::Runtime(
|
|
886
|
+
"module must be instantiated before namespace".into(),
|
|
887
|
+
)),
|
|
888
|
+
_ => {
|
|
889
|
+
let ns = module.get_module_namespace();
|
|
890
|
+
Ok(js_to_jsval(scope, ns))
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
VmReply::Done(outcome)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
fn op_module_status(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
900
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
901
|
+
let outcome = match handle {
|
|
902
|
+
None => Err(VmError::Runtime("unknown module".into())),
|
|
903
|
+
Some((g, _cid)) => {
|
|
904
|
+
let module = v8::Local::new(scope, &g);
|
|
905
|
+
let name = match module.get_status() {
|
|
906
|
+
v8::ModuleStatus::Uninstantiated => "uninstantiated",
|
|
907
|
+
v8::ModuleStatus::Instantiating => "instantiating",
|
|
908
|
+
v8::ModuleStatus::Instantiated => "instantiated",
|
|
909
|
+
v8::ModuleStatus::Evaluating => "evaluating",
|
|
910
|
+
v8::ModuleStatus::Evaluated => "evaluated",
|
|
911
|
+
v8::ModuleStatus::Errored => "errored",
|
|
912
|
+
};
|
|
913
|
+
Ok(JsVal::Str(name.into()))
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
VmReply::Done(outcome)
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
920
|
+
let m = &mut istate!(scope).modules;
|
|
921
|
+
m.by_id.remove(&module_id);
|
|
922
|
+
for bucket in m.by_hash.values_mut() {
|
|
923
|
+
bucket.retain(|(_, id)| *id != module_id);
|
|
924
|
+
}
|
|
925
|
+
VmReply::Done(Ok(JsVal::Undefined))
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
#[allow(clippy::too_many_arguments)]
|
|
929
|
+
fn op_compile_script(scope: &mut v8::PinScope<'_, '_, ()>, context_id: i32, source: String, filename: String, cached_data: Option<Vec<u8>>, produce_cache: bool, eager: bool) -> VmReply {
|
|
930
|
+
let ctx = context_for(istate!(scope), context_id);
|
|
931
|
+
let outcome = match ctx {
|
|
932
|
+
None => Err(VmError::Runtime("context disposed or unknown".into())),
|
|
933
|
+
Some(cx) => {
|
|
934
|
+
let context = v8::Local::new(scope, &cx);
|
|
935
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
936
|
+
v8::tc_scope!(let tc, scope);
|
|
937
|
+
match v8::String::new(tc, &source) {
|
|
938
|
+
None => Err(VmError::Runtime("script source too large".into())),
|
|
939
|
+
Some(code) => {
|
|
940
|
+
let origin = script_origin(tc, &filename);
|
|
941
|
+
let (mut src, opts) = compile_source(code, &origin, &cached_data, eager);
|
|
942
|
+
match v8::script_compiler::compile_unbound_script(
|
|
943
|
+
tc,
|
|
944
|
+
&mut src,
|
|
945
|
+
opts,
|
|
946
|
+
v8::script_compiler::NoCacheReason::NoReason,
|
|
947
|
+
) {
|
|
948
|
+
Some(unbound) => {
|
|
949
|
+
let cache_rejected = cached_data.is_some()
|
|
950
|
+
&& src.get_cached_data().is_some_and(|c| c.rejected());
|
|
951
|
+
let produced = if produce_cache {
|
|
952
|
+
unbound.create_code_cache().map(|c| c.to_vec())
|
|
953
|
+
} else {
|
|
954
|
+
None
|
|
955
|
+
};
|
|
956
|
+
let g = v8::Global::new(tc, unbound);
|
|
957
|
+
let id = {
|
|
958
|
+
let s = &mut istate!(tc).scripts;
|
|
959
|
+
let id = s.next_id;
|
|
960
|
+
s.next_id += 1;
|
|
961
|
+
s.by_id.insert(id, (g, context_id));
|
|
962
|
+
id
|
|
963
|
+
};
|
|
964
|
+
Ok(Compiled {
|
|
965
|
+
id,
|
|
966
|
+
cached_data: produced,
|
|
967
|
+
cache_rejected,
|
|
968
|
+
})
|
|
969
|
+
}
|
|
970
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
971
|
+
// Compile failure = a parse error (with location).
|
|
972
|
+
None => {
|
|
973
|
+
let msg = tc
|
|
974
|
+
.exception()
|
|
975
|
+
.map(|e| e.to_rust_string_lossy(tc))
|
|
976
|
+
.unwrap_or_else(|| "script parse error".to_string());
|
|
977
|
+
let message = tc.message();
|
|
978
|
+
let res = message
|
|
979
|
+
.and_then(|m| m.get_script_resource_name(tc))
|
|
980
|
+
.filter(|v| v.is_string())
|
|
981
|
+
.map(|v| v.to_rust_string_lossy(tc))
|
|
982
|
+
.unwrap_or_else(|| filename.clone());
|
|
983
|
+
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
984
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
985
|
+
None => format!(" at {res}"),
|
|
986
|
+
};
|
|
987
|
+
Err(VmError::Parse(format!("{msg}{loc}")))
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
VmReply::ScriptCompiled(outcome)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
fn op_run_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32, timeout_ms: u64, outermost: bool) -> VmReply {
|
|
998
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "run_script", |scope, outermost| {
|
|
999
|
+
let handle = script_handle(istate!(scope), script_id);
|
|
1000
|
+
match handle {
|
|
1001
|
+
None => (false, Err(VmError::Runtime("unknown script".into()))),
|
|
1002
|
+
Some((g, cid)) => match context_for(istate!(scope), cid) {
|
|
1003
|
+
None => (false, Err(VmError::Runtime("script's context is gone".into()))),
|
|
1004
|
+
Some(cx) => {
|
|
1005
|
+
let context = v8::Local::new(scope, &cx);
|
|
1006
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1007
|
+
let unbound = v8::Local::new(scope, &g);
|
|
1008
|
+
let script = unbound.bind_to_current_context(scope);
|
|
1009
|
+
let out = {
|
|
1010
|
+
v8::tc_scope!(let tc, scope);
|
|
1011
|
+
match script.run(tc) {
|
|
1012
|
+
Some(value) => Ok(js_to_jsval(tc, value)),
|
|
1013
|
+
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
1014
|
+
None => {
|
|
1015
|
+
let exc = tc.exception();
|
|
1016
|
+
let stack = tc.stack_trace();
|
|
1017
|
+
Err(capture_js_error(tc, exc, stack))
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
auto_drain(scope, outermost);
|
|
1022
|
+
(true, out)
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
VmReply::Done(outcome)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn op_dispose_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
|
|
1031
|
+
istate!(scope).scripts.by_id.remove(&script_id);
|
|
1032
|
+
VmReply::Done(Ok(JsVal::Undefined))
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
fn op_script_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
|
|
1036
|
+
let handle = script_handle(istate!(scope), script_id);
|
|
1037
|
+
let outcome = match handle {
|
|
1038
|
+
None => Ok(None),
|
|
1039
|
+
Some((g, _cid)) => {
|
|
1040
|
+
let unbound = v8::Local::new(scope, &g);
|
|
1041
|
+
Ok(unbound.create_code_cache().map(|c| c.to_vec()))
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
VmReply::CodeCache(outcome)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
fn op_module_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1048
|
+
let mh = module_handle(istate!(scope), module_id);
|
|
1049
|
+
let handle = mh.and_then(|(g, cid)| context_for(istate!(scope), cid).map(|cx| (g, cx)));
|
|
1050
|
+
let outcome = match handle {
|
|
1051
|
+
None => Ok(None),
|
|
1052
|
+
Some((g, cx)) => {
|
|
1053
|
+
let context = v8::Local::new(scope, &cx);
|
|
1054
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1055
|
+
let module = v8::Local::new(scope, &g);
|
|
1056
|
+
let unbound = module.get_unbound_module_script(scope);
|
|
1057
|
+
Ok(unbound.create_code_cache().map(|c| c.to_vec()))
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
VmReply::CodeCache(outcome)
|
|
1061
|
+
}
|