rusty_racer 0.2.4 → 0.2.6
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/CHANGELOG.md +60 -0
- data/README.md +79 -1
- data/ext/rusty_racer/Cargo.toml +6 -1
- data/ext/rusty_racer/src/lib.rs +596 -174
- data/ext/rusty_racer/src/marshal.rs +356 -117
- data/ext/rusty_racer/src/ops.rs +353 -184
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +18 -2
- metadata +3 -2
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -59,7 +59,7 @@ use std::cell::RefCell;
|
|
|
59
59
|
use std::collections::HashMap;
|
|
60
60
|
use std::ffi::c_void;
|
|
61
61
|
use std::ptr::null_mut;
|
|
62
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
62
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
63
63
|
use std::sync::{Arc, Mutex, Once, Weak};
|
|
64
64
|
|
|
65
65
|
use magnus::block::Proc;
|
|
@@ -72,7 +72,7 @@ use magnus::{
|
|
|
72
72
|
mod marshal;
|
|
73
73
|
use marshal::{JsVal, js_to_jsval, jsval_to_js, jsval_to_ruby, ruby_to_jsval};
|
|
74
74
|
mod ops;
|
|
75
|
-
use ops::{Compiled, Request, VmReply, run_source, service_request};
|
|
75
|
+
use ops::{Compiled, Completion, Request, VmReply, run_source, service_request};
|
|
76
76
|
mod stack;
|
|
77
77
|
use stack::{STACK_DEBUG, StackScope, current_real_isolate, discover_scan_start_field};
|
|
78
78
|
mod watchdog;
|
|
@@ -93,8 +93,8 @@ use watchdog::{
|
|
|
93
93
|
// SAFETY of the manual Send: the !Send contents (and BoxValue's drop, which
|
|
94
94
|
// calls rb_gc_unregister_address) only run under the GVL. Two conventions
|
|
95
95
|
// keep that true — breaking either is silent UB, so don't:
|
|
96
|
-
// - Arc<Core> lives ONLY in the
|
|
97
|
-
// Module, Script). Never clone it into the V8/watchdog threads, or the
|
|
96
|
+
// - Arc<Core> lives ONLY in the five TypedData wrappers (Isolate, Context,
|
|
97
|
+
// Module, Script, Function). Never clone it into the V8/watchdog threads, or the
|
|
98
98
|
// last drop could run off a Ruby thread.
|
|
99
99
|
// - the wrappers must not set free_immediately: their dfree (and so Core's
|
|
100
100
|
// drop) has to run outside the GC sweep, where Ruby APIs are forbidden.
|
|
@@ -146,23 +146,58 @@ macro_rules! istate {
|
|
|
146
146
|
pub(crate) use istate;
|
|
147
147
|
|
|
148
148
|
// One attach()'d host fn: the realm it was attached into — so resetting or
|
|
149
|
-
// disposing that realm can release it — whether it is still attached
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
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.
|
|
154
154
|
struct ProcSlot {
|
|
155
155
|
context_id: i32,
|
|
156
156
|
live: bool,
|
|
157
|
+
generation: u32,
|
|
157
158
|
name: String,
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
//
|
|
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
|
|
161
196
|
// emptied by release_context_procs so a later attach reuses them instead of
|
|
162
197
|
// growing `slots` forever — a long-lived process that re-navigates iframe
|
|
163
|
-
// realms would otherwise leak one slot per attach.
|
|
164
|
-
//
|
|
165
|
-
//
|
|
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.
|
|
166
201
|
#[derive(Default)]
|
|
167
202
|
struct ProcTable {
|
|
168
203
|
slots: Vec<ProcSlot>,
|
|
@@ -170,17 +205,39 @@ struct ProcTable {
|
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
impl ProcTable {
|
|
173
|
-
// Take a free slot if one exists,
|
|
174
|
-
fn alloc(&mut self,
|
|
175
|
-
|
|
176
|
-
self.slots[
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
181
231
|
}
|
|
182
232
|
}
|
|
183
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
|
+
|
|
184
241
|
// Release every live proc attached into |context_id| (its realm is gone),
|
|
185
242
|
// returning their ids so the caller can clear their roots-array entries (a Ruby
|
|
186
243
|
// write, which this table never does: it is locked on paths a GC must not have to
|
|
@@ -202,11 +259,11 @@ impl ProcTable {
|
|
|
202
259
|
// long as a wrapper of it is reachable, in one place the GC can be shown without
|
|
203
260
|
// taking any Rust lock (a mark that had to lock `procs` could deadlock against
|
|
204
261
|
// the thread whose allocation triggered the GC while holding it). Slot 0 is the
|
|
205
|
-
// dynamic-import resolver; the attached host fns follow, one per
|
|
262
|
+
// dynamic-import resolver; the attached host fns follow, one per proc slot.
|
|
206
263
|
const ROOT_IMPORT_RESOLVER: isize = 0;
|
|
207
264
|
const ROOT_FIRST_PROC: isize = 1;
|
|
208
|
-
fn proc_root_index(
|
|
209
|
-
ROOT_FIRST_PROC +
|
|
265
|
+
fn proc_root_index(index: usize) -> isize {
|
|
266
|
+
ROOT_FIRST_PROC + index as isize
|
|
210
267
|
}
|
|
211
268
|
|
|
212
269
|
// Look up a RustyRacer::<name> exception class at raise time. The classes are
|
|
@@ -221,16 +278,23 @@ fn err_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
|
221
278
|
|
|
222
279
|
#[derive(Debug)]
|
|
223
280
|
enum VmError {
|
|
224
|
-
|
|
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>),
|
|
225
284
|
Runtime(String), // internal failure (no JS stack) -> RustyRacer::RuntimeError
|
|
226
285
|
// A thrown JS exception: its message plus the JS stack frames, which become
|
|
227
|
-
// 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.
|
|
228
289
|
JsError {
|
|
229
290
|
message: String,
|
|
230
291
|
backtrace: Vec<String>,
|
|
292
|
+
thrown: Option<u64>,
|
|
231
293
|
},
|
|
232
294
|
Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
|
|
233
295
|
OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
|
|
296
|
+
// A handle whose realm was reset or disposed under it -> RustyRacer::DisposedError
|
|
297
|
+
Disposed(String),
|
|
234
298
|
}
|
|
235
299
|
|
|
236
300
|
// V8's near-heap-limit callback (registered on EVERY isolate). V8 calls this,
|
|
@@ -330,9 +394,9 @@ where
|
|
|
330
394
|
// instead of saying "a host function". Rendered only on that path, so an
|
|
331
395
|
// ordinary callback pays for a pointer and nothing else.
|
|
332
396
|
enum RubyCall<'a> {
|
|
333
|
-
// An attach()'d host function, by
|
|
397
|
+
// An attach()'d host function, by its key — the name it was attached
|
|
334
398
|
// under lives in the isolate's proc table.
|
|
335
|
-
HostFn(
|
|
399
|
+
HostFn(HostFnKey),
|
|
336
400
|
// A module resolver (a static instantiate block, a dynamic auto-link, or
|
|
337
401
|
// import()), named by the specifier it was handed.
|
|
338
402
|
Resolver(&'a str),
|
|
@@ -341,15 +405,17 @@ enum RubyCall<'a> {
|
|
|
341
405
|
impl RubyCall<'_> {
|
|
342
406
|
fn describe(&self, core: &Core) -> String {
|
|
343
407
|
match self {
|
|
344
|
-
// try_lock, not lock: nothing holds this across a callback (
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
408
|
+
// try_lock, not lock: nothing holds this across a callback (`call_proc`
|
|
409
|
+
// holds it for one comparison, before the proc runs), but blocking here
|
|
410
|
+
// would turn a diagnostic into a hang. A slot handed on since names
|
|
411
|
+
// nobody: its name is another function's.
|
|
412
|
+
Self::HostFn(id) => match core.procs.try_lock().ok().and_then(|procs| {
|
|
413
|
+
procs
|
|
414
|
+
.slots
|
|
415
|
+
.get(id.index())
|
|
416
|
+
.filter(|slot| slot.generation == id.generation)
|
|
417
|
+
.map(|slot| slot.name.clone())
|
|
418
|
+
}) {
|
|
353
419
|
Some(name) => format!("The host function `{name}`"),
|
|
354
420
|
None => "A host function".to_string(),
|
|
355
421
|
},
|
|
@@ -546,8 +612,8 @@ fn host_fn_callback(
|
|
|
546
612
|
args: v8::FunctionCallbackArguments<'_>,
|
|
547
613
|
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
548
614
|
) {
|
|
549
|
-
let
|
|
550
|
-
Ok(e) => e.value()
|
|
615
|
+
let host_fn = match v8::Local::<v8::External>::try_from(args.data()) {
|
|
616
|
+
Ok(e) => HostFnKey::from_external(e.value()),
|
|
551
617
|
Err(_) => return,
|
|
552
618
|
};
|
|
553
619
|
let mut js_args = Vec::with_capacity(args.length() as usize);
|
|
@@ -574,20 +640,22 @@ fn host_fn_callback(
|
|
|
574
640
|
return;
|
|
575
641
|
}
|
|
576
642
|
let core = unsafe { &*core_ptr };
|
|
577
|
-
let result: Result<JsVal,
|
|
643
|
+
let result: Result<JsVal, HostFnFailure> = with_gvl(core, RubyCall::HostFn(host_fn), || {
|
|
578
644
|
let ruby = Ruby::get().unwrap();
|
|
579
645
|
// rb_protect so a Ruby exception raised during arg/return marshalling
|
|
580
646
|
// (ary_new_capa / jsval_to_ruby / ruby_to_jsval can raise on OOM etc.)
|
|
581
647
|
// is CAUGHT here instead of longjmp-ing through V8's C++ frames. The
|
|
582
648
|
// proc's own raise is already a magnus Err; this covers the rest.
|
|
583
649
|
use magnus::rb_sys::AsRawValue;
|
|
584
|
-
let mut out: Option<Result<JsVal,
|
|
650
|
+
let mut out: Option<Result<JsVal, HostFnFailure>> = None;
|
|
585
651
|
match magnus::rb_sys::protect(|| {
|
|
586
|
-
out = Some(core.call_proc(&ruby,
|
|
652
|
+
out = Some(core.call_proc(&ruby, host_fn, &js_args));
|
|
587
653
|
ruby.qnil().as_raw()
|
|
588
654
|
}) {
|
|
589
|
-
Ok(_) =>
|
|
590
|
-
|
|
655
|
+
Ok(_) => {
|
|
656
|
+
out.unwrap_or_else(|| Err("host function did not complete".to_string().into()))
|
|
657
|
+
}
|
|
658
|
+
Err(e) => Err(format!("{e}").into()),
|
|
591
659
|
}
|
|
592
660
|
});
|
|
593
661
|
match result {
|
|
@@ -595,8 +663,30 @@ fn host_fn_callback(
|
|
|
595
663
|
let v = jsval_to_js(scope, val);
|
|
596
664
|
rv.set(v);
|
|
597
665
|
}
|
|
598
|
-
// The proc raised (or marshalling failed): surface as a JS exception
|
|
599
|
-
|
|
666
|
+
// The proc raised (or marshalling failed): surface as a JS exception —
|
|
667
|
+
// the value JS threw, where the error is one a JS throw became.
|
|
668
|
+
Err(failure) => match failure.thrown.and_then(|serial| take_thrown(scope, serial)) {
|
|
669
|
+
Some(thrown) => {
|
|
670
|
+
scope.throw_exception(thrown);
|
|
671
|
+
}
|
|
672
|
+
None => throw_js_error(scope, &failure.message),
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// How a host fn failed: the Ruby error's text, and the serial of the JS value
|
|
678
|
+
// it carries when a JS throw is what it became (`keep_thrown`).
|
|
679
|
+
struct HostFnFailure {
|
|
680
|
+
message: String,
|
|
681
|
+
thrown: Option<u64>,
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
impl From<String> for HostFnFailure {
|
|
685
|
+
fn from(message: String) -> Self {
|
|
686
|
+
HostFnFailure {
|
|
687
|
+
message,
|
|
688
|
+
thrown: None,
|
|
689
|
+
}
|
|
600
690
|
}
|
|
601
691
|
}
|
|
602
692
|
|
|
@@ -696,12 +786,93 @@ fn capture_js_error(
|
|
|
696
786
|
let backtrace = stack_str
|
|
697
787
|
.map(|s| parse_js_stack(s.as_str()))
|
|
698
788
|
.unwrap_or_default();
|
|
699
|
-
|
|
789
|
+
let thrown = keep_thrown(scope, exception);
|
|
790
|
+
VmError::JsError {
|
|
791
|
+
message,
|
|
792
|
+
backtrace,
|
|
793
|
+
thrown,
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// A value JS failed with that no TryCatch caught — an errored module's
|
|
798
|
+
// exception, a rejected promise's reason — as a JsError: its text, no stack
|
|
799
|
+
// frames, and the value kept as a thrown one is.
|
|
800
|
+
fn rejected_with(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>) -> VmError {
|
|
801
|
+
VmError::JsError {
|
|
802
|
+
message: value.to_rust_string_lossy(scope),
|
|
803
|
+
backtrace: vec![],
|
|
804
|
+
thrown: keep_thrown(scope, Some(value)),
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// A compile failure as a ParseError: V8's message, the location it recorded
|
|
809
|
+
// (always the file, the line when it has one), and the SyntaxError kept as a
|
|
810
|
+
// thrown value is.
|
|
811
|
+
fn parse_error(
|
|
812
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
813
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
814
|
+
message: Option<v8::Local<v8::Message>>,
|
|
815
|
+
filename: &str,
|
|
816
|
+
fallback: &str,
|
|
817
|
+
) -> VmError {
|
|
818
|
+
let msg = exception
|
|
819
|
+
.map(|e| e.to_rust_string_lossy(scope))
|
|
820
|
+
.unwrap_or_else(|| fallback.to_string());
|
|
821
|
+
let res = message
|
|
822
|
+
.and_then(|m| m.get_script_resource_name(scope))
|
|
823
|
+
.filter(|v| v.is_string())
|
|
824
|
+
.map(|v| v.to_rust_string_lossy(scope))
|
|
825
|
+
.unwrap_or_else(|| filename.to_string());
|
|
826
|
+
let loc = match message.and_then(|m| m.get_line_number(scope)) {
|
|
827
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
828
|
+
None => format!(" at {res}"),
|
|
829
|
+
};
|
|
830
|
+
VmError::Parse(format!("{msg}{loc}"), keep_thrown(scope, exception))
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Keep the value JS threw, so a host fn that lets the Ruby error it became
|
|
834
|
+
// propagate throws THAT value on — its identity, class and stack — rather than a
|
|
835
|
+
// fresh Error made of its text: a script a host fn ran for JS fails in JS as the
|
|
836
|
+
// script did. One slot per isolate, since only the latest thrown value can still
|
|
837
|
+
// be on its way back out, and only until the outermost op returns (`Core::run`),
|
|
838
|
+
// since only a host fn inside it can still ask — kept longer, it would pin the
|
|
839
|
+
// value, and its realm, for as long as nothing else threw. The serial (carried
|
|
840
|
+
// by the Ruby exception, see `carrying_thrown`) tells it from a stale one, and is
|
|
841
|
+
// drawn process-wide, so an error another isolate's op raised never matches this
|
|
842
|
+
// one's slot: its value cannot cross isolates. None on an isolate with no state
|
|
843
|
+
// (the snapshot creator's), where no host fn can ask.
|
|
844
|
+
fn keep_thrown(
|
|
845
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
846
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
847
|
+
) -> Option<u64> {
|
|
848
|
+
static SERIAL: AtomicU64 = AtomicU64::new(0);
|
|
849
|
+
let exception = exception?;
|
|
850
|
+
let kept = v8::Global::new(scope, exception);
|
|
851
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
852
|
+
let serial = SERIAL.fetch_add(1, Ordering::Relaxed) + 1;
|
|
853
|
+
state.thrown = Some((serial, kept));
|
|
854
|
+
Some(serial)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// The Ruby exception's kept thrown value (`keep_thrown`), when it still is the
|
|
858
|
+
// latest: taken, as it is thrown once.
|
|
859
|
+
fn take_thrown<'s>(
|
|
860
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
861
|
+
serial: u64,
|
|
862
|
+
) -> Option<v8::Local<'s, v8::Value>> {
|
|
863
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
864
|
+
let (kept_serial, _) = state.thrown.as_ref()?;
|
|
865
|
+
if *kept_serial != serial {
|
|
866
|
+
return None;
|
|
867
|
+
}
|
|
868
|
+
let (_, kept) = state.thrown.take()?;
|
|
869
|
+
Some(v8::Local::new(scope, kept))
|
|
700
870
|
}
|
|
701
871
|
|
|
702
872
|
// ---------------------------------------------------------------------------
|
|
703
873
|
// ES modules: V8's raw compile/instantiate/evaluate steps, with the embedder
|
|
704
|
-
// owning the url->Module registry
|
|
874
|
+
// owning the url->Module registry and the resolve policy (ModuleReg is only the
|
|
875
|
+
// binding's id and url bookkeeping).
|
|
705
876
|
// ---------------------------------------------------------------------------
|
|
706
877
|
fn module_origin<'s>(scope: &v8::PinScope<'s, '_>, url: &str) -> v8::ScriptOrigin<'s> {
|
|
707
878
|
let name = v8::String::new(scope, url).unwrap();
|
|
@@ -720,18 +891,47 @@ fn module_origin<'s>(scope: &v8::PinScope<'s, '_>, url: &str) -> v8::ScriptOrigi
|
|
|
720
891
|
)
|
|
721
892
|
}
|
|
722
893
|
|
|
723
|
-
// Registry for the thin compile_module/instantiate API
|
|
724
|
-
// addressed by an id, with its url kept for the resolve round-trip and a
|
|
725
|
-
// hash bucket to map a referrer Local<Module> back to its id.
|
|
894
|
+
// Registry for the thin compile_module/instantiate API.
|
|
726
895
|
#[derive(Default)]
|
|
727
896
|
struct ModuleReg {
|
|
728
|
-
// id -> (module,
|
|
729
|
-
// a module is bound to the v8::Context it
|
|
730
|
-
|
|
731
|
-
|
|
897
|
+
// id -> (module, owning context id): what a Module wrapper's ops address.
|
|
898
|
+
// The context id is needed because a module is bound to the v8::Context it
|
|
899
|
+
// was compiled in. Strong, and gone once the wrapper is disposed or collected
|
|
900
|
+
// (Core.released).
|
|
901
|
+
by_id: HashMap<i32, (v8::Global<v8::Module>, i32)>,
|
|
902
|
+
// identity hash -> (module, url): a Local<Module> V8 hands a callback
|
|
903
|
+
// (import.meta, a referrer being linked) back to the url it was compiled
|
|
904
|
+
// with. WEAK, so a module V8 still keeps — a dependency linked into a live
|
|
905
|
+
// graph, a function of a disposed realm's module another realm holds —
|
|
906
|
+
// answers whether its wrapper (or its realm) is still around or not, and
|
|
907
|
+
// one V8 collected is pruned instead of pinned (`add_url`).
|
|
908
|
+
urls: HashMap<i32, Vec<(v8::Weak<v8::Module>, String)>>,
|
|
909
|
+
// `add_url`'s amortization: entries added since the last prune, and the
|
|
910
|
+
// entries that prune left.
|
|
911
|
+
added_since_prune: usize,
|
|
912
|
+
kept_at_prune: usize,
|
|
732
913
|
next_id: i32,
|
|
733
914
|
}
|
|
734
915
|
|
|
916
|
+
impl ModuleReg {
|
|
917
|
+
// Index |module|'s url, first pruning the entries of collected modules once
|
|
918
|
+
// as many have been added since the last prune as it left — a scan
|
|
919
|
+
// amortized O(1) per compile, keeping the index within about twice the
|
|
920
|
+
// modules alive.
|
|
921
|
+
fn add_url(&mut self, hash: i32, module: v8::Weak<v8::Module>, url: String) {
|
|
922
|
+
if self.added_since_prune > self.kept_at_prune {
|
|
923
|
+
self.urls.retain(|_, bucket| {
|
|
924
|
+
bucket.retain(|(module, _)| !module.is_empty());
|
|
925
|
+
!bucket.is_empty()
|
|
926
|
+
});
|
|
927
|
+
self.kept_at_prune = self.urls.values().map(Vec::len).sum();
|
|
928
|
+
self.added_since_prune = 0;
|
|
929
|
+
}
|
|
930
|
+
self.added_since_prune += 1;
|
|
931
|
+
self.urls.entry(hash).or_default().push((module, url));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
735
935
|
// Classic compiled scripts: id -> (unbound script, owning context id). An
|
|
736
936
|
// UnboundScript is context-independent, but we run it in the context it was
|
|
737
937
|
// compiled in (and reset/dispose of that context drops it).
|
|
@@ -741,6 +941,39 @@ struct ScriptReg {
|
|
|
741
941
|
next_id: i32,
|
|
742
942
|
}
|
|
743
943
|
|
|
944
|
+
// Context#function's handles: id -> the function its path resolved to, the
|
|
945
|
+
// holder it was read off (its `this`), and the realm it was resolved in. The
|
|
946
|
+
// Globals live HERE, not in the Ruby wrapper, so a reset or disposed realm drops
|
|
947
|
+
// them (drop_context_artifacts) whatever wrappers Ruby still holds — a handle
|
|
948
|
+
// must not keep its realm's v8::Context alive — and so they are only ever
|
|
949
|
+
// dropped on the owner thread with the isolate entered, which a wrapper's GC
|
|
950
|
+
// free can't promise. A wrapper that is disposed or collected queues its id on
|
|
951
|
+
// Core.released instead, and the next outermost request boundary removes it
|
|
952
|
+
// here. Ids are never reused, so a stale id can only miss.
|
|
953
|
+
//
|
|
954
|
+
// A path may lead into ANOTHER realm (`frames[0].fn`): the entry belongs to the
|
|
955
|
+
// realm it was resolved in, so it holds that other realm's function until this
|
|
956
|
+
// one is reset or the handle is released — as any reference this realm keeps
|
|
957
|
+
// to it would.
|
|
958
|
+
#[derive(Default)]
|
|
959
|
+
struct FunctionReg {
|
|
960
|
+
by_id: HashMap<u64, FunctionEntry>,
|
|
961
|
+
next_id: u64,
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
struct FunctionEntry {
|
|
965
|
+
function: v8::Global<v8::Function>,
|
|
966
|
+
receiver: v8::Global<v8::Value>,
|
|
967
|
+
context_id: i32,
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// A handle whose Ruby wrapper let go of it, queued on Core.released.
|
|
971
|
+
enum Released {
|
|
972
|
+
Function(u64),
|
|
973
|
+
Module(i32),
|
|
974
|
+
Script(i32),
|
|
975
|
+
}
|
|
976
|
+
|
|
744
977
|
// The V8 thread's realm registry: the main context (id 0, swappable by reset),
|
|
745
978
|
// the extra realms from create_context, and the host-namespace name to
|
|
746
979
|
// re-install on fresh realms. Thread-local (like MODULES/SCRIPTS) so
|
|
@@ -825,6 +1058,7 @@ struct IsolateState {
|
|
|
825
1058
|
realms: V8State,
|
|
826
1059
|
modules: ModuleReg,
|
|
827
1060
|
scripts: ScriptReg,
|
|
1061
|
+
functions: FunctionReg,
|
|
828
1062
|
active_realms: Vec<i32>,
|
|
829
1063
|
instantiating: bool,
|
|
830
1064
|
watchdog_fired: bool,
|
|
@@ -864,6 +1098,8 @@ struct IsolateState {
|
|
|
864
1098
|
// the ScriptTerminatedError can name what was running. Taken (cleared) when the
|
|
865
1099
|
// terminated op's error is built; None for any non-timeout outcome.
|
|
866
1100
|
timeout_backtrace: Option<Vec<String>>,
|
|
1101
|
+
// The latest value JS threw out of an op, with its serial (`keep_thrown`).
|
|
1102
|
+
thrown: Option<(u64, v8::Global<v8::Value>)>,
|
|
867
1103
|
}
|
|
868
1104
|
|
|
869
1105
|
impl IsolateState {
|
|
@@ -876,6 +1112,7 @@ impl IsolateState {
|
|
|
876
1112
|
},
|
|
877
1113
|
modules: ModuleReg::default(),
|
|
878
1114
|
scripts: ScriptReg::default(),
|
|
1115
|
+
functions: FunctionReg::default(),
|
|
879
1116
|
active_realms: Vec::new(),
|
|
880
1117
|
instantiating: false,
|
|
881
1118
|
watchdog_fired: false,
|
|
@@ -888,6 +1125,7 @@ impl IsolateState {
|
|
|
888
1125
|
oom_fired: false,
|
|
889
1126
|
oom_initial_limit: 0,
|
|
890
1127
|
timeout_backtrace: None,
|
|
1128
|
+
thrown: None,
|
|
891
1129
|
}
|
|
892
1130
|
}
|
|
893
1131
|
}
|
|
@@ -973,7 +1211,7 @@ fn transfer_registry() -> &'static Mutex<HashMap<u64, SendBackingStore>> {
|
|
|
973
1211
|
// Monotonic token source. Starts at 1 so 0 is free as the JS-side "not
|
|
974
1212
|
// transferable — fall back to a copy" sentinel. A token is f64-exact for ~2^53
|
|
975
1213
|
// transfers, which a process will never reach.
|
|
976
|
-
static NEXT_TRANSFER_TOKEN:
|
|
1214
|
+
static NEXT_TRANSFER_TOKEN: AtomicU64 = AtomicU64::new(1);
|
|
977
1215
|
|
|
978
1216
|
// Drain EVERY realm's microtask queue once. Each realm has its own queue (see
|
|
979
1217
|
// V8State::queues), so a single scope.perform_microtask_checkpoint() — which only
|
|
@@ -1033,23 +1271,19 @@ fn auto_drain(scope: &mut v8::PinScope<'_, '_>, outermost: bool) {
|
|
|
1033
1271
|
}
|
|
1034
1272
|
}
|
|
1035
1273
|
|
|
1036
|
-
// Drop every module
|
|
1037
|
-
// going away — on reset or dispose — so those handles are now
|
|
1274
|
+
// Drop every module, script and function handle of `context_id` (its
|
|
1275
|
+
// v8::Context is going away — on reset or dispose — so those handles are now
|
|
1276
|
+
// dead, and a function's Globals would otherwise keep that context alive).
|
|
1038
1277
|
fn drop_context_artifacts(state: &mut IsolateState, context_id: i32) {
|
|
1039
1278
|
let m = &mut state.modules;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
.filter(|(_, (_, _, cid))| *cid == context_id)
|
|
1044
|
-
.map(|(id, _)| *id)
|
|
1045
|
-
.collect();
|
|
1046
|
-
for id in dead {
|
|
1047
|
-
m.by_id.remove(&id);
|
|
1048
|
-
for bucket in m.by_hash.values_mut() {
|
|
1049
|
-
bucket.retain(|(_, mid)| *mid != id);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1279
|
+
// (Not its url index: weak, it pins nothing, and a module of this realm that
|
|
1280
|
+
// another realm still reaches keeps its import.meta.url.)
|
|
1281
|
+
m.by_id.retain(|_, (_, cid)| *cid != context_id);
|
|
1052
1282
|
state.scripts.by_id.retain(|_, (_, cid)| *cid != context_id);
|
|
1283
|
+
state
|
|
1284
|
+
.functions
|
|
1285
|
+
.by_id
|
|
1286
|
+
.retain(|_, entry| entry.context_id != context_id);
|
|
1053
1287
|
// A promise-reject recorder created in this context is unusable now.
|
|
1054
1288
|
if state
|
|
1055
1289
|
.realms
|
|
@@ -1073,24 +1307,21 @@ fn script_handle(
|
|
|
1073
1307
|
.map(|(g, cid)| (g.clone(), *cid))
|
|
1074
1308
|
}
|
|
1075
1309
|
|
|
1076
|
-
// A
|
|
1077
|
-
//
|
|
1078
|
-
// url for the resolve round-trip and to fill
|
|
1310
|
+
// A compiled module's url (the filename it was compiled with), by identity in
|
|
1311
|
+
// the weak url index (ModuleReg.urls) — for as long as V8 keeps the module. Used
|
|
1312
|
+
// to give a referrer its url for the resolve round-trip and to fill
|
|
1313
|
+
// import.meta.url.
|
|
1079
1314
|
fn module_url(scope: &mut v8::PinScope<'_, '_>, module: v8::Local<v8::Module>) -> Option<String> {
|
|
1080
1315
|
let hash = module.get_identity_hash().get();
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
let bucket = istate!(scope).modules.by_hash.get(&hash)?.clone();
|
|
1085
|
-
let id = bucket
|
|
1086
|
-
.iter()
|
|
1087
|
-
.find(|(g, _)| v8::Local::new(&*scope, g) == module)
|
|
1088
|
-
.map(|(_, id)| *id)?;
|
|
1089
|
-
istate!(scope)
|
|
1316
|
+
// A shared borrow of the slot beside the shared one to_local takes.
|
|
1317
|
+
scope
|
|
1318
|
+
.get_slot::<IsolateState>()?
|
|
1090
1319
|
.modules
|
|
1091
|
-
.
|
|
1092
|
-
.get(&
|
|
1093
|
-
.
|
|
1320
|
+
.urls
|
|
1321
|
+
.get(&hash)?
|
|
1322
|
+
.iter()
|
|
1323
|
+
.find(|(weak, _)| weak.to_local(scope) == Some(module))
|
|
1324
|
+
.map(|(_, url)| url.clone())
|
|
1094
1325
|
}
|
|
1095
1326
|
|
|
1096
1327
|
// V8 calls this the first time a module reads import.meta. Fills in
|
|
@@ -1214,7 +1445,7 @@ fn resolve_imported<'s>(
|
|
|
1214
1445
|
.modules
|
|
1215
1446
|
.by_id
|
|
1216
1447
|
.get(&dep_id)
|
|
1217
|
-
.map(|(g,
|
|
1448
|
+
.map(|(g, cid)| (g.clone(), *cid));
|
|
1218
1449
|
let g = match (here, found) {
|
|
1219
1450
|
(Some(here), Some((g, cid))) if cid == here => g,
|
|
1220
1451
|
(Some(_), Some(_)) => {
|
|
@@ -1291,8 +1522,8 @@ fn dynamic_import_cb<'s>(
|
|
|
1291
1522
|
.modules
|
|
1292
1523
|
.by_id
|
|
1293
1524
|
.get(&id)
|
|
1294
|
-
.filter(|(_,
|
|
1295
|
-
.map(|(g, _
|
|
1525
|
+
.filter(|(_, cid)| Some(*cid) == here)
|
|
1526
|
+
.map(|(g, _)| g.clone());
|
|
1296
1527
|
match g {
|
|
1297
1528
|
Some(g) => {
|
|
1298
1529
|
let module = v8::Local::new(scope, &g);
|
|
@@ -1473,7 +1704,7 @@ fn module_handle(state: &IsolateState, module_id: i32) -> Option<(v8::Global<v8:
|
|
|
1473
1704
|
.modules
|
|
1474
1705
|
.by_id
|
|
1475
1706
|
.get(&module_id)
|
|
1476
|
-
.map(|(g,
|
|
1707
|
+
.map(|(g, cid)| (g.clone(), *cid))
|
|
1477
1708
|
}
|
|
1478
1709
|
|
|
1479
1710
|
// ---------------------------------------------------------------------------
|
|
@@ -1534,11 +1765,19 @@ struct Core {
|
|
|
1534
1765
|
// layout. Read on the V8 side without the GVL (the import hooks), which is
|
|
1535
1766
|
// sound because the entries are pinned and nothing here allocates or raises.
|
|
1536
1767
|
roots: Opaque<RArray>,
|
|
1537
|
-
//
|
|
1768
|
+
// A HostFnKey indexes ProcTable.slots — bookkeeping only (the proc itself is in
|
|
1538
1769
|
// `roots`). Mutex (uncontended — single owner thread) so host_fn_callback can
|
|
1539
1770
|
// reach it through Core (via the slot's core_ptr) while a &Core method also
|
|
1540
1771
|
// holds it. reset/dispose releases the slots and recycles them.
|
|
1541
1772
|
procs: Mutex<ProcTable>,
|
|
1773
|
+
// The handles (Function, Module, Script) whose Ruby wrappers were disposed or
|
|
1774
|
+
// collected, for the next outermost request boundary to remove from their
|
|
1775
|
+
// registries (`forget_released_handles`). A wrapper's free may run on any
|
|
1776
|
+
// Ruby thread, outside any op, so all it can do is queue its id here.
|
|
1777
|
+
released: Mutex<Vec<Released>>,
|
|
1778
|
+
// Set with each push there, so the request boundary looks at the lock only
|
|
1779
|
+
// when there is something under it.
|
|
1780
|
+
handles_released: AtomicBool,
|
|
1542
1781
|
// Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
|
|
1543
1782
|
// value overrides it. Guards against an in-V8 infinite loop without a watchdog.
|
|
1544
1783
|
default_timeout_ms: u64,
|
|
@@ -1608,10 +1847,22 @@ struct Script {
|
|
|
1608
1847
|
cache_rejected: bool,
|
|
1609
1848
|
}
|
|
1610
1849
|
|
|
1611
|
-
//
|
|
1850
|
+
// Context#function result: a JS function resolved once, with its `this`, kept in
|
|
1851
|
+
// FunctionReg by id — the realm it belongs to rides along so a call enters it
|
|
1852
|
+
// without a lookup.
|
|
1853
|
+
#[derive(TypedData)]
|
|
1854
|
+
#[magnus(class = "RustyRacer::Function", mark)]
|
|
1855
|
+
struct JsFunction {
|
|
1856
|
+
core: Arc<Core>,
|
|
1857
|
+
context_id: i32,
|
|
1858
|
+
function_id: u64,
|
|
1859
|
+
disposed: AtomicBool,
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// The five wrappers that hold an Arc<Core> all mark the isolate's roots, because any
|
|
1612
1863
|
// one of them may be the last one reachable: Ruby code is free to keep only the
|
|
1613
1864
|
// Context a `Isolate#context` handed it, or only a Module, and the host fns must
|
|
1614
|
-
// stay callable through it. Marking the same array
|
|
1865
|
+
// stay callable through it. Marking the same array five times costs nothing (the GC
|
|
1615
1866
|
// stops at an already-marked object); missing it once would free a live proc.
|
|
1616
1867
|
macro_rules! marks_core_roots {
|
|
1617
1868
|
($t:ty) => {
|
|
@@ -1626,6 +1877,7 @@ marks_core_roots!(Isolate);
|
|
|
1626
1877
|
marks_core_roots!(Context);
|
|
1627
1878
|
marks_core_roots!(JsModule);
|
|
1628
1879
|
marks_core_roots!(Script);
|
|
1880
|
+
marks_core_roots!(JsFunction);
|
|
1629
1881
|
|
|
1630
1882
|
// Set true once V8 is initialized; Platform.set_flags! refuses after that
|
|
1631
1883
|
// (flags must be set before V8::initialize), like mini_racer's
|
|
@@ -2152,12 +2404,12 @@ fn install_host_namespace(
|
|
|
2152
2404
|
];
|
|
2153
2405
|
for (member, function) in members {
|
|
2154
2406
|
if let (Some(f), Some(k)) = (function, v8::String::new(scope, member)) {
|
|
2155
|
-
ns.
|
|
2407
|
+
ns.create_data_property(scope, k.into(), f.into());
|
|
2156
2408
|
}
|
|
2157
2409
|
}
|
|
2158
2410
|
if let Some(key) = v8::String::new(scope, name) {
|
|
2159
2411
|
let global = context.global(scope);
|
|
2160
|
-
global.
|
|
2412
|
+
global.create_data_property(scope, key.into(), ns.into());
|
|
2161
2413
|
}
|
|
2162
2414
|
}
|
|
2163
2415
|
|
|
@@ -2253,11 +2505,11 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
2253
2505
|
// leaves behind); nothing reads its completion value, so
|
|
2254
2506
|
// don't marshal it — a bundle ending in an expression must
|
|
2255
2507
|
// not fail the snapshot over a value no one wants.
|
|
2256
|
-
|
|
2508
|
+
Completion::Void,
|
|
2257
2509
|
)
|
|
2258
2510
|
{
|
|
2259
2511
|
err = Some(match e {
|
|
2260
|
-
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
2512
|
+
VmError::Parse(m, _) | VmError::Runtime(m) | VmError::Disposed(m) => m,
|
|
2261
2513
|
VmError::JsError { message, .. } => message,
|
|
2262
2514
|
VmError::Terminated => "snapshot code was terminated".to_string(),
|
|
2263
2515
|
// Unreachable: the snapshot-creator is a separate isolate
|
|
@@ -2394,6 +2646,8 @@ impl Isolate {
|
|
|
2394
2646
|
depth: std::sync::atomic::AtomicU32::new(0),
|
|
2395
2647
|
roots: Opaque::from(roots),
|
|
2396
2648
|
procs: Mutex::new(ProcTable::default()),
|
|
2649
|
+
released: Mutex::new(Vec::new()),
|
|
2650
|
+
handles_released: AtomicBool::new(false),
|
|
2397
2651
|
default_timeout_ms: timeout_ms,
|
|
2398
2652
|
memory_limit,
|
|
2399
2653
|
watchdog,
|
|
@@ -2641,6 +2895,9 @@ impl Core {
|
|
|
2641
2895
|
// on an op.
|
|
2642
2896
|
if reply.is_none() {
|
|
2643
2897
|
self.shared.lock().unwrap().disposed = true;
|
|
2898
|
+
} else if depth == 0 {
|
|
2899
|
+
// No host fn is left to throw a kept value on (`keep_thrown`).
|
|
2900
|
+
istate!(unsafe { &mut *iso }).thrown = None;
|
|
2644
2901
|
}
|
|
2645
2902
|
self.depth.fetch_sub(1, Ordering::SeqCst);
|
|
2646
2903
|
reply
|
|
@@ -2716,7 +2973,7 @@ impl Core {
|
|
|
2716
2973
|
}
|
|
2717
2974
|
|
|
2718
2975
|
// Show the GC everything this isolate holds. Called from EVERY wrapper's mark
|
|
2719
|
-
// (Isolate, Context, Module, Script all keep an Arc<Core>, and any one of them
|
|
2976
|
+
// (Isolate, Context, Module, Script, Function all keep an Arc<Core>, and any one of them
|
|
2720
2977
|
// may be the last reachable): a wrapper that forgot to would leave a live
|
|
2721
2978
|
// isolate's host fns unmarked. `mark` PINS — an Array marks its own elements
|
|
2722
2979
|
// MOVABLE, and the copies Rust takes out of it (call_proc, root_proc) would go
|
|
@@ -2741,15 +2998,24 @@ impl Core {
|
|
|
2741
2998
|
}
|
|
2742
2999
|
}
|
|
2743
3000
|
|
|
2744
|
-
fn call_proc(
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3001
|
+
fn call_proc(
|
|
3002
|
+
&self,
|
|
3003
|
+
ruby: &Ruby,
|
|
3004
|
+
host_fn: HostFnKey,
|
|
3005
|
+
args: &[JsVal],
|
|
3006
|
+
) -> Result<JsVal, HostFnFailure> {
|
|
3007
|
+
// A function of a released realm is refused here, before its slot's roots
|
|
3008
|
+
// entry is read: that entry may by now hold another realm's proc. (The lock
|
|
3009
|
+
// is uncontended and held for the comparison alone — nothing here allocates,
|
|
3010
|
+
// so no GC can need it meanwhile.)
|
|
3011
|
+
if !self.procs.lock().unwrap().current(host_fn) {
|
|
3012
|
+
return Err("unknown host function".to_string().into());
|
|
3013
|
+
}
|
|
3014
|
+
// `root_proc` and not `RArray::entry`, for the reason given there: a nil
|
|
3015
|
+
// slot must answer None, never dispatch `to_proc` and raise.
|
|
2750
3016
|
let proc = self
|
|
2751
|
-
.root_proc(proc_root_index(
|
|
2752
|
-
.
|
|
3017
|
+
.root_proc(proc_root_index(host_fn.index()))
|
|
3018
|
+
.ok_or_else(|| "unknown host function".to_string())?;
|
|
2753
3019
|
// Marshal into a Ruby Array, NOT a Vec<Value>: bare Values in a heap Vec
|
|
2754
3020
|
// are hidden from Ruby's GC mark phase (magnus's own RArray::to_vec doc
|
|
2755
3021
|
// spells this out). With several args, once arg N is parked in the Vec
|
|
@@ -2769,8 +3035,11 @@ impl Core {
|
|
|
2769
3035
|
// op the proc issues re-enters Core::run (depth > 0) directly — no nested
|
|
2770
3036
|
// frame bookkeeping is needed any more (the call stack IS the nesting).
|
|
2771
3037
|
let result: Result<Value, Error> = proc.call(unsafe { ruby_args.as_slice() });
|
|
2772
|
-
let value = result.map_err(|e|
|
|
2773
|
-
|
|
3038
|
+
let value = result.map_err(|e| HostFnFailure {
|
|
3039
|
+
message: e.to_string(),
|
|
3040
|
+
thrown: thrown_serial_of(&e),
|
|
3041
|
+
})?;
|
|
3042
|
+
ruby_to_jsval(value).map_err(|e| e.to_string().into())
|
|
2774
3043
|
}
|
|
2775
3044
|
|
|
2776
3045
|
// Context#call (and call_void). Resolves a dotted function path
|
|
@@ -2789,18 +3058,47 @@ impl Core {
|
|
|
2789
3058
|
"call requires a function name",
|
|
2790
3059
|
));
|
|
2791
3060
|
};
|
|
2792
|
-
let name = String::try_convert(*name)?;
|
|
2793
|
-
let jsargs: Vec<JsVal> = call_args
|
|
2794
|
-
.iter()
|
|
2795
|
-
.map(|v| ruby_to_jsval(*v))
|
|
2796
|
-
.collect::<Result<_, _>>()?;
|
|
2797
|
-
|
|
2798
3061
|
let reply = self.run(
|
|
2799
3062
|
ruby,
|
|
2800
3063
|
Request::Call {
|
|
2801
3064
|
context_id,
|
|
2802
|
-
name
|
|
2803
|
-
args:
|
|
3065
|
+
name: String::try_convert(*name)?,
|
|
3066
|
+
args: js_args(call_args)?,
|
|
3067
|
+
void,
|
|
3068
|
+
timeout_ms: self.default_timeout_ms,
|
|
3069
|
+
},
|
|
3070
|
+
)?;
|
|
3071
|
+
self.reply_value(ruby, reply)
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// Context#function: resolve |path| in the realm once; the handle's id.
|
|
3075
|
+
fn resolve_function(&self, ruby: &Ruby, context_id: i32, path: String) -> Result<u64, Error> {
|
|
3076
|
+
let reply = self.run(
|
|
3077
|
+
ruby,
|
|
3078
|
+
Request::ResolveFunction {
|
|
3079
|
+
context_id,
|
|
3080
|
+
path,
|
|
3081
|
+
timeout_ms: self.default_timeout_ms,
|
|
3082
|
+
},
|
|
3083
|
+
)?;
|
|
3084
|
+
u64::try_convert(self.reply_value(ruby, reply)?)
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// Function#call (and call_void): Context#call's invocation, of a resolved handle.
|
|
3088
|
+
fn call_function(
|
|
3089
|
+
&self,
|
|
3090
|
+
ruby: &Ruby,
|
|
3091
|
+
context_id: i32,
|
|
3092
|
+
function_id: u64,
|
|
3093
|
+
args: &[Value],
|
|
3094
|
+
void: bool,
|
|
3095
|
+
) -> Result<Value, Error> {
|
|
3096
|
+
let reply = self.run(
|
|
3097
|
+
ruby,
|
|
3098
|
+
Request::CallFunction {
|
|
3099
|
+
context_id,
|
|
3100
|
+
function_id,
|
|
3101
|
+
args: js_args(args)?,
|
|
2804
3102
|
void,
|
|
2805
3103
|
timeout_ms: self.default_timeout_ms,
|
|
2806
3104
|
},
|
|
@@ -2808,6 +3106,13 @@ impl Core {
|
|
|
2808
3106
|
self.reply_value(ruby, reply)
|
|
2809
3107
|
}
|
|
2810
3108
|
|
|
3109
|
+
// A wrapper let go of its handle (see `released`). Takes only this lock,
|
|
3110
|
+
// briefly, and allocates no Ruby object — it runs from a GC free.
|
|
3111
|
+
fn release(&self, handle: Released) {
|
|
3112
|
+
self.released.lock().unwrap().push(handle);
|
|
3113
|
+
self.handles_released.store(true, Ordering::Relaxed);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
2811
3116
|
fn drain_microtasks(&self, ruby: &Ruby) -> Result<Value, Error> {
|
|
2812
3117
|
let reply = self.run(
|
|
2813
3118
|
ruby,
|
|
@@ -2880,7 +3185,7 @@ impl Core {
|
|
|
2880
3185
|
source: String,
|
|
2881
3186
|
filename: String,
|
|
2882
3187
|
timeout_ms: u64,
|
|
2883
|
-
|
|
3188
|
+
completion: Completion,
|
|
2884
3189
|
) -> Result<Value, Error> {
|
|
2885
3190
|
let reply = self.run(
|
|
2886
3191
|
ruby,
|
|
@@ -2889,7 +3194,7 @@ impl Core {
|
|
|
2889
3194
|
source,
|
|
2890
3195
|
filename,
|
|
2891
3196
|
timeout_ms,
|
|
2892
|
-
|
|
3197
|
+
completion,
|
|
2893
3198
|
},
|
|
2894
3199
|
)?;
|
|
2895
3200
|
self.reply_value(ruby, reply)
|
|
@@ -2906,18 +3211,14 @@ impl Core {
|
|
|
2906
3211
|
// array's element storage, which the import hooks read with the GVL released —
|
|
2907
3212
|
// and a refused attach must not consume a slot or leave its proc rooted either.
|
|
2908
3213
|
self.ensure_owner_and_live(ruby)?;
|
|
2909
|
-
let
|
|
2910
|
-
|
|
2911
|
-
live: true,
|
|
2912
|
-
name: name.clone(),
|
|
2913
|
-
});
|
|
2914
|
-
self.root_store(ruby, proc_root_index(host_fn_id), proc)?;
|
|
3214
|
+
let host_fn = self.procs.lock().unwrap().alloc(context_id, name.clone());
|
|
3215
|
+
self.root_store(ruby, proc_root_index(host_fn.index()), proc)?;
|
|
2915
3216
|
let reply = self.run(
|
|
2916
3217
|
ruby,
|
|
2917
3218
|
Request::Attach {
|
|
2918
3219
|
context_id,
|
|
2919
3220
|
name,
|
|
2920
|
-
|
|
3221
|
+
host_fn,
|
|
2921
3222
|
timeout_ms: self.default_timeout_ms,
|
|
2922
3223
|
},
|
|
2923
3224
|
)?;
|
|
@@ -2926,7 +3227,7 @@ impl Core {
|
|
|
2926
3227
|
|
|
2927
3228
|
// attach_many: install several host fns in ONE round-trip to the V8 thread
|
|
2928
3229
|
// (a fresh realm needs ~dozens; one rendezvous instead of one per fn). Slots
|
|
2929
|
-
// are allocated up front so each carries a stable
|
|
3230
|
+
// are allocated up front so each carries a stable key; entries are
|
|
2930
3231
|
// applied IN ORDER and a build/attach failure aborts the batch with that
|
|
2931
3232
|
// (name-tagged) error WITHOUT rolling back earlier entries (see the
|
|
2932
3233
|
// AttachMany arm). On that error path the unused slots are reclaimed at the
|
|
@@ -2941,18 +3242,11 @@ impl Core {
|
|
|
2941
3242
|
return Ok(ruby.qnil().as_value()); // nothing to install, skip the round-trip
|
|
2942
3243
|
}
|
|
2943
3244
|
self.ensure_owner_and_live(ruby)?; // before the roots writes — see `attach`
|
|
2944
|
-
let allocated: Vec<(String,
|
|
3245
|
+
let allocated: Vec<(String, HostFnKey, Proc)> = {
|
|
2945
3246
|
let mut procs = self.procs.lock().unwrap();
|
|
2946
3247
|
entries
|
|
2947
3248
|
.into_iter()
|
|
2948
|
-
.map(|(name, proc)|
|
|
2949
|
-
let id = procs.alloc(ProcSlot {
|
|
2950
|
-
context_id,
|
|
2951
|
-
live: true,
|
|
2952
|
-
name: name.clone(),
|
|
2953
|
-
});
|
|
2954
|
-
(name, id, proc)
|
|
2955
|
-
})
|
|
3249
|
+
.map(|(name, proc)| (name.clone(), procs.alloc(context_id, name), proc))
|
|
2956
3250
|
.collect()
|
|
2957
3251
|
};
|
|
2958
3252
|
// Stored OUTSIDE the table's lock: storing into the array can allocate (the
|
|
@@ -2960,16 +3254,16 @@ impl Core {
|
|
|
2960
3254
|
// wrappers, a path that must never wait on `procs`. The Procs parked in the
|
|
2961
3255
|
// Vec meanwhile are invisible to the GC (see call_proc's note), but the caller's
|
|
2962
3256
|
// `table: RHash` argument is live in its own frame and marks every one of them.
|
|
2963
|
-
let mut
|
|
2964
|
-
for (name,
|
|
2965
|
-
self.root_store(ruby, proc_root_index(
|
|
2966
|
-
|
|
3257
|
+
let mut named_keys: Vec<(String, HostFnKey)> = Vec::with_capacity(allocated.len());
|
|
3258
|
+
for (name, key, proc) in allocated {
|
|
3259
|
+
self.root_store(ruby, proc_root_index(key.index()), proc)?;
|
|
3260
|
+
named_keys.push((name, key));
|
|
2967
3261
|
}
|
|
2968
3262
|
let reply = self.run(
|
|
2969
3263
|
ruby,
|
|
2970
3264
|
Request::AttachMany {
|
|
2971
3265
|
context_id,
|
|
2972
|
-
entries:
|
|
3266
|
+
entries: named_keys,
|
|
2973
3267
|
timeout_ms: self.default_timeout_ms,
|
|
2974
3268
|
},
|
|
2975
3269
|
)?;
|
|
@@ -2977,11 +3271,12 @@ impl Core {
|
|
|
2977
3271
|
}
|
|
2978
3272
|
|
|
2979
3273
|
// Drop the procs attached into |context_id| out of the roots array — its
|
|
2980
|
-
// realm is gone (reset or disposed),
|
|
2981
|
-
//
|
|
2982
|
-
//
|
|
2983
|
-
//
|
|
2984
|
-
// the
|
|
3274
|
+
// realm is gone (reset or disposed), and holding them would keep the embedder's
|
|
3275
|
+
// world alive for the rest of the isolate's life. A V8 function of that realm
|
|
3276
|
+
// may still be reachable (the embedder can hold the old global), but its key
|
|
3277
|
+
// stops answering (`ProcTable::current`), so it never calls a proc again.
|
|
3278
|
+
// Runs on a Ruby thread: it writes nil into the Array. The slots stay: the
|
|
3279
|
+
// keys of other realms are indices into the same Vec.
|
|
2985
3280
|
fn release_context_procs(&self, ruby: &Ruby, context_id: i32) {
|
|
2986
3281
|
// Nil the entries BEFORE the ids go back on the free list, so no attach can
|
|
2987
3282
|
// ever be handed an id whose old proc is still in the array.
|
|
@@ -3094,12 +3389,18 @@ impl Core {
|
|
|
3094
3389
|
self.reply_value(ruby, reply?)
|
|
3095
3390
|
}
|
|
3096
3391
|
|
|
3097
|
-
fn evaluate_module(
|
|
3392
|
+
fn evaluate_module(
|
|
3393
|
+
&self,
|
|
3394
|
+
ruby: &Ruby,
|
|
3395
|
+
module_id: i32,
|
|
3396
|
+
report: Option<String>,
|
|
3397
|
+
) -> Result<Value, Error> {
|
|
3098
3398
|
let reply = self.run(
|
|
3099
3399
|
ruby,
|
|
3100
3400
|
Request::EvaluateModule {
|
|
3101
3401
|
module_id,
|
|
3102
3402
|
timeout_ms: self.default_timeout_ms,
|
|
3403
|
+
report,
|
|
3103
3404
|
},
|
|
3104
3405
|
)?;
|
|
3105
3406
|
self.reply_value(ruby, reply)
|
|
@@ -3120,11 +3421,6 @@ impl Core {
|
|
|
3120
3421
|
self.reply_value(ruby, reply)
|
|
3121
3422
|
}
|
|
3122
3423
|
|
|
3123
|
-
fn dispose_module(&self, ruby: &Ruby, module_id: i32) -> Result<(), Error> {
|
|
3124
|
-
let reply = self.run(ruby, Request::DisposeModule { module_id })?;
|
|
3125
|
-
self.reply_value(ruby, reply).map(|_| ())
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
3424
|
// Classic script: compile, run, dispose.
|
|
3129
3425
|
#[allow(clippy::too_many_arguments)]
|
|
3130
3426
|
fn compile_script(
|
|
@@ -3170,11 +3466,6 @@ impl Core {
|
|
|
3170
3466
|
self.reply_value(ruby, reply)
|
|
3171
3467
|
}
|
|
3172
3468
|
|
|
3173
|
-
fn dispose_script(&self, ruby: &Ruby, script_id: i32) -> Result<(), Error> {
|
|
3174
|
-
let reply = self.run(ruby, Request::DisposeScript { script_id })?;
|
|
3175
|
-
self.reply_value(ruby, reply).map(|_| ())
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
3469
|
// Serialize a fresh bytecode cache from a compiled handle's current state
|
|
3179
3470
|
// (Script#/Module#create_code_cache). None = V8 couldn't produce one (or the
|
|
3180
3471
|
// realm is gone).
|
|
@@ -3245,6 +3536,7 @@ impl Core {
|
|
|
3245
3536
|
st.realms = V8State::default();
|
|
3246
3537
|
st.modules = ModuleReg::default();
|
|
3247
3538
|
st.scripts = ScriptReg::default();
|
|
3539
|
+
st.functions = FunctionReg::default();
|
|
3248
3540
|
st.instantiate_resolve = None;
|
|
3249
3541
|
}
|
|
3250
3542
|
// Unregister the near-heap-limit callback before disposal: dropping the box
|
|
@@ -3369,6 +3661,11 @@ fn pending_transfer_count() -> usize {
|
|
|
3369
3661
|
transfer_registry().lock().unwrap().len()
|
|
3370
3662
|
}
|
|
3371
3663
|
|
|
3664
|
+
// A call's Ruby arguments, marshalled for JS.
|
|
3665
|
+
fn js_args(args: &[Value]) -> Result<Vec<JsVal>, Error> {
|
|
3666
|
+
args.iter().map(|v| ruby_to_jsval(*v)).collect()
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3372
3669
|
// Thin magnus-method wrappers.
|
|
3373
3670
|
// Isolate = the VM and its isolate-level operations; it hands out Contexts.
|
|
3374
3671
|
impl Isolate {
|
|
@@ -3443,14 +3740,14 @@ impl Context {
|
|
|
3443
3740
|
Ok(())
|
|
3444
3741
|
}
|
|
3445
3742
|
// timeout_ms 0 = use the isolate's default; an explicit value overrides it.
|
|
3446
|
-
// |
|
|
3743
|
+
// |completion| 0 marshals the completion value, 1 discards it, 2 keeps a primitive alone (`Completion`).
|
|
3447
3744
|
fn eval(
|
|
3448
3745
|
ruby: &Ruby,
|
|
3449
3746
|
rb_self: &Self,
|
|
3450
3747
|
source: String,
|
|
3451
3748
|
timeout_ms: u64,
|
|
3452
3749
|
filename: String,
|
|
3453
|
-
|
|
3750
|
+
completion: u8,
|
|
3454
3751
|
) -> Result<Value, Error> {
|
|
3455
3752
|
rb_self.check_live(ruby)?;
|
|
3456
3753
|
let timeout = if timeout_ms == 0 {
|
|
@@ -3458,9 +3755,14 @@ impl Context {
|
|
|
3458
3755
|
} else {
|
|
3459
3756
|
timeout_ms
|
|
3460
3757
|
};
|
|
3758
|
+
let completion = match completion {
|
|
3759
|
+
1 => Completion::Void,
|
|
3760
|
+
2 => Completion::Primitive,
|
|
3761
|
+
_ => Completion::Value,
|
|
3762
|
+
};
|
|
3461
3763
|
rb_self
|
|
3462
3764
|
.core
|
|
3463
|
-
.eval_t(ruby, rb_self.id, source, filename, timeout,
|
|
3765
|
+
.eval_t(ruby, rb_self.id, source, filename, timeout, completion)
|
|
3464
3766
|
}
|
|
3465
3767
|
fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
3466
3768
|
rb_self.check_live(ruby)?;
|
|
@@ -3470,6 +3772,18 @@ impl Context {
|
|
|
3470
3772
|
rb_self.check_live(ruby)?;
|
|
3471
3773
|
rb_self.core.call(ruby, rb_self.id, args, true)
|
|
3472
3774
|
}
|
|
3775
|
+
// function(path) -> Function: what `call(path, ...)` would call, resolved
|
|
3776
|
+
// now, once — its holder kept as its `this` — so each call skips the path walk.
|
|
3777
|
+
fn function(ruby: &Ruby, rb_self: &Self, path: String) -> Result<JsFunction, Error> {
|
|
3778
|
+
rb_self.check_live(ruby)?;
|
|
3779
|
+
let function_id = rb_self.core.resolve_function(ruby, rb_self.id, path)?;
|
|
3780
|
+
Ok(JsFunction {
|
|
3781
|
+
core: rb_self.core.clone(),
|
|
3782
|
+
context_id: rb_self.id,
|
|
3783
|
+
function_id,
|
|
3784
|
+
disposed: AtomicBool::new(false),
|
|
3785
|
+
})
|
|
3786
|
+
}
|
|
3473
3787
|
fn attach(ruby: &Ruby, rb_self: &Self, name: String, proc: Proc) -> Result<Value, Error> {
|
|
3474
3788
|
rb_self.check_live(ruby)?;
|
|
3475
3789
|
rb_self.core.attach(ruby, rb_self.id, name, proc)
|
|
@@ -3649,9 +3963,12 @@ impl JsModule {
|
|
|
3649
3963
|
.core
|
|
3650
3964
|
.instantiate_module(ruby, rb_self.module_id, resolver)
|
|
3651
3965
|
}
|
|
3652
|
-
|
|
3966
|
+
// |report|: nil, or the name of the context global that reports the evaluation's rejection (`Request::EvaluateModule`).
|
|
3967
|
+
fn evaluate(ruby: &Ruby, rb_self: &Self, report: Option<String>) -> Result<Value, Error> {
|
|
3653
3968
|
rb_self.check_live(ruby)?;
|
|
3654
|
-
rb_self
|
|
3969
|
+
rb_self
|
|
3970
|
+
.core
|
|
3971
|
+
.evaluate_module(ruby, rb_self.module_id, report)
|
|
3655
3972
|
}
|
|
3656
3973
|
fn namespace(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3657
3974
|
rb_self.check_live(ruby)?;
|
|
@@ -3695,12 +4012,12 @@ impl JsModule {
|
|
|
3695
4012
|
let bytes = rb_self.core.module_code_cache(ruby, rb_self.module_id)?;
|
|
3696
4013
|
Ok(code_cache_value(ruby, bytes.as_ref()))
|
|
3697
4014
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
4015
|
+
// Let go of the module now rather than when this wrapper is collected (see
|
|
4016
|
+
// JsFunction::dispose). Its url still answers while V8 keeps the module.
|
|
4017
|
+
fn dispose(&self) {
|
|
4018
|
+
if !self.disposed.swap(true, Ordering::SeqCst) {
|
|
4019
|
+
self.core.release(Released::Module(self.module_id));
|
|
3701
4020
|
}
|
|
3702
|
-
let _ = rb_self.core.dispose_module(ruby, rb_self.module_id);
|
|
3703
|
-
Ok(())
|
|
3704
4021
|
}
|
|
3705
4022
|
fn disposed(&self) -> bool {
|
|
3706
4023
|
self.disposed.load(Ordering::SeqCst)
|
|
@@ -3746,18 +4063,72 @@ impl Script {
|
|
|
3746
4063
|
let bytes = rb_self.core.script_code_cache(ruby, rb_self.script_id)?;
|
|
3747
4064
|
Ok(code_cache_value(ruby, bytes.as_ref()))
|
|
3748
4065
|
}
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
4066
|
+
// Let go of the script now rather than when this wrapper is collected (see
|
|
4067
|
+
// JsFunction::dispose).
|
|
4068
|
+
fn dispose(&self) {
|
|
4069
|
+
if !self.disposed.swap(true, Ordering::SeqCst) {
|
|
4070
|
+
self.core.release(Released::Script(self.script_id));
|
|
3752
4071
|
}
|
|
3753
|
-
let _ = rb_self.core.dispose_script(ruby, rb_self.script_id);
|
|
3754
|
-
Ok(())
|
|
3755
4072
|
}
|
|
3756
4073
|
fn disposed(&self) -> bool {
|
|
3757
4074
|
self.disposed.load(Ordering::SeqCst)
|
|
3758
4075
|
}
|
|
3759
4076
|
}
|
|
3760
4077
|
|
|
4078
|
+
impl JsFunction {
|
|
4079
|
+
fn check_live(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
4080
|
+
// Its realm's reset or dispose is found by the call itself (the handle is
|
|
4081
|
+
// gone from FunctionReg): that is not something this wrapper can see.
|
|
4082
|
+
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
4083
|
+
return Err(Error::new(
|
|
4084
|
+
err_class(ruby, "DisposedError"),
|
|
4085
|
+
"disposed function",
|
|
4086
|
+
));
|
|
4087
|
+
}
|
|
4088
|
+
Ok(())
|
|
4089
|
+
}
|
|
4090
|
+
fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
4091
|
+
rb_self.check_live(ruby)?;
|
|
4092
|
+
rb_self
|
|
4093
|
+
.core
|
|
4094
|
+
.call_function(ruby, rb_self.context_id, rb_self.function_id, args, false)
|
|
4095
|
+
}
|
|
4096
|
+
fn call_void(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
4097
|
+
rb_self.check_live(ruby)?;
|
|
4098
|
+
rb_self
|
|
4099
|
+
.core
|
|
4100
|
+
.call_function(ruby, rb_self.context_id, rb_self.function_id, args, true)
|
|
4101
|
+
}
|
|
4102
|
+
// Let go of the function now rather than when this wrapper is collected. Any
|
|
4103
|
+
// thread, any time: the Globals go at the end of the isolate's next op.
|
|
4104
|
+
fn dispose(&self) {
|
|
4105
|
+
if !self.disposed.swap(true, Ordering::SeqCst) {
|
|
4106
|
+
self.core.release(Released::Function(self.function_id));
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
fn disposed(&self) -> bool {
|
|
4110
|
+
self.disposed.load(Ordering::SeqCst) || self.core.is_disposed()
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
impl Drop for JsFunction {
|
|
4115
|
+
fn drop(&mut self) {
|
|
4116
|
+
self.dispose();
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
impl Drop for JsModule {
|
|
4121
|
+
fn drop(&mut self) {
|
|
4122
|
+
self.dispose();
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
impl Drop for Script {
|
|
4127
|
+
fn drop(&mut self) {
|
|
4128
|
+
self.dispose();
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
|
|
3761
4132
|
// A bytecode cache as a binary (ASCII-8BIT) Ruby String, or nil when there's
|
|
3762
4133
|
// none — the shared return shape of #cached_data and #create_code_cache.
|
|
3763
4134
|
fn code_cache_value(ruby: &Ruby, bytes: Option<&Vec<u8>>) -> Value {
|
|
@@ -3808,9 +4179,15 @@ fn binary_bytes(
|
|
|
3808
4179
|
|
|
3809
4180
|
fn vm_err(ruby: &Ruby, e: VmError) -> Error {
|
|
3810
4181
|
match e {
|
|
3811
|
-
VmError::Parse(m) =>
|
|
4182
|
+
VmError::Parse(m, thrown) => {
|
|
4183
|
+
carrying_thrown(Error::new(err_class(ruby, "ParseError"), m), thrown)
|
|
4184
|
+
}
|
|
3812
4185
|
VmError::Runtime(m) => Error::new(err_class(ruby, "RuntimeError"), m),
|
|
3813
|
-
VmError::JsError {
|
|
4186
|
+
VmError::JsError {
|
|
4187
|
+
message,
|
|
4188
|
+
backtrace,
|
|
4189
|
+
thrown,
|
|
4190
|
+
} => carrying_thrown(js_runtime_error(ruby, message, backtrace), thrown),
|
|
3814
4191
|
VmError::Terminated => Error::new(
|
|
3815
4192
|
err_class(ruby, "ScriptTerminatedError"),
|
|
3816
4193
|
"JavaScript was terminated (timeout or stop)",
|
|
@@ -3819,6 +4196,7 @@ fn vm_err(ruby: &Ruby, e: VmError) -> Error {
|
|
|
3819
4196
|
err_class(ruby, "V8OutOfMemoryError"),
|
|
3820
4197
|
"JavaScript exceeded the isolate memory_limit",
|
|
3821
4198
|
),
|
|
4199
|
+
VmError::Disposed(m) => Error::new(err_class(ruby, "DisposedError"), m),
|
|
3822
4200
|
}
|
|
3823
4201
|
}
|
|
3824
4202
|
|
|
@@ -3852,6 +4230,42 @@ fn terminated_error(ruby: &Ruby, js_backtrace: Option<Vec<String>>) -> Error {
|
|
|
3852
4230
|
}
|
|
3853
4231
|
}
|
|
3854
4232
|
|
|
4233
|
+
// The ivar a Ruby exception carries its thrown value's serial in (`keep_thrown`),
|
|
4234
|
+
// beside the message it was raised with.
|
|
4235
|
+
const THROWN_IVAR: &str = "@__rusty_racer_thrown";
|
|
4236
|
+
|
|
4237
|
+
// |error| as an instance carrying |thrown|'s serial, for a host fn it propagates
|
|
4238
|
+
// out of to throw the kept value on (`take_thrown`).
|
|
4239
|
+
fn carrying_thrown(error: Error, thrown: Option<u64>) -> Error {
|
|
4240
|
+
let Some(serial) = thrown else {
|
|
4241
|
+
return error;
|
|
4242
|
+
};
|
|
4243
|
+
match error_to_exception(&error) {
|
|
4244
|
+
Some(exc) => {
|
|
4245
|
+
let Ok(message) = exc.funcall::<_, _, Value>("message", ()) else {
|
|
4246
|
+
return Error::from(exc);
|
|
4247
|
+
};
|
|
4248
|
+
let _ = exc
|
|
4249
|
+
.funcall::<_, _, Value>("instance_variable_set", (THROWN_IVAR, (serial, message)));
|
|
4250
|
+
Error::from(exc)
|
|
4251
|
+
}
|
|
4252
|
+
None => error,
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
// The serial of the thrown value a host fn's raised |error| carries, if any — not
|
|
4257
|
+
// when Ruby re-raised it with a message of its own (`raise e, 'other'` copies
|
|
4258
|
+
// the ivar): that error says something the value does not.
|
|
4259
|
+
fn thrown_serial_of(error: &Error) -> Option<u64> {
|
|
4260
|
+
let exc = error_to_exception(error)?;
|
|
4261
|
+
let (serial, message): (u64, Value) = exc
|
|
4262
|
+
.funcall::<_, _, Option<(u64, Value)>>("instance_variable_get", (THROWN_IVAR,))
|
|
4263
|
+
.ok()
|
|
4264
|
+
.flatten()?;
|
|
4265
|
+
let now: Value = exc.funcall("message", ()).ok()?;
|
|
4266
|
+
now.eql(message).ok()?.then_some(serial)
|
|
4267
|
+
}
|
|
4268
|
+
|
|
3855
4269
|
// Build a RustyRacer::RuntimeError carrying the JS stack as its Ruby backtrace.
|
|
3856
4270
|
// Constructs the exception instance so we can set_backtrace before raising;
|
|
3857
4271
|
// falls back to a plain Error if any of that fails.
|
|
@@ -3961,10 +4375,11 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3961
4375
|
// A v8::Context (realm): eval/call/attach/compile_module.
|
|
3962
4376
|
let context = module.define_class("Context", ruby.class_object())?;
|
|
3963
4377
|
// Backs both keyword-arg wrappers in lib: Context#eval(source, timeout_ms:,
|
|
3964
|
-
// filename:) and #
|
|
4378
|
+
// filename:), #eval_void and #eval_primitive, which differ only in the 4th arg (what they keep of the completion value).
|
|
3965
4379
|
context.define_method("_eval", method!(Context::eval, 4))?;
|
|
3966
4380
|
context.define_method("call", method!(Context::call, -1))?;
|
|
3967
4381
|
context.define_method("call_void", method!(Context::call_void, -1))?;
|
|
4382
|
+
context.define_method("function", method!(Context::function, 1))?;
|
|
3968
4383
|
context.define_method("attach", method!(Context::attach, 2))?;
|
|
3969
4384
|
context.define_method("attach_many", method!(Context::attach_many, 1))?;
|
|
3970
4385
|
context.define_method("reset", method!(Context::reset, 0))?;
|
|
@@ -3985,6 +4400,13 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3985
4400
|
script.define_method("dispose", method!(Script::dispose, 0))?;
|
|
3986
4401
|
script.define_method("disposed?", method!(Script::disposed, 0))?;
|
|
3987
4402
|
|
|
4403
|
+
// A resolved function: Context#function -> #call / #call_void.
|
|
4404
|
+
let function = module.define_class("Function", ruby.class_object())?;
|
|
4405
|
+
function.define_method("call", method!(JsFunction::call, -1))?;
|
|
4406
|
+
function.define_method("call_void", method!(JsFunction::call_void, -1))?;
|
|
4407
|
+
function.define_method("dispose", method!(JsFunction::dispose, 0))?;
|
|
4408
|
+
function.define_method("disposed?", method!(JsFunction::disposed, 0))?;
|
|
4409
|
+
|
|
3988
4410
|
// V8 startup blob: Snapshot.new(code) -> Isolate.new(snapshot:).
|
|
3989
4411
|
let snapshot = module.define_class("Snapshot", ruby.class_object())?;
|
|
3990
4412
|
snapshot.define_singleton_method("new", function!(Snapshot::new, -1))?;
|
|
@@ -3996,7 +4418,7 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3996
4418
|
// Thin ES-module handle: Context#compile_module -> instantiate/evaluate.
|
|
3997
4419
|
let jsmodule = module.define_class("Module", ruby.class_object())?;
|
|
3998
4420
|
jsmodule.define_method("_instantiate", method!(JsModule::instantiate, 1))?;
|
|
3999
|
-
jsmodule.define_method("
|
|
4421
|
+
jsmodule.define_method("_evaluate", method!(JsModule::evaluate, 1))?;
|
|
4000
4422
|
jsmodule.define_method("namespace", method!(JsModule::namespace, 0))?;
|
|
4001
4423
|
jsmodule.define_method("_status", method!(JsModule::status, 0))?;
|
|
4002
4424
|
jsmodule.define_method("graph_async?", method!(JsModule::graph_async, 0))?;
|