rusty_racer 0.2.3 → 0.2.5
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 +18 -1
- data/ext/rusty_racer/Cargo.toml +6 -2
- data/ext/rusty_racer/src/lib.rs +612 -145
- data/ext/rusty_racer/src/ops.rs +153 -98
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +34 -9
- metadata +2 -2
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -31,28 +31,48 @@
|
|
|
31
31
|
// OUTERMOST op cancels a stale terminate so it can't poison the next op, while a
|
|
32
32
|
// nested op's cancel never erases a termination aimed at the suspended outer JS.
|
|
33
33
|
//
|
|
34
|
-
// Attached procs and the dynamic-import resolver are
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
34
|
+
// Attached procs and the dynamic-import resolver are kept alive by the ISOLATE'S
|
|
35
|
+
// OWN ROOTS ARRAY (Core.roots), which every wrapper marks — never by a GC root.
|
|
36
|
+
// A root would be a leak: a host fn closes over its embedder's world, and that
|
|
37
|
+
// world reaches back to this isolate's wrappers, so rooting the proc roots the
|
|
38
|
+
// whole cycle and mark-and-sweep can never break it (the wrapper's free would
|
|
39
|
+
// then never run, and the procs would never be released — one un-disposable
|
|
40
|
+
// isolate per embedder session). Marked from the wrappers instead, the cycle is
|
|
41
|
+
// ordinary Ruby garbage. `mark` PINS each entry (GC.compact must not move them:
|
|
42
|
+
// the copies living in Rust are invisible to the compactor and would go stale),
|
|
43
|
+
// which an RArray's own movable marking would not do.
|
|
44
|
+
//
|
|
45
|
+
// Two things stay OUTSIDE that array, deliberately:
|
|
46
|
+
// - the owner Thread (_owner_root) keeps its rb_gc_register_address, because the
|
|
47
|
+
// guarantee it buys has to outlive the last wrapper: Core::drop compares the
|
|
48
|
+
// raw owner VALUE when no wrapper is left to mark anything, and a freed Thread
|
|
49
|
+
// slot reused by a live thread would be a false owner match (see RootedThread).
|
|
50
|
+
// It is not the cycle above — a Thread reaches back to an isolate only if the
|
|
51
|
+
// embedder parks its world in that thread's thread-local storage.
|
|
52
|
+
// - an isolate whose last wrapper is collected on a thread OTHER than its owner
|
|
53
|
+
// still cannot be disposed (V8 forbids it) and is counted leaked, as it always
|
|
54
|
+
// was — see Core::drop. Collection now happens where it never used to, so that
|
|
55
|
+
// counter reports cases it used to miss entirely; disposing explicitly on the
|
|
56
|
+
// owner thread is still the only way to free a foreign-owned isolate promptly.
|
|
38
57
|
|
|
39
58
|
use std::cell::RefCell;
|
|
40
59
|
use std::collections::HashMap;
|
|
41
60
|
use std::ffi::c_void;
|
|
42
61
|
use std::ptr::null_mut;
|
|
43
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
62
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
44
63
|
use std::sync::{Arc, Mutex, Once, Weak};
|
|
45
64
|
|
|
46
65
|
use magnus::block::Proc;
|
|
47
|
-
use magnus::value::{BoxValue, ReprValue};
|
|
66
|
+
use magnus::value::{BoxValue, Opaque, ReprValue};
|
|
48
67
|
use magnus::{
|
|
49
|
-
Error, Exception, ExceptionClass, RHash, Ruby, TryConvert,
|
|
68
|
+
DataTypeFunctions, Error, Exception, ExceptionClass, RArray, RHash, Ruby, TryConvert,
|
|
69
|
+
TypedData, Value, function, gc, method, prelude::*,
|
|
50
70
|
};
|
|
51
71
|
|
|
52
72
|
mod marshal;
|
|
53
73
|
use marshal::{JsVal, js_to_jsval, jsval_to_js, jsval_to_ruby, ruby_to_jsval};
|
|
54
74
|
mod ops;
|
|
55
|
-
use ops::{Compiled, Request, VmReply, run_source, service_request};
|
|
75
|
+
use ops::{Compiled, Completion, Request, VmReply, run_source, service_request};
|
|
56
76
|
mod stack;
|
|
57
77
|
use stack::{STACK_DEBUG, StackScope, current_real_isolate, discover_scan_start_field};
|
|
58
78
|
mod watchdog;
|
|
@@ -62,10 +82,14 @@ use watchdog::{
|
|
|
62
82
|
|
|
63
83
|
// A Ruby Proc rooted for as long as the Core holds it. BoxValue registers a
|
|
64
84
|
// stable heap address with rb_gc_register_address, which both MARKS the proc
|
|
65
|
-
// (the extension may hold the only reference — e.g. attach("f", -> {...}))
|
|
66
85
|
// and PINS it (GC.compact must not move it: the copies living in Rust are
|
|
67
86
|
// invisible to the compactor and would go stale).
|
|
68
87
|
//
|
|
88
|
+
// Only for a proc the isolate holds TRANSIENTLY — today just the instantiate
|
|
89
|
+
// resolver, parked in the slot for the length of one InstantiateModule op and
|
|
90
|
+
// restored after it. Anything held for the isolate's LIFE goes in the roots
|
|
91
|
+
// array instead (see the module header): a lasting root is a lasting leak.
|
|
92
|
+
//
|
|
69
93
|
// SAFETY of the manual Send: the !Send contents (and BoxValue's drop, which
|
|
70
94
|
// calls rb_gc_unregister_address) only run under the GVL. Two conventions
|
|
71
95
|
// keep that true — breaking either is silent UB, so don't:
|
|
@@ -122,22 +146,58 @@ macro_rules! istate {
|
|
|
122
146
|
pub(crate) use istate;
|
|
123
147
|
|
|
124
148
|
// One attach()'d host fn: the realm it was attached into — so resetting or
|
|
125
|
-
// disposing that realm can release
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
149
|
+
// disposing that realm can release it — whether it is still attached, which
|
|
150
|
+
// occupant of the slot it is (`HostFnKey`), and the name it was attached under,
|
|
151
|
+
// kept only so a diagnostic can say which host function is involved (one String
|
|
152
|
+
// per attach, never per call). The PROC itself lives in the roots array at
|
|
153
|
+
// `proc_root_index(index)`, where the GC can see it without taking this lock.
|
|
129
154
|
struct ProcSlot {
|
|
130
155
|
context_id: i32,
|
|
131
|
-
|
|
156
|
+
live: bool,
|
|
157
|
+
generation: u32,
|
|
132
158
|
name: String,
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
//
|
|
161
|
+
// What a host function's V8 External carries: its slot, and the slot's generation
|
|
162
|
+
// when it was attached. A slot is reused once its realm is released, but a function
|
|
163
|
+
// of that realm can outlive the release — the embedder may still hold its document
|
|
164
|
+
// — so a call has to prove it is the slot's CURRENT occupant, or it would run
|
|
165
|
+
// whichever proc the slot was handed to next.
|
|
166
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
167
|
+
pub(crate) struct HostFnKey {
|
|
168
|
+
index: u32,
|
|
169
|
+
generation: u32,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
impl HostFnKey {
|
|
173
|
+
// (…both halves in one pointer-sized word: V8 builds for 64-bit targets alone.
|
|
174
|
+
// The word is stored as is only while V8's sandbox is off — the v8 crate's
|
|
175
|
+
// opt-in `v8_enable_sandbox`, which this crate leaves off; a sandboxed External
|
|
176
|
+
// goes through a pointer table that keeps tags in the high bits.)
|
|
177
|
+
pub(crate) fn to_external(self) -> *mut c_void {
|
|
178
|
+
const { assert!(usize::BITS >= 64) };
|
|
179
|
+
(((self.generation as u64) << 32) | self.index as u64) as usize as *mut c_void
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn from_external(value: *mut c_void) -> Self {
|
|
183
|
+
let value = value as usize as u64;
|
|
184
|
+
Self {
|
|
185
|
+
index: value as u32,
|
|
186
|
+
generation: (value >> 32) as u32,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn index(self) -> usize {
|
|
191
|
+
self.index as usize
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The isolate's host-fn registry, indexed by HostFnKey::index. `free` lists slots
|
|
136
196
|
// emptied by release_context_procs so a later attach reuses them instead of
|
|
137
197
|
// growing `slots` forever — a long-lived process that re-navigates iframe
|
|
138
|
-
// realms would otherwise leak one slot per attach.
|
|
139
|
-
//
|
|
140
|
-
//
|
|
198
|
+
// realms would otherwise leak one slot per attach. A reused slot's generation
|
|
199
|
+
// moves on, so a function of the released realm that is still reachable calls
|
|
200
|
+
// a key the table no longer answers (`current`) instead of the new occupant.
|
|
141
201
|
#[derive(Default)]
|
|
142
202
|
struct ProcTable {
|
|
143
203
|
slots: Vec<ProcSlot>,
|
|
@@ -145,30 +205,67 @@ struct ProcTable {
|
|
|
145
205
|
}
|
|
146
206
|
|
|
147
207
|
impl ProcTable {
|
|
148
|
-
// Take a free slot if one exists,
|
|
149
|
-
fn alloc(&mut self,
|
|
150
|
-
|
|
151
|
-
self.slots[
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
208
|
+
// Take a free slot if one exists, its generation the next, else grow.
|
|
209
|
+
fn alloc(&mut self, context_id: i32, name: String) -> HostFnKey {
|
|
210
|
+
let (index, generation) = match self.free.pop() {
|
|
211
|
+
Some(index) => (index, self.slots[index].generation.wrapping_add(1)),
|
|
212
|
+
None => {
|
|
213
|
+
self.slots.push(ProcSlot {
|
|
214
|
+
context_id,
|
|
215
|
+
live: false,
|
|
216
|
+
generation: 0,
|
|
217
|
+
name: String::new(),
|
|
218
|
+
});
|
|
219
|
+
(self.slots.len() - 1, 0)
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
self.slots[index] = ProcSlot {
|
|
223
|
+
context_id,
|
|
224
|
+
live: true,
|
|
225
|
+
generation,
|
|
226
|
+
name,
|
|
227
|
+
};
|
|
228
|
+
HostFnKey {
|
|
229
|
+
index: index as u32,
|
|
230
|
+
generation,
|
|
156
231
|
}
|
|
157
232
|
}
|
|
158
233
|
|
|
234
|
+
// Whether `key` is its slot's live occupant.
|
|
235
|
+
fn current(&self, key: HostFnKey) -> bool {
|
|
236
|
+
self.slots
|
|
237
|
+
.get(key.index())
|
|
238
|
+
.is_some_and(|slot| slot.live && slot.generation == key.generation)
|
|
239
|
+
}
|
|
240
|
+
|
|
159
241
|
// Release every live proc attached into |context_id| (its realm is gone),
|
|
160
|
-
// returning
|
|
161
|
-
//
|
|
162
|
-
|
|
242
|
+
// returning their ids so the caller can clear their roots-array entries (a Ruby
|
|
243
|
+
// write, which this table never does: it is locked on paths a GC must not have to
|
|
244
|
+
// wait behind) and only THEN put them on the free list. Idempotent: an
|
|
245
|
+
// already-released slot is skipped, so it can't be double-freed.
|
|
246
|
+
fn release(&mut self, context_id: i32) -> Vec<usize> {
|
|
247
|
+
let mut released = Vec::new();
|
|
163
248
|
for (id, slot) in self.slots.iter_mut().enumerate() {
|
|
164
|
-
if slot.context_id == context_id && slot.
|
|
165
|
-
slot.
|
|
166
|
-
|
|
249
|
+
if slot.context_id == context_id && slot.live {
|
|
250
|
+
slot.live = false;
|
|
251
|
+
released.push(id);
|
|
167
252
|
}
|
|
168
253
|
}
|
|
254
|
+
released
|
|
169
255
|
}
|
|
170
256
|
}
|
|
171
257
|
|
|
258
|
+
// The isolate's ROOTS ARRAY: every Ruby object the isolate must keep alive for as
|
|
259
|
+
// long as a wrapper of it is reachable, in one place the GC can be shown without
|
|
260
|
+
// taking any Rust lock (a mark that had to lock `procs` could deadlock against
|
|
261
|
+
// the thread whose allocation triggered the GC while holding it). Slot 0 is the
|
|
262
|
+
// dynamic-import resolver; the attached host fns follow, one per proc slot.
|
|
263
|
+
const ROOT_IMPORT_RESOLVER: isize = 0;
|
|
264
|
+
const ROOT_FIRST_PROC: isize = 1;
|
|
265
|
+
fn proc_root_index(index: usize) -> isize {
|
|
266
|
+
ROOT_FIRST_PROC + index as isize
|
|
267
|
+
}
|
|
268
|
+
|
|
172
269
|
// Look up a RustyRacer::<name> exception class at raise time. The classes are
|
|
173
270
|
// defined in lib/rusty_racer.rb (loaded after this extension), so they exist
|
|
174
271
|
// by the time any eval can raise. Falls back to Ruby's RuntimeError.
|
|
@@ -181,13 +278,18 @@ fn err_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
|
181
278
|
|
|
182
279
|
#[derive(Debug)]
|
|
183
280
|
enum VmError {
|
|
184
|
-
|
|
281
|
+
// compile-time failure -> RustyRacer::ParseError; the SyntaxError V8 made of
|
|
282
|
+
// it kept as for a JsError (`keep_thrown`)
|
|
283
|
+
Parse(String, Option<u64>),
|
|
185
284
|
Runtime(String), // internal failure (no JS stack) -> RustyRacer::RuntimeError
|
|
186
285
|
// A thrown JS exception: its message plus the JS stack frames, which become
|
|
187
|
-
// the Ruby exception's backtrace -> RustyRacer::RuntimeError
|
|
286
|
+
// the Ruby exception's backtrace -> RustyRacer::RuntimeError — and the value
|
|
287
|
+
// thrown, kept (`keep_thrown`), so a host fn the error propagates out of
|
|
288
|
+
// throws it on.
|
|
188
289
|
JsError {
|
|
189
290
|
message: String,
|
|
190
291
|
backtrace: Vec<String>,
|
|
292
|
+
thrown: Option<u64>,
|
|
191
293
|
},
|
|
192
294
|
Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
|
|
193
295
|
OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
|
|
@@ -290,9 +392,9 @@ where
|
|
|
290
392
|
// instead of saying "a host function". Rendered only on that path, so an
|
|
291
393
|
// ordinary callback pays for a pointer and nothing else.
|
|
292
394
|
enum RubyCall<'a> {
|
|
293
|
-
// An attach()'d host function, by
|
|
395
|
+
// An attach()'d host function, by its key — the name it was attached
|
|
294
396
|
// under lives in the isolate's proc table.
|
|
295
|
-
HostFn(
|
|
397
|
+
HostFn(HostFnKey),
|
|
296
398
|
// A module resolver (a static instantiate block, a dynamic auto-link, or
|
|
297
399
|
// import()), named by the specifier it was handed.
|
|
298
400
|
Resolver(&'a str),
|
|
@@ -301,15 +403,17 @@ enum RubyCall<'a> {
|
|
|
301
403
|
impl RubyCall<'_> {
|
|
302
404
|
fn describe(&self, core: &Core) -> String {
|
|
303
405
|
match self {
|
|
304
|
-
// try_lock, not lock: nothing holds this across a callback (call_proc
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
406
|
+
// try_lock, not lock: nothing holds this across a callback (`call_proc`
|
|
407
|
+
// holds it for one comparison, before the proc runs), but blocking here
|
|
408
|
+
// would turn a diagnostic into a hang. A slot handed on since names
|
|
409
|
+
// nobody: its name is another function's.
|
|
410
|
+
Self::HostFn(id) => match core.procs.try_lock().ok().and_then(|procs| {
|
|
411
|
+
procs
|
|
412
|
+
.slots
|
|
413
|
+
.get(id.index())
|
|
414
|
+
.filter(|slot| slot.generation == id.generation)
|
|
415
|
+
.map(|slot| slot.name.clone())
|
|
416
|
+
}) {
|
|
313
417
|
Some(name) => format!("The host function `{name}`"),
|
|
314
418
|
None => "A host function".to_string(),
|
|
315
419
|
},
|
|
@@ -506,8 +610,8 @@ fn host_fn_callback(
|
|
|
506
610
|
args: v8::FunctionCallbackArguments<'_>,
|
|
507
611
|
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
508
612
|
) {
|
|
509
|
-
let
|
|
510
|
-
Ok(e) => e.value()
|
|
613
|
+
let host_fn = match v8::Local::<v8::External>::try_from(args.data()) {
|
|
614
|
+
Ok(e) => HostFnKey::from_external(e.value()),
|
|
511
615
|
Err(_) => return,
|
|
512
616
|
};
|
|
513
617
|
let mut js_args = Vec::with_capacity(args.length() as usize);
|
|
@@ -534,20 +638,22 @@ fn host_fn_callback(
|
|
|
534
638
|
return;
|
|
535
639
|
}
|
|
536
640
|
let core = unsafe { &*core_ptr };
|
|
537
|
-
let result: Result<JsVal,
|
|
641
|
+
let result: Result<JsVal, HostFnFailure> = with_gvl(core, RubyCall::HostFn(host_fn), || {
|
|
538
642
|
let ruby = Ruby::get().unwrap();
|
|
539
643
|
// rb_protect so a Ruby exception raised during arg/return marshalling
|
|
540
644
|
// (ary_new_capa / jsval_to_ruby / ruby_to_jsval can raise on OOM etc.)
|
|
541
645
|
// is CAUGHT here instead of longjmp-ing through V8's C++ frames. The
|
|
542
646
|
// proc's own raise is already a magnus Err; this covers the rest.
|
|
543
647
|
use magnus::rb_sys::AsRawValue;
|
|
544
|
-
let mut out: Option<Result<JsVal,
|
|
648
|
+
let mut out: Option<Result<JsVal, HostFnFailure>> = None;
|
|
545
649
|
match magnus::rb_sys::protect(|| {
|
|
546
|
-
out = Some(core.call_proc(&ruby,
|
|
650
|
+
out = Some(core.call_proc(&ruby, host_fn, &js_args));
|
|
547
651
|
ruby.qnil().as_raw()
|
|
548
652
|
}) {
|
|
549
|
-
Ok(_) =>
|
|
550
|
-
|
|
653
|
+
Ok(_) => {
|
|
654
|
+
out.unwrap_or_else(|| Err("host function did not complete".to_string().into()))
|
|
655
|
+
}
|
|
656
|
+
Err(e) => Err(format!("{e}").into()),
|
|
551
657
|
}
|
|
552
658
|
});
|
|
553
659
|
match result {
|
|
@@ -555,8 +661,30 @@ fn host_fn_callback(
|
|
|
555
661
|
let v = jsval_to_js(scope, val);
|
|
556
662
|
rv.set(v);
|
|
557
663
|
}
|
|
558
|
-
// The proc raised (or marshalling failed): surface as a JS exception
|
|
559
|
-
|
|
664
|
+
// The proc raised (or marshalling failed): surface as a JS exception —
|
|
665
|
+
// the value JS threw, where the error is one a JS throw became.
|
|
666
|
+
Err(failure) => match failure.thrown.and_then(|serial| take_thrown(scope, serial)) {
|
|
667
|
+
Some(thrown) => {
|
|
668
|
+
scope.throw_exception(thrown);
|
|
669
|
+
}
|
|
670
|
+
None => throw_js_error(scope, &failure.message),
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// How a host fn failed: the Ruby error's text, and the serial of the JS value
|
|
676
|
+
// it carries when a JS throw is what it became (`keep_thrown`).
|
|
677
|
+
struct HostFnFailure {
|
|
678
|
+
message: String,
|
|
679
|
+
thrown: Option<u64>,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
impl From<String> for HostFnFailure {
|
|
683
|
+
fn from(message: String) -> Self {
|
|
684
|
+
HostFnFailure {
|
|
685
|
+
message,
|
|
686
|
+
thrown: None,
|
|
687
|
+
}
|
|
560
688
|
}
|
|
561
689
|
}
|
|
562
690
|
|
|
@@ -656,7 +784,87 @@ fn capture_js_error(
|
|
|
656
784
|
let backtrace = stack_str
|
|
657
785
|
.map(|s| parse_js_stack(s.as_str()))
|
|
658
786
|
.unwrap_or_default();
|
|
659
|
-
|
|
787
|
+
let thrown = keep_thrown(scope, exception);
|
|
788
|
+
VmError::JsError {
|
|
789
|
+
message,
|
|
790
|
+
backtrace,
|
|
791
|
+
thrown,
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// A value JS failed with that no TryCatch caught — an errored module's
|
|
796
|
+
// exception, a rejected promise's reason — as a JsError: its text, no stack
|
|
797
|
+
// frames, and the value kept as a thrown one is.
|
|
798
|
+
fn rejected_with(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>) -> VmError {
|
|
799
|
+
VmError::JsError {
|
|
800
|
+
message: value.to_rust_string_lossy(scope),
|
|
801
|
+
backtrace: vec![],
|
|
802
|
+
thrown: keep_thrown(scope, Some(value)),
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// A compile failure as a ParseError: V8's message, the location it recorded
|
|
807
|
+
// (always the file, the line when it has one), and the SyntaxError kept as a
|
|
808
|
+
// thrown value is.
|
|
809
|
+
fn parse_error(
|
|
810
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
811
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
812
|
+
message: Option<v8::Local<v8::Message>>,
|
|
813
|
+
filename: &str,
|
|
814
|
+
fallback: &str,
|
|
815
|
+
) -> VmError {
|
|
816
|
+
let msg = exception
|
|
817
|
+
.map(|e| e.to_rust_string_lossy(scope))
|
|
818
|
+
.unwrap_or_else(|| fallback.to_string());
|
|
819
|
+
let res = message
|
|
820
|
+
.and_then(|m| m.get_script_resource_name(scope))
|
|
821
|
+
.filter(|v| v.is_string())
|
|
822
|
+
.map(|v| v.to_rust_string_lossy(scope))
|
|
823
|
+
.unwrap_or_else(|| filename.to_string());
|
|
824
|
+
let loc = match message.and_then(|m| m.get_line_number(scope)) {
|
|
825
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
826
|
+
None => format!(" at {res}"),
|
|
827
|
+
};
|
|
828
|
+
VmError::Parse(format!("{msg}{loc}"), keep_thrown(scope, exception))
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Keep the value JS threw, so a host fn that lets the Ruby error it became
|
|
832
|
+
// propagate throws THAT value on — its identity, class and stack — rather than a
|
|
833
|
+
// fresh Error made of its text: a script a host fn ran for JS fails in JS as the
|
|
834
|
+
// script did. One slot per isolate, since only the latest thrown value can still
|
|
835
|
+
// be on its way back out, and only until the outermost op returns (`Core::run`),
|
|
836
|
+
// since only a host fn inside it can still ask — kept longer, it would pin the
|
|
837
|
+
// value, and its realm, for as long as nothing else threw. The serial (carried
|
|
838
|
+
// by the Ruby exception, see `carrying_thrown`) tells it from a stale one, and is
|
|
839
|
+
// drawn process-wide, so an error another isolate's op raised never matches this
|
|
840
|
+
// one's slot: its value cannot cross isolates. None on an isolate with no state
|
|
841
|
+
// (the snapshot creator's), where no host fn can ask.
|
|
842
|
+
fn keep_thrown(
|
|
843
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
844
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
845
|
+
) -> Option<u64> {
|
|
846
|
+
static SERIAL: AtomicU64 = AtomicU64::new(0);
|
|
847
|
+
let exception = exception?;
|
|
848
|
+
let kept = v8::Global::new(scope, exception);
|
|
849
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
850
|
+
let serial = SERIAL.fetch_add(1, Ordering::Relaxed) + 1;
|
|
851
|
+
state.thrown = Some((serial, kept));
|
|
852
|
+
Some(serial)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// The Ruby exception's kept thrown value (`keep_thrown`), when it still is the
|
|
856
|
+
// latest: taken, as it is thrown once.
|
|
857
|
+
fn take_thrown<'s>(
|
|
858
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
859
|
+
serial: u64,
|
|
860
|
+
) -> Option<v8::Local<'s, v8::Value>> {
|
|
861
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
862
|
+
let (kept_serial, _) = state.thrown.as_ref()?;
|
|
863
|
+
if *kept_serial != serial {
|
|
864
|
+
return None;
|
|
865
|
+
}
|
|
866
|
+
let (_, kept) = state.thrown.take()?;
|
|
867
|
+
Some(v8::Local::new(scope, kept))
|
|
660
868
|
}
|
|
661
869
|
|
|
662
870
|
// ---------------------------------------------------------------------------
|
|
@@ -824,6 +1032,8 @@ struct IsolateState {
|
|
|
824
1032
|
// the ScriptTerminatedError can name what was running. Taken (cleared) when the
|
|
825
1033
|
// terminated op's error is built; None for any non-timeout outcome.
|
|
826
1034
|
timeout_backtrace: Option<Vec<String>>,
|
|
1035
|
+
// The latest value JS threw out of an op, with its serial (`keep_thrown`).
|
|
1036
|
+
thrown: Option<(u64, v8::Global<v8::Value>)>,
|
|
827
1037
|
}
|
|
828
1038
|
|
|
829
1039
|
impl IsolateState {
|
|
@@ -848,6 +1058,7 @@ impl IsolateState {
|
|
|
848
1058
|
oom_fired: false,
|
|
849
1059
|
oom_initial_limit: 0,
|
|
850
1060
|
timeout_backtrace: None,
|
|
1061
|
+
thrown: None,
|
|
851
1062
|
}
|
|
852
1063
|
}
|
|
853
1064
|
}
|
|
@@ -933,7 +1144,7 @@ fn transfer_registry() -> &'static Mutex<HashMap<u64, SendBackingStore>> {
|
|
|
933
1144
|
// Monotonic token source. Starts at 1 so 0 is free as the JS-side "not
|
|
934
1145
|
// transferable — fall back to a copy" sentinel. A token is f64-exact for ~2^53
|
|
935
1146
|
// transfers, which a process will never reach.
|
|
936
|
-
static NEXT_TRANSFER_TOKEN:
|
|
1147
|
+
static NEXT_TRANSFER_TOKEN: AtomicU64 = AtomicU64::new(1);
|
|
937
1148
|
|
|
938
1149
|
// Drain EVERY realm's microtask queue once. Each realm has its own queue (see
|
|
939
1150
|
// V8State::queues), so a single scope.perform_microtask_checkpoint() — which only
|
|
@@ -1118,13 +1329,7 @@ fn resolve_imported<'s>(
|
|
|
1118
1329
|
}
|
|
1119
1330
|
}
|
|
1120
1331
|
None => {
|
|
1121
|
-
|
|
1122
|
-
.dynamic_import_resolver
|
|
1123
|
-
.lock()
|
|
1124
|
-
.unwrap()
|
|
1125
|
-
.as_ref()
|
|
1126
|
-
.map(|r| r.get());
|
|
1127
|
-
match resolver {
|
|
1332
|
+
match core.dynamic_import_resolver() {
|
|
1128
1333
|
Some(p) => match with_gvl(core, RubyCall::Resolver(&spec), || {
|
|
1129
1334
|
resolve_module_via_ruby(core, p, &spec, &ref_url, Some(here.unwrap_or(0)))
|
|
1130
1335
|
// Rendering the message reads the Ruby exception, so it too
|
|
@@ -1235,13 +1440,7 @@ fn dynamic_import_cb<'s>(
|
|
|
1235
1440
|
return Some(promise);
|
|
1236
1441
|
}
|
|
1237
1442
|
let core = unsafe { &*core_ptr };
|
|
1238
|
-
let
|
|
1239
|
-
.dynamic_import_resolver
|
|
1240
|
-
.lock()
|
|
1241
|
-
.unwrap()
|
|
1242
|
-
.as_ref()
|
|
1243
|
-
.map(|r| r.get());
|
|
1244
|
-
let id = match resolver_proc {
|
|
1443
|
+
let id = match core.dynamic_import_resolver() {
|
|
1245
1444
|
// A raising resolver only fails the import() (it rejects generically);
|
|
1246
1445
|
// it must NOT abort the surrounding eval, so swallow the Err here.
|
|
1247
1446
|
Some(p) => with_gvl(core, RubyCall::Resolver(&spec), || {
|
|
@@ -1500,10 +1699,16 @@ struct Core {
|
|
|
1500
1699
|
// stack (bootstrap via callback_scope! onto the ambient scope). Bumped around
|
|
1501
1700
|
// each `run`.
|
|
1502
1701
|
depth: std::sync::atomic::AtomicU32,
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
1702
|
+
// Everything this isolate keeps alive on the Ruby side — the attached host fns
|
|
1703
|
+
// and the dynamic-import resolver — in one Array the wrappers MARK (and pin).
|
|
1704
|
+
// See the module header for why these must not be GC roots; ROOT_* for the
|
|
1705
|
+
// layout. Read on the V8 side without the GVL (the import hooks), which is
|
|
1706
|
+
// sound because the entries are pinned and nothing here allocates or raises.
|
|
1707
|
+
roots: Opaque<RArray>,
|
|
1708
|
+
// A HostFnKey indexes ProcTable.slots — bookkeeping only (the proc itself is in
|
|
1709
|
+
// `roots`). Mutex (uncontended — single owner thread) so host_fn_callback can
|
|
1710
|
+
// reach it through Core (via the slot's core_ptr) while a &Core method also
|
|
1711
|
+
// holds it. reset/dispose releases the slots and recycles them.
|
|
1507
1712
|
procs: Mutex<ProcTable>,
|
|
1508
1713
|
// Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
|
|
1509
1714
|
// value overrides it. Guards against an in-V8 infinite loop without a watchdog.
|
|
@@ -1515,9 +1720,6 @@ struct Core {
|
|
|
1515
1720
|
// the ceiling after each OOM (to this when set, else V8's captured default — see
|
|
1516
1721
|
// oom_initial_limit). Space-axis twin of default_timeout_ms.
|
|
1517
1722
|
memory_limit: usize,
|
|
1518
|
-
// Set by Context#dynamic_import_resolver=; called for a JS import() to map
|
|
1519
|
-
// (specifier, referrer) to an already-loaded Module. GC-rooted like procs.
|
|
1520
|
-
dynamic_import_resolver: Mutex<Option<RootedProc>>,
|
|
1521
1723
|
// The watchdog (armed per timed op, fires TerminateExecution via the handle)
|
|
1522
1724
|
// and its thread's join handle, held here so dispose/Drop — which run on the
|
|
1523
1725
|
// owner thread with no scope — can stop and join it before the isolate drops.
|
|
@@ -1528,7 +1730,8 @@ struct Core {
|
|
|
1528
1730
|
// The V8 isolate (one per Isolate): lifecycle + the isolate-level ops
|
|
1529
1731
|
// (terminate, microtask checkpoint, dynamic import). eval/call/etc. live on
|
|
1530
1732
|
// Context, which an Isolate hands out (a v8::Context).
|
|
1531
|
-
#[
|
|
1733
|
+
#[derive(TypedData)]
|
|
1734
|
+
#[magnus(class = "RustyRacer::Isolate", mark)]
|
|
1532
1735
|
struct Isolate {
|
|
1533
1736
|
core: Arc<Core>,
|
|
1534
1737
|
}
|
|
@@ -1537,7 +1740,8 @@ struct Isolate {
|
|
|
1537
1740
|
// extra one (id >= 1, via Isolate#create_context). eval/call/attach/
|
|
1538
1741
|
// compile_module run here. Its own `disposed` is per-context; the Core's is
|
|
1539
1742
|
// isolate-level.
|
|
1540
|
-
#[
|
|
1743
|
+
#[derive(TypedData)]
|
|
1744
|
+
#[magnus(class = "RustyRacer::Context", mark)]
|
|
1541
1745
|
struct Context {
|
|
1542
1746
|
core: Arc<Core>,
|
|
1543
1747
|
id: i32,
|
|
@@ -1552,7 +1756,8 @@ struct Snapshot {
|
|
|
1552
1756
|
}
|
|
1553
1757
|
|
|
1554
1758
|
// Context#compile_module result: a handle to a V8 module (by id).
|
|
1555
|
-
#[
|
|
1759
|
+
#[derive(TypedData)]
|
|
1760
|
+
#[magnus(class = "RustyRacer::Module", mark)]
|
|
1556
1761
|
struct JsModule {
|
|
1557
1762
|
core: Arc<Core>,
|
|
1558
1763
|
module_id: i32,
|
|
@@ -1564,7 +1769,8 @@ struct JsModule {
|
|
|
1564
1769
|
}
|
|
1565
1770
|
|
|
1566
1771
|
// Context#compile result: a handle to a classic compiled script (by id).
|
|
1567
|
-
#[
|
|
1772
|
+
#[derive(TypedData)]
|
|
1773
|
+
#[magnus(class = "RustyRacer::Script", mark)]
|
|
1568
1774
|
struct Script {
|
|
1569
1775
|
core: Arc<Core>,
|
|
1570
1776
|
script_id: i32,
|
|
@@ -1573,11 +1779,49 @@ struct Script {
|
|
|
1573
1779
|
cache_rejected: bool,
|
|
1574
1780
|
}
|
|
1575
1781
|
|
|
1782
|
+
// The four wrappers that hold an Arc<Core> all mark the isolate's roots, because any
|
|
1783
|
+
// one of them may be the last one reachable: Ruby code is free to keep only the
|
|
1784
|
+
// Context a `Isolate#context` handed it, or only a Module, and the host fns must
|
|
1785
|
+
// stay callable through it. Marking the same array four times costs nothing (the GC
|
|
1786
|
+
// stops at an already-marked object); missing it once would free a live proc.
|
|
1787
|
+
macro_rules! marks_core_roots {
|
|
1788
|
+
($t:ty) => {
|
|
1789
|
+
impl DataTypeFunctions for $t {
|
|
1790
|
+
fn mark(&self, marker: &gc::Marker) {
|
|
1791
|
+
self.core.mark_roots(marker);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
marks_core_roots!(Isolate);
|
|
1797
|
+
marks_core_roots!(Context);
|
|
1798
|
+
marks_core_roots!(JsModule);
|
|
1799
|
+
marks_core_roots!(Script);
|
|
1800
|
+
|
|
1576
1801
|
// Set true once V8 is initialized; Platform.set_flags! refuses after that
|
|
1577
1802
|
// (flags must be set before V8::initialize), like mini_racer's
|
|
1578
1803
|
// PlatformAlreadyInitialized.
|
|
1579
1804
|
static V8_INITED: AtomicBool = AtomicBool::new(false);
|
|
1580
1805
|
|
|
1806
|
+
// The shared default platform, kept so ops can pump its foreground task queue (see
|
|
1807
|
+
// `platform()` / op_pump_message_loop). V8 posts FinalizationRegistry cleanup — and other
|
|
1808
|
+
// deferred foreground work — as platform tasks that only run when the embedder pumps the
|
|
1809
|
+
// message loop; without a handle to the platform we could never drive them. The platform is
|
|
1810
|
+
// process-global and thread-safe by V8's own contract (it is shared across every isolate on
|
|
1811
|
+
// every owner thread), which is what makes the SharedRef sound to park in a `static`.
|
|
1812
|
+
struct SharedPlatform(v8::SharedRef<v8::Platform>);
|
|
1813
|
+
// SAFETY: v8's default platform is explicitly designed to be shared across isolates and their
|
|
1814
|
+
// owner threads; PumpMessageLoop is called per-isolate on that isolate's owner thread. The
|
|
1815
|
+
// SharedRef is a refcounted handle to that one shared object.
|
|
1816
|
+
unsafe impl Send for SharedPlatform {}
|
|
1817
|
+
unsafe impl Sync for SharedPlatform {}
|
|
1818
|
+
static PLATFORM: std::sync::OnceLock<SharedPlatform> = std::sync::OnceLock::new();
|
|
1819
|
+
|
|
1820
|
+
// The shared platform, once V8 is initialized. `None` before init (no op can run then anyway).
|
|
1821
|
+
pub(crate) fn platform() -> Option<&'static v8::SharedRef<v8::Platform>> {
|
|
1822
|
+
PLATFORM.get().map(|p| &p.0)
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1581
1825
|
fn init_v8() {
|
|
1582
1826
|
static ONCE: Once = Once::new();
|
|
1583
1827
|
ONCE.call_once(|| {
|
|
@@ -1590,6 +1834,10 @@ fn init_v8() {
|
|
|
1590
1834
|
Ordering::Relaxed,
|
|
1591
1835
|
);
|
|
1592
1836
|
let platform = v8::new_default_platform(0, false).make_shared();
|
|
1837
|
+
// Keep a handle before handing ownership to V8, so ops can pump the foreground task
|
|
1838
|
+
// queue (FinalizationRegistry cleanup etc.). make_shared yields a refcounted SharedRef;
|
|
1839
|
+
// clone one into the static and give the other to V8.
|
|
1840
|
+
let _ = PLATFORM.set(SharedPlatform(platform.clone()));
|
|
1593
1841
|
v8::V8::initialize_platform(platform);
|
|
1594
1842
|
v8::V8::initialize();
|
|
1595
1843
|
V8_INITED.store(true, Ordering::SeqCst);
|
|
@@ -1927,8 +2175,22 @@ unsafe extern "C" fn promise_reject_cb(message: v8::PromiseRejectMessage) {
|
|
|
1927
2175
|
// reset and create_context so realms can't drift apart. Returns the context
|
|
1928
2176
|
// Global AND its dedicated microtask queue; the caller owns the queue in
|
|
1929
2177
|
// V8State alongside the context (see V8State::queues for why per-realm).
|
|
2178
|
+
// An embedder-supplied hook run in every realm just after the host namespace is
|
|
2179
|
+
// installed, with the fresh realm's scope and context. Set once (OnceLock) by an
|
|
2180
|
+
// embedder that links rusty_racer as a library; None for standalone gem use. A
|
|
2181
|
+
// plain fn pointer so it stays Send + Sync with no allocation.
|
|
2182
|
+
pub type RealmInitHook = fn(&mut v8::PinScope<'_, '_, ()>, &v8::Global<v8::Context>, i32);
|
|
2183
|
+
static REALM_INIT_HOOK: std::sync::OnceLock<RealmInitHook> = std::sync::OnceLock::new();
|
|
2184
|
+
|
|
2185
|
+
// Register the per-realm init hook. Idempotent-ish: the first call wins (later
|
|
2186
|
+
// calls are ignored), which suits a single embedder wiring it once at boot.
|
|
2187
|
+
pub fn set_realm_init_hook(hook: RealmInitHook) {
|
|
2188
|
+
let _ = REALM_INIT_HOOK.set(hook);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
1930
2191
|
fn new_realm(
|
|
1931
2192
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
2193
|
+
context_id: i32,
|
|
1932
2194
|
) -> (v8::Global<v8::Context>, v8::UniqueRef<v8::MicrotaskQueue>) {
|
|
1933
2195
|
// Explicit policy like the isolate's: rusty drives every drain by hand
|
|
1934
2196
|
// (auto_drain / NS.drainMicrotasks), so V8 must never auto-run this queue.
|
|
@@ -1980,6 +2242,15 @@ fn new_realm(
|
|
|
1980
2242
|
if let Some(name) = host_namespace {
|
|
1981
2243
|
install_host_namespace(scope, &fresh, &name);
|
|
1982
2244
|
}
|
|
2245
|
+
// Generic, DOM-agnostic extension seam: an embedder that links rusty_racer as a
|
|
2246
|
+
// library can register one hook (set_realm_init_hook) to run native setup in
|
|
2247
|
+
// every realm — including frame realms the engine creates internally. This is
|
|
2248
|
+
// how capybara-simulated installs its native DOM without rusty_racer knowing
|
|
2249
|
+
// anything about a DOM. Extension state lives in the embedder's OWN typed
|
|
2250
|
+
// isolate slot (rusty_v8 slots are keyed by TypeId), never in IsolateState.
|
|
2251
|
+
if let Some(hook) = REALM_INIT_HOOK.get() {
|
|
2252
|
+
hook(scope, &fresh, context_id);
|
|
2253
|
+
}
|
|
1983
2254
|
(fresh, queue)
|
|
1984
2255
|
}
|
|
1985
2256
|
|
|
@@ -2153,11 +2424,11 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
2153
2424
|
// leaves behind); nothing reads its completion value, so
|
|
2154
2425
|
// don't marshal it — a bundle ending in an expression must
|
|
2155
2426
|
// not fail the snapshot over a value no one wants.
|
|
2156
|
-
|
|
2427
|
+
Completion::Void,
|
|
2157
2428
|
)
|
|
2158
2429
|
{
|
|
2159
2430
|
err = Some(match e {
|
|
2160
|
-
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
2431
|
+
VmError::Parse(m, _) | VmError::Runtime(m) => m,
|
|
2161
2432
|
VmError::JsError { message, .. } => message,
|
|
2162
2433
|
VmError::Terminated => "snapshot code was terminated".to_string(),
|
|
2163
2434
|
// Unreachable: the snapshot-creator is a separate isolate
|
|
@@ -2193,13 +2464,13 @@ impl Isolate {
|
|
|
2193
2464
|
// Core keeps its id + a stable raw ptr so `run` can open scopes on it. The
|
|
2194
2465
|
// isolate is thread-bound from here on (every op asserts the owner thread).
|
|
2195
2466
|
fn new(
|
|
2196
|
-
|
|
2467
|
+
ruby: &Ruby,
|
|
2197
2468
|
host_namespace: Option<String>,
|
|
2198
2469
|
snapshot: Option<magnus::typed_data::Obj<Snapshot>>,
|
|
2199
2470
|
timeout_ms: u64,
|
|
2200
2471
|
memory_limit: usize,
|
|
2201
2472
|
explicit_microtasks: bool,
|
|
2202
|
-
) -> Result<Self
|
|
2473
|
+
) -> Result<magnus::typed_data::Obj<Self>, Error> {
|
|
2203
2474
|
init_v8();
|
|
2204
2475
|
// A snapshot blob bakes globalThis state in: the first Context::new (in
|
|
2205
2476
|
// new_realm below) deserializes that default context for free.
|
|
@@ -2245,7 +2516,7 @@ impl Isolate {
|
|
|
2245
2516
|
// namespace from the slot (seeded above).
|
|
2246
2517
|
{
|
|
2247
2518
|
v8::scope!(let scope, &mut isolate);
|
|
2248
|
-
let (main_context, main_queue) = new_realm(scope);
|
|
2519
|
+
let (main_context, main_queue) = new_realm(scope, 0);
|
|
2249
2520
|
istate!(scope).realms.main_context = Some(main_context);
|
|
2250
2521
|
istate!(scope).realms.main_queue = Some(main_queue);
|
|
2251
2522
|
// The shared graveyard for retired realms' contexts (see V8State).
|
|
@@ -2275,6 +2546,10 @@ impl Isolate {
|
|
|
2275
2546
|
// owner check.
|
|
2276
2547
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
2277
2548
|
let owner_thread = unsafe { Value::from_raw(rb_sys::rb_thread_current()) };
|
|
2549
|
+
// The roots array (see ROOT_IMPORT_RESOLVER): created empty, grown by the
|
|
2550
|
+
// first attach. Nothing roots it — the wrappers mark it, so it lives
|
|
2551
|
+
// exactly as long as this isolate is reachable from Ruby.
|
|
2552
|
+
let roots = ruby.ary_new();
|
|
2278
2553
|
let core = Arc::new_cyclic(|me| Core {
|
|
2279
2554
|
me: me.clone(),
|
|
2280
2555
|
shared: Mutex::new(Shared {
|
|
@@ -2288,10 +2563,10 @@ impl Isolate {
|
|
|
2288
2563
|
scan_start_field: std::sync::atomic::AtomicUsize::new(0),
|
|
2289
2564
|
installed_stack_limit: std::sync::atomic::AtomicUsize::new(0),
|
|
2290
2565
|
depth: std::sync::atomic::AtomicU32::new(0),
|
|
2566
|
+
roots: Opaque::from(roots),
|
|
2291
2567
|
procs: Mutex::new(ProcTable::default()),
|
|
2292
2568
|
default_timeout_ms: timeout_ms,
|
|
2293
2569
|
memory_limit,
|
|
2294
|
-
dynamic_import_resolver: Mutex::new(None),
|
|
2295
2570
|
watchdog,
|
|
2296
2571
|
watchdog_join: Mutex::new(Some(watchdog_join)),
|
|
2297
2572
|
});
|
|
@@ -2314,7 +2589,14 @@ impl Isolate {
|
|
|
2314
2589
|
// enter/exit stack (an out-of-order drop aborts). Instead each op enters
|
|
2315
2590
|
// around its run (Core::run) and teardown re-enters just before drop.
|
|
2316
2591
|
unsafe { (*core.iso_ptr.0).exit() };
|
|
2317
|
-
|
|
2592
|
+
// Wrap HERE rather than returning `Self` for magnus to wrap after this frame is
|
|
2593
|
+
// gone: wrapping allocates, and until the wrapper exists the roots array is
|
|
2594
|
+
// referenced only from the Arc<Core>'s malloc'd block, which Ruby does not scan.
|
|
2595
|
+
// `black_box` keeps the local VALUE live across that allocation — where the
|
|
2596
|
+
// conservative stack scan does reach it.
|
|
2597
|
+
let wrapped = ruby.obj_wrap(Isolate { core });
|
|
2598
|
+
std::hint::black_box(roots);
|
|
2599
|
+
Ok(wrapped)
|
|
2318
2600
|
}
|
|
2319
2601
|
}
|
|
2320
2602
|
|
|
@@ -2530,6 +2812,9 @@ impl Core {
|
|
|
2530
2812
|
// on an op.
|
|
2531
2813
|
if reply.is_none() {
|
|
2532
2814
|
self.shared.lock().unwrap().disposed = true;
|
|
2815
|
+
} else if depth == 0 {
|
|
2816
|
+
// No host fn is left to throw a kept value on (`keep_thrown`).
|
|
2817
|
+
istate!(unsafe { &mut *iso }).thrown = None;
|
|
2533
2818
|
}
|
|
2534
2819
|
self.depth.fetch_sub(1, Ordering::SeqCst);
|
|
2535
2820
|
reply
|
|
@@ -2578,16 +2863,76 @@ impl Core {
|
|
|
2578
2863
|
.clone()
|
|
2579
2864
|
}
|
|
2580
2865
|
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2866
|
+
// ── the roots array (see the module header + ROOT_IMPORT_RESOLVER) ──────
|
|
2867
|
+
// Keep `val` alive for as long as this isolate is reachable from Ruby. On a
|
|
2868
|
+
// Ruby thread with the GVL: storing can grow the Array, which allocates.
|
|
2869
|
+
fn root_store<T: magnus::IntoValue>(
|
|
2870
|
+
&self,
|
|
2871
|
+
ruby: &Ruby,
|
|
2872
|
+
index: isize,
|
|
2873
|
+
val: T,
|
|
2874
|
+
) -> Result<(), Error> {
|
|
2875
|
+
ruby.get_inner(self.roots).store(index, val)
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// One entry, or None where nothing is stored (an unset resolver, a released
|
|
2879
|
+
// host fn). Reads the Array WITHOUT the GVL — V8 calls the import hooks with it
|
|
2880
|
+
// released — so it must not run one line of Ruby: `rb_ary_entry` plus
|
|
2881
|
+
// `Proc::from_value` (rb_obj_is_proc) and nothing else. NOT `RArray::entry`,
|
|
2882
|
+
// whose TryConvert would answer a nil slot by CALLING `nil.to_proc`, raising
|
|
2883
|
+
// NoMethodError and rescuing it — Ruby code, an allocation and a longjmp, with
|
|
2884
|
+
// no GVL held (measured: it corrupts the VM — "[BUG] unexpected situation" then
|
|
2885
|
+
// a SEGV — under any concurrent Ruby thread). The entries are pinned by
|
|
2886
|
+
// `mark_roots`, so the VALUE stays put until the caller has it under the GVL.
|
|
2887
|
+
fn root_proc(&self, index: isize) -> Option<Proc> {
|
|
2888
|
+
let roots = unsafe { Ruby::get_unchecked() }.get_inner(self.roots);
|
|
2889
|
+
Proc::from_value(roots.entry::<Value>(index).ok()?)
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Show the GC everything this isolate holds. Called from EVERY wrapper's mark
|
|
2893
|
+
// (Isolate, Context, Module, Script all keep an Arc<Core>, and any one of them
|
|
2894
|
+
// may be the last reachable): a wrapper that forgot to would leave a live
|
|
2895
|
+
// isolate's host fns unmarked. `mark` PINS — an Array marks its own elements
|
|
2896
|
+
// MOVABLE, and the copies Rust takes out of it (call_proc, root_proc) would go
|
|
2897
|
+
// stale if the compactor moved one. Takes no Rust lock, by construction.
|
|
2898
|
+
fn mark_roots(&self, marker: &gc::Marker) {
|
|
2899
|
+
let roots = unsafe { Ruby::get_unchecked() }.get_inner(self.roots);
|
|
2900
|
+
marker.mark(roots);
|
|
2901
|
+
// Entry by entry, exactly — not `mark_slice`, whose rb_gc_mark_locations is
|
|
2902
|
+
// CONSERVATIVE (it tests each word for heap-likeness) and measured no faster
|
|
2903
|
+
// here. The walk is O(entries) per wrapper per GC, MINOR GCs included (these
|
|
2904
|
+
// wrappers are not wb_protected, so they are marked every time): measured
|
|
2905
|
+
// 2026-09-13 with 600 procs, ~0.1 ms per GC at the 18 wrappers a real embedder
|
|
2906
|
+
// session holds (capybara-simulated: 1 Isolate + 17 Contexts), ~3 ms at 500. If
|
|
2907
|
+
// that ever bites, the fix is for the derived wrappers to mark the ISOLATE'S
|
|
2908
|
+
// Ruby object and let its own mark walk the array once — which needs Core to
|
|
2909
|
+
// hold that object (an Opaque<Value> nothing stores today), not just a rewrite
|
|
2910
|
+
// of this loop.
|
|
2911
|
+
for i in 0..roots.len() {
|
|
2912
|
+
if let Ok(v) = roots.entry::<Value>(i as isize) {
|
|
2913
|
+
marker.mark(v);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
fn call_proc(
|
|
2919
|
+
&self,
|
|
2920
|
+
ruby: &Ruby,
|
|
2921
|
+
host_fn: HostFnKey,
|
|
2922
|
+
args: &[JsVal],
|
|
2923
|
+
) -> Result<JsVal, HostFnFailure> {
|
|
2924
|
+
// A function of a released realm is refused here, before its slot's roots
|
|
2925
|
+
// entry is read: that entry may by now hold another realm's proc. (The lock
|
|
2926
|
+
// is uncontended and held for the comparison alone — nothing here allocates,
|
|
2927
|
+
// so no GC can need it meanwhile.)
|
|
2928
|
+
if !self.procs.lock().unwrap().current(host_fn) {
|
|
2929
|
+
return Err("unknown host function".to_string().into());
|
|
2930
|
+
}
|
|
2931
|
+
// `root_proc` and not `RArray::entry`, for the reason given there: a nil
|
|
2932
|
+
// slot must answer None, never dispatch `to_proc` and raise.
|
|
2933
|
+
let proc = self
|
|
2934
|
+
.root_proc(proc_root_index(host_fn.index()))
|
|
2935
|
+
.ok_or_else(|| "unknown host function".to_string())?;
|
|
2591
2936
|
// Marshal into a Ruby Array, NOT a Vec<Value>: bare Values in a heap Vec
|
|
2592
2937
|
// are hidden from Ruby's GC mark phase (magnus's own RArray::to_vec doc
|
|
2593
2938
|
// spells this out). With several args, once arg N is parked in the Vec
|
|
@@ -2607,8 +2952,11 @@ impl Core {
|
|
|
2607
2952
|
// op the proc issues re-enters Core::run (depth > 0) directly — no nested
|
|
2608
2953
|
// frame bookkeeping is needed any more (the call stack IS the nesting).
|
|
2609
2954
|
let result: Result<Value, Error> = proc.call(unsafe { ruby_args.as_slice() });
|
|
2610
|
-
let value = result.map_err(|e|
|
|
2611
|
-
|
|
2955
|
+
let value = result.map_err(|e| HostFnFailure {
|
|
2956
|
+
message: e.to_string(),
|
|
2957
|
+
thrown: thrown_serial_of(&e),
|
|
2958
|
+
})?;
|
|
2959
|
+
ruby_to_jsval(value).map_err(|e| e.to_string().into())
|
|
2612
2960
|
}
|
|
2613
2961
|
|
|
2614
2962
|
// Context#call (and call_void). Resolves a dotted function path
|
|
@@ -2694,6 +3042,23 @@ impl Core {
|
|
|
2694
3042
|
Ok(())
|
|
2695
3043
|
}
|
|
2696
3044
|
|
|
3045
|
+
// Isolate#pump_message_loop: run every pending foreground platform task, without blocking.
|
|
3046
|
+
// These are the deferred tasks V8 posts to the platform's task runner rather than running
|
|
3047
|
+
// inline — most importantly FinalizationRegistry cleanup callbacks (posted after a GC finds a
|
|
3048
|
+
// registered target dead) and WeakRef clearing. A pure microtask checkpoint does NOT drive
|
|
3049
|
+
// them; only pumping the message loop does. Returns nothing; safe to call any time (a no-op
|
|
3050
|
+
// when the queue is empty).
|
|
3051
|
+
fn pump_message_loop(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
3052
|
+
let reply = self.run(
|
|
3053
|
+
ruby,
|
|
3054
|
+
Request::PumpMessageLoop {
|
|
3055
|
+
timeout_ms: self.default_timeout_ms,
|
|
3056
|
+
},
|
|
3057
|
+
)?;
|
|
3058
|
+
self.reply_value(ruby, reply)?;
|
|
3059
|
+
Ok(())
|
|
3060
|
+
}
|
|
3061
|
+
|
|
2697
3062
|
fn eval_t(
|
|
2698
3063
|
&self,
|
|
2699
3064
|
ruby: &Ruby,
|
|
@@ -2701,7 +3066,7 @@ impl Core {
|
|
|
2701
3066
|
source: String,
|
|
2702
3067
|
filename: String,
|
|
2703
3068
|
timeout_ms: u64,
|
|
2704
|
-
|
|
3069
|
+
completion: Completion,
|
|
2705
3070
|
) -> Result<Value, Error> {
|
|
2706
3071
|
let reply = self.run(
|
|
2707
3072
|
ruby,
|
|
@@ -2710,7 +3075,7 @@ impl Core {
|
|
|
2710
3075
|
source,
|
|
2711
3076
|
filename,
|
|
2712
3077
|
timeout_ms,
|
|
2713
|
-
|
|
3078
|
+
completion,
|
|
2714
3079
|
},
|
|
2715
3080
|
)?;
|
|
2716
3081
|
self.reply_value(ruby, reply)
|
|
@@ -2723,17 +3088,18 @@ impl Core {
|
|
|
2723
3088
|
name: String,
|
|
2724
3089
|
proc: Proc,
|
|
2725
3090
|
) -> Result<Value, Error> {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
3091
|
+
// BEFORE the roots write, not just inside `run` below: storing can REALLOC the
|
|
3092
|
+
// array's element storage, which the import hooks read with the GVL released —
|
|
3093
|
+
// and a refused attach must not consume a slot or leave its proc rooted either.
|
|
3094
|
+
self.ensure_owner_and_live(ruby)?;
|
|
3095
|
+
let host_fn = self.procs.lock().unwrap().alloc(context_id, name.clone());
|
|
3096
|
+
self.root_store(ruby, proc_root_index(host_fn.index()), proc)?;
|
|
2731
3097
|
let reply = self.run(
|
|
2732
3098
|
ruby,
|
|
2733
3099
|
Request::Attach {
|
|
2734
3100
|
context_id,
|
|
2735
3101
|
name,
|
|
2736
|
-
|
|
3102
|
+
host_fn,
|
|
2737
3103
|
timeout_ms: self.default_timeout_ms,
|
|
2738
3104
|
},
|
|
2739
3105
|
)?;
|
|
@@ -2742,7 +3108,7 @@ impl Core {
|
|
|
2742
3108
|
|
|
2743
3109
|
// attach_many: install several host fns in ONE round-trip to the V8 thread
|
|
2744
3110
|
// (a fresh realm needs ~dozens; one rendezvous instead of one per fn). Slots
|
|
2745
|
-
// are allocated up front so each carries a stable
|
|
3111
|
+
// are allocated up front so each carries a stable key; entries are
|
|
2746
3112
|
// applied IN ORDER and a build/attach failure aborts the batch with that
|
|
2747
3113
|
// (name-tagged) error WITHOUT rolling back earlier entries (see the
|
|
2748
3114
|
// AttachMany arm). On that error path the unused slots are reclaimed at the
|
|
@@ -2756,38 +3122,50 @@ impl Core {
|
|
|
2756
3122
|
if entries.is_empty() {
|
|
2757
3123
|
return Ok(ruby.qnil().as_value()); // nothing to install, skip the round-trip
|
|
2758
3124
|
}
|
|
2759
|
-
|
|
3125
|
+
self.ensure_owner_and_live(ruby)?; // before the roots writes — see `attach`
|
|
3126
|
+
let allocated: Vec<(String, HostFnKey, Proc)> = {
|
|
2760
3127
|
let mut procs = self.procs.lock().unwrap();
|
|
2761
3128
|
entries
|
|
2762
3129
|
.into_iter()
|
|
2763
|
-
.map(|(name, proc)|
|
|
2764
|
-
let id = procs.alloc(ProcSlot {
|
|
2765
|
-
context_id,
|
|
2766
|
-
proc: Some(RootedProc(BoxValue::new(proc))),
|
|
2767
|
-
name: name.clone(),
|
|
2768
|
-
});
|
|
2769
|
-
(name, id)
|
|
2770
|
-
})
|
|
3130
|
+
.map(|(name, proc)| (name.clone(), procs.alloc(context_id, name), proc))
|
|
2771
3131
|
.collect()
|
|
2772
3132
|
};
|
|
3133
|
+
// Stored OUTSIDE the table's lock: storing into the array can allocate (the
|
|
3134
|
+
// Array grows), and an allocation may GC — which marks through this isolate's
|
|
3135
|
+
// wrappers, a path that must never wait on `procs`. The Procs parked in the
|
|
3136
|
+
// Vec meanwhile are invisible to the GC (see call_proc's note), but the caller's
|
|
3137
|
+
// `table: RHash` argument is live in its own frame and marks every one of them.
|
|
3138
|
+
let mut named_keys: Vec<(String, HostFnKey)> = Vec::with_capacity(allocated.len());
|
|
3139
|
+
for (name, key, proc) in allocated {
|
|
3140
|
+
self.root_store(ruby, proc_root_index(key.index()), proc)?;
|
|
3141
|
+
named_keys.push((name, key));
|
|
3142
|
+
}
|
|
2773
3143
|
let reply = self.run(
|
|
2774
3144
|
ruby,
|
|
2775
3145
|
Request::AttachMany {
|
|
2776
3146
|
context_id,
|
|
2777
|
-
entries:
|
|
3147
|
+
entries: named_keys,
|
|
2778
3148
|
timeout_ms: self.default_timeout_ms,
|
|
2779
3149
|
},
|
|
2780
3150
|
)?;
|
|
2781
3151
|
self.reply_value(ruby, reply)
|
|
2782
3152
|
}
|
|
2783
3153
|
|
|
2784
|
-
//
|
|
2785
|
-
// realm is gone (reset or disposed),
|
|
2786
|
-
//
|
|
2787
|
-
//
|
|
2788
|
-
//
|
|
2789
|
-
|
|
2790
|
-
|
|
3154
|
+
// Drop the procs attached into |context_id| out of the roots array — its
|
|
3155
|
+
// realm is gone (reset or disposed), and holding them would keep the embedder's
|
|
3156
|
+
// world alive for the rest of the isolate's life. A V8 function of that realm
|
|
3157
|
+
// may still be reachable (the embedder can hold the old global), but its key
|
|
3158
|
+
// stops answering (`ProcTable::current`), so it never calls a proc again.
|
|
3159
|
+
// Runs on a Ruby thread: it writes nil into the Array. The slots stay: the
|
|
3160
|
+
// keys of other realms are indices into the same Vec.
|
|
3161
|
+
fn release_context_procs(&self, ruby: &Ruby, context_id: i32) {
|
|
3162
|
+
// Nil the entries BEFORE the ids go back on the free list, so no attach can
|
|
3163
|
+
// ever be handed an id whose old proc is still in the array.
|
|
3164
|
+
let released = self.procs.lock().unwrap().release(context_id);
|
|
3165
|
+
for &id in &released {
|
|
3166
|
+
let _ = self.root_store(ruby, proc_root_index(id), ruby.qnil());
|
|
3167
|
+
}
|
|
3168
|
+
self.procs.lock().unwrap().free.extend(released);
|
|
2791
3169
|
}
|
|
2792
3170
|
|
|
2793
3171
|
fn reset(&self, ruby: &Ruby, context_id: i32) -> Result<Value, Error> {
|
|
@@ -2795,7 +3173,7 @@ impl Core {
|
|
|
2795
3173
|
let out = self.reply_value(ruby, reply)?;
|
|
2796
3174
|
// Only on success — a refused reset (unknown/suspended realm) keeps
|
|
2797
3175
|
// its attached fns callable.
|
|
2798
|
-
self.release_context_procs(context_id);
|
|
3176
|
+
self.release_context_procs(ruby, context_id);
|
|
2799
3177
|
Ok(out)
|
|
2800
3178
|
}
|
|
2801
3179
|
|
|
@@ -2809,7 +3187,7 @@ impl Core {
|
|
|
2809
3187
|
fn dispose_context(&self, ruby: &Ruby, context_id: i32) -> Result<(), Error> {
|
|
2810
3188
|
let reply = self.run(ruby, Request::DisposeContext { context_id })?;
|
|
2811
3189
|
self.reply_value(ruby, reply)?;
|
|
2812
|
-
self.release_context_procs(context_id);
|
|
3190
|
+
self.release_context_procs(ruby, context_id);
|
|
2813
3191
|
Ok(())
|
|
2814
3192
|
}
|
|
2815
3193
|
|
|
@@ -2892,12 +3270,18 @@ impl Core {
|
|
|
2892
3270
|
self.reply_value(ruby, reply?)
|
|
2893
3271
|
}
|
|
2894
3272
|
|
|
2895
|
-
fn evaluate_module(
|
|
3273
|
+
fn evaluate_module(
|
|
3274
|
+
&self,
|
|
3275
|
+
ruby: &Ruby,
|
|
3276
|
+
module_id: i32,
|
|
3277
|
+
report: Option<String>,
|
|
3278
|
+
) -> Result<Value, Error> {
|
|
2896
3279
|
let reply = self.run(
|
|
2897
3280
|
ruby,
|
|
2898
3281
|
Request::EvaluateModule {
|
|
2899
3282
|
module_id,
|
|
2900
3283
|
timeout_ms: self.default_timeout_ms,
|
|
3284
|
+
report,
|
|
2901
3285
|
},
|
|
2902
3286
|
)?;
|
|
2903
3287
|
self.reply_value(ruby, reply)
|
|
@@ -2986,10 +3370,18 @@ impl Core {
|
|
|
2986
3370
|
code_cache_from_reply(ruby, reply)
|
|
2987
3371
|
}
|
|
2988
3372
|
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3373
|
+
// Owner-thread only, like every other writer of the roots array (`attach`,
|
|
3374
|
+
// `attach_many`, `dispose`): the store can REALLOC the array's element storage, and
|
|
3375
|
+
// the import hooks read that storage with the GVL released.
|
|
3376
|
+
fn set_dynamic_import_resolver(&self, ruby: &Ruby, proc: Proc) -> Result<(), Error> {
|
|
3377
|
+
self.ensure_owner_and_live(ruby)?;
|
|
3378
|
+
self.root_store(ruby, ROOT_IMPORT_RESOLVER, proc)
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// The resolver, or None if none was set. Read on the V8 side without the GVL —
|
|
3382
|
+
// see root_proc.
|
|
3383
|
+
fn dynamic_import_resolver(&self) -> Option<Proc> {
|
|
3384
|
+
self.root_proc(ROOT_IMPORT_RESOLVER)
|
|
2993
3385
|
}
|
|
2994
3386
|
|
|
2995
3387
|
// Terminate whatever is running. IsolateHandle is Send + refcounted —
|
|
@@ -3020,12 +3412,16 @@ impl Core {
|
|
|
3020
3412
|
// it entered, and OwnedIsolate's Drop asserts `self == GetCurrent()`
|
|
3021
3413
|
// (then exits). Between ops the isolate is exited, so we must enter here.
|
|
3022
3414
|
unsafe { (*self.iso_ptr.0).enter() };
|
|
3415
|
+
// Only the Rust-side bookkeeping. Clearing the roots ARRAY here would be
|
|
3416
|
+
// pointless work: teardown also runs from Core::drop, and by then the array
|
|
3417
|
+
// is garbage itself — the last wrapper marking it is exactly what went away.
|
|
3418
|
+
// `dispose` clears it explicitly instead, for the isolate the caller disposes
|
|
3419
|
+
// but keeps a handle to.
|
|
3023
3420
|
{
|
|
3024
3421
|
let mut procs = self.procs.lock().unwrap();
|
|
3025
3422
|
procs.slots.clear();
|
|
3026
3423
|
procs.free.clear();
|
|
3027
3424
|
}
|
|
3028
|
-
*self.dynamic_import_resolver.lock().unwrap() = None;
|
|
3029
3425
|
{
|
|
3030
3426
|
let st = istate!(unsafe { &mut *self.iso_ptr.0 });
|
|
3031
3427
|
st.realms = V8State::default();
|
|
@@ -3074,6 +3470,10 @@ impl Core {
|
|
|
3074
3470
|
shared.disposed = true;
|
|
3075
3471
|
}
|
|
3076
3472
|
self.teardown();
|
|
3473
|
+
// Release the host fns NOW rather than when the wrappers are collected: a
|
|
3474
|
+
// disposed isolate can never call one again, and each closes over as much
|
|
3475
|
+
// of the embedder's world as it happens to capture.
|
|
3476
|
+
let _ = ruby.get_inner(self.roots).clear();
|
|
3077
3477
|
Ok(())
|
|
3078
3478
|
}
|
|
3079
3479
|
}
|
|
@@ -3189,9 +3589,15 @@ impl Isolate {
|
|
|
3189
3589
|
fn low_memory_notification(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
3190
3590
|
rb_self.core.low_memory_notification(ruby)
|
|
3191
3591
|
}
|
|
3592
|
+
// Isolate#pump_message_loop: run pending foreground platform tasks (FinalizationRegistry
|
|
3593
|
+
// cleanup callbacks and WeakRef clearing among them), without blocking. Pair it with a GC
|
|
3594
|
+
// (low_memory_notification) or call it periodically so weak-collection callbacks actually fire.
|
|
3595
|
+
fn pump_message_loop(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
3596
|
+
rb_self.core.pump_message_loop(ruby)
|
|
3597
|
+
}
|
|
3192
3598
|
// dynamic_import_resolver = ->(specifier, referrer_url) { module } for import().
|
|
3193
|
-
fn set_dynamic_import_resolver(rb_self: &Self, proc: Proc) {
|
|
3194
|
-
rb_self.core.set_dynamic_import_resolver(proc)
|
|
3599
|
+
fn set_dynamic_import_resolver(ruby: &Ruby, rb_self: &Self, proc: Proc) -> Result<(), Error> {
|
|
3600
|
+
rb_self.core.set_dynamic_import_resolver(ruby, proc)
|
|
3195
3601
|
}
|
|
3196
3602
|
fn dispose(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
3197
3603
|
rb_self.core.dispose(ruby)
|
|
@@ -3219,14 +3625,14 @@ impl Context {
|
|
|
3219
3625
|
Ok(())
|
|
3220
3626
|
}
|
|
3221
3627
|
// timeout_ms 0 = use the isolate's default; an explicit value overrides it.
|
|
3222
|
-
// |
|
|
3628
|
+
// |completion| 0 marshals the completion value, 1 discards it, 2 keeps a primitive alone (`Completion`).
|
|
3223
3629
|
fn eval(
|
|
3224
3630
|
ruby: &Ruby,
|
|
3225
3631
|
rb_self: &Self,
|
|
3226
3632
|
source: String,
|
|
3227
3633
|
timeout_ms: u64,
|
|
3228
3634
|
filename: String,
|
|
3229
|
-
|
|
3635
|
+
completion: u8,
|
|
3230
3636
|
) -> Result<Value, Error> {
|
|
3231
3637
|
rb_self.check_live(ruby)?;
|
|
3232
3638
|
let timeout = if timeout_ms == 0 {
|
|
@@ -3234,9 +3640,14 @@ impl Context {
|
|
|
3234
3640
|
} else {
|
|
3235
3641
|
timeout_ms
|
|
3236
3642
|
};
|
|
3643
|
+
let completion = match completion {
|
|
3644
|
+
1 => Completion::Void,
|
|
3645
|
+
2 => Completion::Primitive,
|
|
3646
|
+
_ => Completion::Value,
|
|
3647
|
+
};
|
|
3237
3648
|
rb_self
|
|
3238
3649
|
.core
|
|
3239
|
-
.eval_t(ruby, rb_self.id, source, filename, timeout,
|
|
3650
|
+
.eval_t(ruby, rb_self.id, source, filename, timeout, completion)
|
|
3240
3651
|
}
|
|
3241
3652
|
fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
3242
3653
|
rb_self.check_live(ruby)?;
|
|
@@ -3425,9 +3836,12 @@ impl JsModule {
|
|
|
3425
3836
|
.core
|
|
3426
3837
|
.instantiate_module(ruby, rb_self.module_id, resolver)
|
|
3427
3838
|
}
|
|
3428
|
-
|
|
3839
|
+
// |report|: nil, or the name of the context global that reports the evaluation's rejection (`Request::EvaluateModule`).
|
|
3840
|
+
fn evaluate(ruby: &Ruby, rb_self: &Self, report: Option<String>) -> Result<Value, Error> {
|
|
3429
3841
|
rb_self.check_live(ruby)?;
|
|
3430
|
-
rb_self
|
|
3842
|
+
rb_self
|
|
3843
|
+
.core
|
|
3844
|
+
.evaluate_module(ruby, rb_self.module_id, report)
|
|
3431
3845
|
}
|
|
3432
3846
|
fn namespace(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3433
3847
|
rb_self.check_live(ruby)?;
|
|
@@ -3584,9 +3998,15 @@ fn binary_bytes(
|
|
|
3584
3998
|
|
|
3585
3999
|
fn vm_err(ruby: &Ruby, e: VmError) -> Error {
|
|
3586
4000
|
match e {
|
|
3587
|
-
VmError::Parse(m) =>
|
|
4001
|
+
VmError::Parse(m, thrown) => {
|
|
4002
|
+
carrying_thrown(Error::new(err_class(ruby, "ParseError"), m), thrown)
|
|
4003
|
+
}
|
|
3588
4004
|
VmError::Runtime(m) => Error::new(err_class(ruby, "RuntimeError"), m),
|
|
3589
|
-
VmError::JsError {
|
|
4005
|
+
VmError::JsError {
|
|
4006
|
+
message,
|
|
4007
|
+
backtrace,
|
|
4008
|
+
thrown,
|
|
4009
|
+
} => carrying_thrown(js_runtime_error(ruby, message, backtrace), thrown),
|
|
3590
4010
|
VmError::Terminated => Error::new(
|
|
3591
4011
|
err_class(ruby, "ScriptTerminatedError"),
|
|
3592
4012
|
"JavaScript was terminated (timeout or stop)",
|
|
@@ -3628,6 +4048,42 @@ fn terminated_error(ruby: &Ruby, js_backtrace: Option<Vec<String>>) -> Error {
|
|
|
3628
4048
|
}
|
|
3629
4049
|
}
|
|
3630
4050
|
|
|
4051
|
+
// The ivar a Ruby exception carries its thrown value's serial in (`keep_thrown`),
|
|
4052
|
+
// beside the message it was raised with.
|
|
4053
|
+
const THROWN_IVAR: &str = "@__rusty_racer_thrown";
|
|
4054
|
+
|
|
4055
|
+
// |error| as an instance carrying |thrown|'s serial, for a host fn it propagates
|
|
4056
|
+
// out of to throw the kept value on (`take_thrown`).
|
|
4057
|
+
fn carrying_thrown(error: Error, thrown: Option<u64>) -> Error {
|
|
4058
|
+
let Some(serial) = thrown else {
|
|
4059
|
+
return error;
|
|
4060
|
+
};
|
|
4061
|
+
match error_to_exception(&error) {
|
|
4062
|
+
Some(exc) => {
|
|
4063
|
+
let Ok(message) = exc.funcall::<_, _, Value>("message", ()) else {
|
|
4064
|
+
return Error::from(exc);
|
|
4065
|
+
};
|
|
4066
|
+
let _ = exc
|
|
4067
|
+
.funcall::<_, _, Value>("instance_variable_set", (THROWN_IVAR, (serial, message)));
|
|
4068
|
+
Error::from(exc)
|
|
4069
|
+
}
|
|
4070
|
+
None => error,
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// The serial of the thrown value a host fn's raised |error| carries, if any — not
|
|
4075
|
+
// when Ruby re-raised it with a message of its own (`raise e, 'other'` copies
|
|
4076
|
+
// the ivar): that error says something the value does not.
|
|
4077
|
+
fn thrown_serial_of(error: &Error) -> Option<u64> {
|
|
4078
|
+
let exc = error_to_exception(error)?;
|
|
4079
|
+
let (serial, message): (u64, Value) = exc
|
|
4080
|
+
.funcall::<_, _, Option<(u64, Value)>>("instance_variable_get", (THROWN_IVAR,))
|
|
4081
|
+
.ok()
|
|
4082
|
+
.flatten()?;
|
|
4083
|
+
let now: Value = exc.funcall("message", ()).ok()?;
|
|
4084
|
+
now.eql(message).ok()?.then_some(serial)
|
|
4085
|
+
}
|
|
4086
|
+
|
|
3631
4087
|
// Build a RustyRacer::RuntimeError carrying the JS stack as its Ruby backtrace.
|
|
3632
4088
|
// Constructs the exception instance so we can set_backtrace before raising;
|
|
3633
4089
|
// falls back to a plain Error if any of that fails.
|
|
@@ -3697,6 +4153,16 @@ fn resolve_module_via_ruby(
|
|
|
3697
4153
|
|
|
3698
4154
|
#[magnus::init]
|
|
3699
4155
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
4156
|
+
install_classes(ruby)
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
// Define the RustyRacer::* Ruby classes/modules. Split out of the magnus init so
|
|
4160
|
+
// an embedder that links rusty_racer as a LIBRARY (rather than loading its gem
|
|
4161
|
+
// .so) can build its own cdylib and call this from its own `#[magnus::init]` —
|
|
4162
|
+
// e.g. capybara-simulated, which links rusty_racer + a native DOM into one
|
|
4163
|
+
// extension. Standalone gem use goes through `init` above; both define the exact
|
|
4164
|
+
// same surface.
|
|
4165
|
+
pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
3700
4166
|
let module = ruby.define_module("RustyRacer")?;
|
|
3701
4167
|
|
|
3702
4168
|
// The isolate (VM) + its isolate-level ops; hands out Contexts.
|
|
@@ -3720,13 +4186,14 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3720
4186
|
"low_memory_notification",
|
|
3721
4187
|
method!(Isolate::low_memory_notification, 0),
|
|
3722
4188
|
)?;
|
|
4189
|
+
isolate.define_method("pump_message_loop", method!(Isolate::pump_message_loop, 0))?;
|
|
3723
4190
|
isolate.define_method("dispose", method!(Isolate::dispose, 0))?;
|
|
3724
4191
|
isolate.define_method("disposed?", method!(Isolate::disposed, 0))?;
|
|
3725
4192
|
|
|
3726
4193
|
// A v8::Context (realm): eval/call/attach/compile_module.
|
|
3727
4194
|
let context = module.define_class("Context", ruby.class_object())?;
|
|
3728
4195
|
// Backs both keyword-arg wrappers in lib: Context#eval(source, timeout_ms:,
|
|
3729
|
-
// filename:) and #
|
|
4196
|
+
// filename:), #eval_void and #eval_primitive, which differ only in the 4th arg (what they keep of the completion value).
|
|
3730
4197
|
context.define_method("_eval", method!(Context::eval, 4))?;
|
|
3731
4198
|
context.define_method("call", method!(Context::call, -1))?;
|
|
3732
4199
|
context.define_method("call_void", method!(Context::call_void, -1))?;
|
|
@@ -3761,7 +4228,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3761
4228
|
// Thin ES-module handle: Context#compile_module -> instantiate/evaluate.
|
|
3762
4229
|
let jsmodule = module.define_class("Module", ruby.class_object())?;
|
|
3763
4230
|
jsmodule.define_method("_instantiate", method!(JsModule::instantiate, 1))?;
|
|
3764
|
-
jsmodule.define_method("
|
|
4231
|
+
jsmodule.define_method("_evaluate", method!(JsModule::evaluate, 1))?;
|
|
3765
4232
|
jsmodule.define_method("namespace", method!(JsModule::namespace, 0))?;
|
|
3766
4233
|
jsmodule.define_method("_status", method!(JsModule::status, 0))?;
|
|
3767
4234
|
jsmodule.define_method("graph_async?", method!(JsModule::graph_async, 0))?;
|