mini_racer_rs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AI-DISCLOSURE.md +13 -0
- data/CHANGELOG.md +45 -0
- data/COMPATIBILITY.md +235 -0
- data/Cargo.lock +1263 -0
- data/Cargo.toml +62 -0
- data/LICENSE +21 -0
- data/README.md +254 -0
- data/THIRD_PARTY_NOTICES.md +4501 -0
- data/crates/mini_racer_engine/Cargo.toml +25 -0
- data/crates/mini_racer_engine/build.rs +13 -0
- data/crates/mini_racer_engine/src/context.rs +802 -0
- data/crates/mini_racer_engine/src/control.rs +239 -0
- data/crates/mini_racer_engine/src/error.rs +88 -0
- data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
- data/crates/mini_racer_engine/src/lib.rs +20 -0
- data/crates/mini_racer_engine/src/notify.rs +107 -0
- data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
- data/crates/mini_racer_engine/src/process/fork.rs +261 -0
- data/crates/mini_racer_engine/src/process/platform.rs +194 -0
- data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
- data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
- data/crates/mini_racer_engine/src/process/workers.rs +165 -0
- data/crates/mini_racer_engine/src/process.rs +69 -0
- data/crates/mini_racer_engine/src/runner.rs +1560 -0
- data/crates/mini_racer_engine/src/serialize.rs +253 -0
- data/crates/mini_racer_engine/src/snapshot.rs +221 -0
- data/crates/mini_racer_engine/src/spin.rs +23 -0
- data/crates/mini_racer_engine/src/sys.rs +566 -0
- data/crates/mini_racer_sync/Cargo.toml +18 -0
- data/crates/mini_racer_sync/src/channel.rs +621 -0
- data/crates/mini_racer_sync/src/fork.rs +116 -0
- data/crates/mini_racer_sync/src/lib.rs +20 -0
- data/crates/mini_racer_sync/src/lock.rs +49 -0
- data/crates/mini_racer_sync/src/parker.rs +44 -0
- data/crates/mini_racer_sync/src/primitives.rs +10 -0
- data/crates/mini_racer_sync/src/tasks.rs +345 -0
- data/crates/mini_racer_sync/src/termination.rs +1234 -0
- data/crates/mini_racer_sync/src/workers.rs +283 -0
- data/crates/mini_racer_wire/Cargo.toml +18 -0
- data/crates/mini_racer_wire/src/decode.rs +926 -0
- data/crates/mini_racer_wire/src/lib.rs +67 -0
- data/crates/mini_racer_wire/src/marker.rs +43 -0
- data/crates/mini_racer_wire/src/text.rs +106 -0
- data/crates/mini_racer_wire/src/write.rs +187 -0
- data/ext/mini_racer_rs/Cargo.toml +26 -0
- data/ext/mini_racer_rs/build.rs +16 -0
- data/ext/mini_racer_rs/extconf.rb +78 -0
- data/ext/mini_racer_rs/src/context.rs +485 -0
- data/ext/mini_racer_rs/src/decode.rs +450 -0
- data/ext/mini_racer_rs/src/encode.rs +333 -0
- data/ext/mini_racer_rs/src/errors.rs +156 -0
- data/ext/mini_racer_rs/src/fork.rs +31 -0
- data/ext/mini_racer_rs/src/gc_root.rs +117 -0
- data/ext/mini_racer_rs/src/guard.rs +7 -0
- data/ext/mini_racer_rs/src/host.rs +339 -0
- data/ext/mini_racer_rs/src/lib.rs +44 -0
- data/ext/mini_racer_rs/src/options.rs +269 -0
- data/ext/mini_racer_rs/src/platform.rs +42 -0
- data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
- data/ext/mini_racer_rs/src/snapshot.rs +149 -0
- data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
- data/lib/mini_racer/shared.rb +420 -0
- data/lib/mini_racer/truffleruby.rb +439 -0
- data/lib/mini_racer/version.rb +6 -0
- data/lib/mini_racer.rb +3 -0
- data/lib/mini_racer_rs/internal_error.rb +7 -0
- data/lib/mini_racer_rs/version.rb +5 -0
- data/lib/mini_racer_rs/vocabulary.rb +105 -0
- data/lib/mini_racer_rs.rb +33 -0
- data/licenses/mini_racer.txt +21 -0
- data/licenses/rust/SOURCES.tsv +6 -0
- data/licenses/rust/std/COPYRIGHT +34 -0
- data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
- data/licenses/rust/std/LICENSE-APACHE +176 -0
- data/licenses/rust/std/LICENSE-MIT +25 -0
- data/licenses/v8/SOURCES.tsv +20 -0
- data/licenses/v8/abseil-cpp/LICENSE +203 -0
- data/licenses/v8/chromium-build/LICENSE +27 -0
- data/licenses/v8/components.tsv +29 -0
- data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
- data/licenses/v8/fast_float/LICENSE-MIT +27 -0
- data/licenses/v8/highway/LICENSE +371 -0
- data/licenses/v8/icu/LICENSE +568 -0
- data/licenses/v8/inspector_protocol/LICENSE +27 -0
- data/licenses/v8/libc++/LICENSE.TXT +311 -0
- data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
- data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
- data/licenses/v8/rapidhash/LICENSE +34 -0
- data/licenses/v8/simdutf/LICENSE +18 -0
- data/licenses/v8/siphash/LICENSE +116 -0
- data/licenses/v8/utf8-decoder/LICENSE +19 -0
- data/licenses/v8/v8/LICENSE +61 -0
- data/licenses/v8/v8/LICENSE.fdlibm +6 -0
- data/licenses/v8/v8/LICENSE.strongtalk +29 -0
- data/licenses/v8/v8/LICENSE.v8 +26 -0
- metadata +156 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
use std::io;
|
|
2
|
+
use std::os::fd::OwnedFd;
|
|
3
|
+
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
|
4
|
+
use std::sync::{Arc, OnceLock};
|
|
5
|
+
use std::time::Duration;
|
|
6
|
+
|
|
7
|
+
use mini_racer_sync::{
|
|
8
|
+
Channel, ChannelError, DispatchKind, Inherited, Interrupt, Phase, Side, TermError, ToRuby, lock,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
use crate::control::{CallbackReply, ContextConfig, ControlBlock, Depth, JsText, Request, Wire};
|
|
12
|
+
use crate::notify::NotifyFd;
|
|
13
|
+
use crate::process::fork::{self, Generation};
|
|
14
|
+
use crate::process::platform::Mode;
|
|
15
|
+
use crate::process::{self, platform, watchdog};
|
|
16
|
+
use crate::runner::Start;
|
|
17
|
+
use crate::spin::spin_until;
|
|
18
|
+
use crate::{EngineError, InvalidSnapshot, SnapshotBlob, runner};
|
|
19
|
+
|
|
20
|
+
const MIN_RUNNER_STACK_BYTES: usize = 2 << 20;
|
|
21
|
+
const STACK_HEADROOM_BYTES: usize = 1 << 20;
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
24
|
+
pub enum WaitOutcome {
|
|
25
|
+
Ready,
|
|
26
|
+
/// Abandon the dispatch: the engine terminates it and then waits for its
|
|
27
|
+
/// response through `Host::wait_blocking`.
|
|
28
|
+
Cancel,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// How the calling thread waits for its runner. The extension spins, releases
|
|
32
|
+
/// the GVL and parks, and turns Ruby interrupts into `Cancel`.
|
|
33
|
+
pub trait Host {
|
|
34
|
+
/// Returns `Ready` once `wait.ready()` holds, or `Cancel`.
|
|
35
|
+
fn wait(&mut self, wait: &WaitHandle<'_>) -> WaitOutcome;
|
|
36
|
+
/// Returns once `wait.ready()` holds, whatever interrupts arrive.
|
|
37
|
+
fn wait_blocking(&mut self, wait: &WaitHandle<'_>);
|
|
38
|
+
/// Runs the callback `request` names: an array of its arguments with its
|
|
39
|
+
/// id last, in V8's wire format. Dispatches it makes on this context from
|
|
40
|
+
/// this thread run nested inside the current one. `Abort` cancels the
|
|
41
|
+
/// dispatch, and is all a host that attaches nothing needs.
|
|
42
|
+
fn callback(&mut self, _request: &[u8]) -> CallbackReply {
|
|
43
|
+
CallbackReply::Abort
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub struct WaitHandle<'a> {
|
|
48
|
+
channel: &'a Channel<Wire>,
|
|
49
|
+
notify: &'a NotifyFd,
|
|
50
|
+
epoch: u64,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl<'a> WaitHandle<'a> {
|
|
54
|
+
fn new(block: &'a ControlBlock) -> Self {
|
|
55
|
+
Self {
|
|
56
|
+
channel: &block.channel,
|
|
57
|
+
notify: &block.notify,
|
|
58
|
+
epoch: fork::epoch(),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A fork from a callback leaves the child waiting on a runner it does not
|
|
63
|
+
// have; it stops waiting and the dispatch ends orphaned.
|
|
64
|
+
fn forked(&self) -> bool {
|
|
65
|
+
fork::epoch() != self.epoch
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[must_use]
|
|
69
|
+
pub fn ready(&self) -> bool {
|
|
70
|
+
self.channel.ready(Side::Ruby) || self.forked()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Spins up to `budget`; returns whether the runner answered meanwhile.
|
|
74
|
+
#[must_use]
|
|
75
|
+
pub fn spin(&self, budget: Duration) -> bool {
|
|
76
|
+
spin_until(budget, || self.ready())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Sleeps until the runner answers or `Unparker::unpark`; may return early.
|
|
80
|
+
pub fn park(&self) {
|
|
81
|
+
self.channel.park(Side::Ruby, None);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// `park` for a host that sleeps on a file descriptor instead, such as a
|
|
85
|
+
/// fiber scheduler: `wait` should return once a `notifier` descriptor is
|
|
86
|
+
/// readable, which it becomes when the runner answers, and may return
|
|
87
|
+
/// earlier.
|
|
88
|
+
pub fn park_notified(&self, wait: impl FnOnce()) -> io::Result<()> {
|
|
89
|
+
// The descriptor is shared with the parent: draining it in a child
|
|
90
|
+
// would take the parent's ring.
|
|
91
|
+
if self.forked() {
|
|
92
|
+
return Ok(());
|
|
93
|
+
}
|
|
94
|
+
// Drained before the channel is marked, so a ring for this handover
|
|
95
|
+
// always comes after the drain.
|
|
96
|
+
self.notify.drain(self.channel)?;
|
|
97
|
+
self.channel.park_notified(Side::Ruby, wait);
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// A descriptor the host owns that `park_notified` waits are rung on.
|
|
102
|
+
pub fn notifier(&self) -> io::Result<OwnedFd> {
|
|
103
|
+
if self.forked() {
|
|
104
|
+
return Err(io::Error::other(
|
|
105
|
+
"the context's runner stayed with the parent process",
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
self.notify.duplicate(self.channel)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[must_use]
|
|
112
|
+
pub const fn unparker(&self) -> Unparker<'a> {
|
|
113
|
+
Unparker {
|
|
114
|
+
channel: self.channel,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// The only thing an interrupt handler on another thread may touch.
|
|
120
|
+
#[derive(Clone, Copy)]
|
|
121
|
+
pub struct Unparker<'a> {
|
|
122
|
+
channel: &'a Channel<Wire>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl Unparker<'_> {
|
|
126
|
+
pub fn unpark(self) {
|
|
127
|
+
self.channel.wake(Side::Ruby);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pub struct ContextHandle {
|
|
132
|
+
lineage: Lineage,
|
|
133
|
+
session: Session,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The control block this handle uses in each process of its fork lineage. A
|
|
137
|
+
// child that takes an idle context over gives it a block of its own and never
|
|
138
|
+
// touches the inherited one, whose locks threads that are gone may hold.
|
|
139
|
+
struct Lineage {
|
|
140
|
+
epoch: u64,
|
|
141
|
+
held: Held,
|
|
142
|
+
next: OnceLock<Box<Self>>,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
enum Held {
|
|
146
|
+
Block(Arc<ControlBlock>),
|
|
147
|
+
// Disposed, or being disposed, before the fork that led here.
|
|
148
|
+
Disposed,
|
|
149
|
+
Orphaned(Orphan),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// An inherited context left to the parent.
|
|
153
|
+
struct Orphan {
|
|
154
|
+
in_use: bool,
|
|
155
|
+
disposed: AtomicBool,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fn orphaned(in_use: bool, disposed: bool) -> Held {
|
|
159
|
+
Held::Orphaned(Orphan {
|
|
160
|
+
in_use,
|
|
161
|
+
disposed: AtomicBool::new(disposed),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl Orphan {
|
|
166
|
+
fn disposed(&self) -> bool {
|
|
167
|
+
self.disposed.load(Ordering::Acquire)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
impl Lineage {
|
|
172
|
+
// The link for `epoch`, or else the last one, without making one.
|
|
173
|
+
fn find(&self, epoch: u64) -> Result<&Self, &Self> {
|
|
174
|
+
let mut lineage = self;
|
|
175
|
+
while lineage.epoch != epoch {
|
|
176
|
+
match lineage.next.get() {
|
|
177
|
+
Some(next) => lineage = next,
|
|
178
|
+
None => return Err(lineage),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
Ok(lineage)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The thread running the outermost dispatch, the callbacks it is running and
|
|
186
|
+
// the nested dispatches those callbacks wait on. Only that thread may dispatch
|
|
187
|
+
// meanwhile, and only from a callback that is not already waiting on one: a
|
|
188
|
+
// signal trap during the wait would otherwise send out of turn.
|
|
189
|
+
#[derive(Default)]
|
|
190
|
+
struct Session {
|
|
191
|
+
owner: AtomicU64,
|
|
192
|
+
callbacks: AtomicU32,
|
|
193
|
+
waits: AtomicU32,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
impl Session {
|
|
197
|
+
fn in_callback(&self, thread: u64) -> bool {
|
|
198
|
+
self.owner.load(Ordering::Acquire) == thread
|
|
199
|
+
&& self.callbacks.load(Ordering::Relaxed) > self.waits.load(Ordering::Relaxed)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
struct Counted<'a>(&'a AtomicU32);
|
|
204
|
+
|
|
205
|
+
impl<'a> Counted<'a> {
|
|
206
|
+
fn enter(count: &'a AtomicU32) -> Self {
|
|
207
|
+
count.fetch_add(1, Ordering::Relaxed);
|
|
208
|
+
Self(count)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl Drop for Counted<'_> {
|
|
213
|
+
fn drop(&mut self) {
|
|
214
|
+
self.0.fetch_sub(1, Ordering::Relaxed);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
struct Owning<'a>(&'a AtomicU64);
|
|
219
|
+
|
|
220
|
+
impl Drop for Owning<'_> {
|
|
221
|
+
fn drop(&mut self) {
|
|
222
|
+
self.0.store(0, Ordering::Release);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
impl ContextHandle {
|
|
227
|
+
/// Starts a runner and waits, through `host`, until its isolate exists.
|
|
228
|
+
pub fn spawn(config: ContextConfig, host: &mut impl Host) -> Result<Self, EngineError> {
|
|
229
|
+
process::ensure_initialized();
|
|
230
|
+
// Read here, on the caller's thread, not first on a runner's.
|
|
231
|
+
let _ = process::code_cache::enabled();
|
|
232
|
+
let _ = process::fast_calls();
|
|
233
|
+
let generation = fork::current();
|
|
234
|
+
let block = Arc::new(ControlBlock::new(config));
|
|
235
|
+
start(&block, generation, Start::Fresh)?;
|
|
236
|
+
let handle = Self {
|
|
237
|
+
lineage: Lineage {
|
|
238
|
+
epoch: fork::epoch(),
|
|
239
|
+
held: Held::Block(block),
|
|
240
|
+
next: OnceLock::new(),
|
|
241
|
+
},
|
|
242
|
+
session: Session::default(),
|
|
243
|
+
};
|
|
244
|
+
handle.dispatch(
|
|
245
|
+
DispatchKind::Maintenance,
|
|
246
|
+
|depth| Request::Init { depth },
|
|
247
|
+
host,
|
|
248
|
+
)?;
|
|
249
|
+
Ok(handle)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pub fn eval(
|
|
253
|
+
&self,
|
|
254
|
+
filename: impl Into<JsText>,
|
|
255
|
+
source: impl Into<JsText>,
|
|
256
|
+
host: &mut impl Host,
|
|
257
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
258
|
+
self.evaluate((filename.into(), source.into()), false, host)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// `eval`, then awaits a promise result: runs microtasks and due tasks
|
|
262
|
+
/// until it settles, as `mini_racer`'s `eval_await` does.
|
|
263
|
+
pub fn eval_await(
|
|
264
|
+
&self,
|
|
265
|
+
filename: impl Into<JsText>,
|
|
266
|
+
source: impl Into<JsText>,
|
|
267
|
+
host: &mut impl Host,
|
|
268
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
269
|
+
self.evaluate((filename.into(), source.into()), true, host)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn evaluate(
|
|
273
|
+
&self,
|
|
274
|
+
(filename, source): (JsText, JsText),
|
|
275
|
+
awaited: bool,
|
|
276
|
+
host: &mut impl Host,
|
|
277
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
278
|
+
self.dispatch(
|
|
279
|
+
DispatchKind::Script,
|
|
280
|
+
|depth| Request::Eval {
|
|
281
|
+
filename,
|
|
282
|
+
source,
|
|
283
|
+
depth,
|
|
284
|
+
awaited,
|
|
285
|
+
},
|
|
286
|
+
host,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/// `request` is the wire form `mini_racer` sends: one array holding the
|
|
291
|
+
/// function's dotted path, then its arguments.
|
|
292
|
+
pub fn call(&self, request: Vec<u8>, host: &mut impl Host) -> Result<Vec<u8>, EngineError> {
|
|
293
|
+
self.invoke(request, false, host)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// `call`, then awaits a promise result, as `eval_await` does.
|
|
297
|
+
pub fn call_await(
|
|
298
|
+
&self,
|
|
299
|
+
request: Vec<u8>,
|
|
300
|
+
host: &mut impl Host,
|
|
301
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
302
|
+
self.invoke(request, true, host)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn invoke(
|
|
306
|
+
&self,
|
|
307
|
+
request: Vec<u8>,
|
|
308
|
+
awaited: bool,
|
|
309
|
+
host: &mut impl Host,
|
|
310
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
311
|
+
self.dispatch(
|
|
312
|
+
DispatchKind::Script,
|
|
313
|
+
|depth| Request::Call {
|
|
314
|
+
request,
|
|
315
|
+
depth,
|
|
316
|
+
awaited,
|
|
317
|
+
},
|
|
318
|
+
host,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Runs the microtasks queued so far.
|
|
323
|
+
pub fn perform_microtask_checkpoint(&self, host: &mut impl Host) -> Result<(), EngineError> {
|
|
324
|
+
self.dispatch(
|
|
325
|
+
DispatchKind::Maintenance,
|
|
326
|
+
|depth| Request::MicrotaskCheckpoint { depth },
|
|
327
|
+
host,
|
|
328
|
+
)
|
|
329
|
+
.map(drop)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// V8's heap statistics under `mini_racer`'s names: a wire object whose
|
|
333
|
+
/// values are numbers.
|
|
334
|
+
pub fn heap_stats(&self, host: &mut impl Host) -> Result<Vec<u8>, EngineError> {
|
|
335
|
+
self.dispatch(
|
|
336
|
+
DispatchKind::Maintenance,
|
|
337
|
+
|depth| Request::HeapStats { depth },
|
|
338
|
+
host,
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/// A heap snapshot, as the JSON V8 writes.
|
|
343
|
+
pub fn heap_snapshot(&self, host: &mut impl Host) -> Result<Vec<u8>, EngineError> {
|
|
344
|
+
self.dispatch(
|
|
345
|
+
DispatchKind::Maintenance,
|
|
346
|
+
|depth| Request::HeapSnapshot { depth },
|
|
347
|
+
host,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// Runs one due task of V8's, then the microtasks it queued; returns
|
|
352
|
+
/// whether a task ran.
|
|
353
|
+
pub fn pump_message_loop(&self, host: &mut impl Host) -> Result<bool, EngineError> {
|
|
354
|
+
self.dispatch(
|
|
355
|
+
DispatchKind::Pump,
|
|
356
|
+
|depth| Request::PumpMessageLoop { depth },
|
|
357
|
+
host,
|
|
358
|
+
)
|
|
359
|
+
.map(|ran| ran == [1])
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// `request` is the wire form `mini_racer` sends: an array of the dotted
|
|
363
|
+
/// path to define and the callback id `Host::callback` will be given.
|
|
364
|
+
pub fn attach(&self, request: Vec<u8>, host: &mut impl Host) -> Result<(), EngineError> {
|
|
365
|
+
self.dispatch(
|
|
366
|
+
DispatchKind::Maintenance,
|
|
367
|
+
|depth| Request::Attach { request, depth },
|
|
368
|
+
host,
|
|
369
|
+
)
|
|
370
|
+
.map(drop)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/// Asks V8 to collect as much garbage as it can; returns once it has.
|
|
374
|
+
pub fn low_memory_notification(&self, host: &mut impl Host) -> Result<(), EngineError> {
|
|
375
|
+
self.dispatch(
|
|
376
|
+
DispatchKind::Maintenance,
|
|
377
|
+
|depth| Request::LowMemoryNotification { depth },
|
|
378
|
+
host,
|
|
379
|
+
)
|
|
380
|
+
.map(drop)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// Builds a startup snapshot from `code`, or warms `base` up with it, on
|
|
384
|
+
/// a context made for the purpose and disposed after, as `mini_racer`
|
|
385
|
+
/// does.
|
|
386
|
+
pub fn create_snapshot(
|
|
387
|
+
code: JsText,
|
|
388
|
+
base: Option<SnapshotBlob>,
|
|
389
|
+
host: &mut impl Host,
|
|
390
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
391
|
+
// A warmup from no snapshot would build the read-only heap again, which
|
|
392
|
+
// V8 aborts on once the process has one.
|
|
393
|
+
if base.as_ref().is_some_and(SnapshotBlob::is_empty) {
|
|
394
|
+
return Err(InvalidSnapshot::Empty.into());
|
|
395
|
+
}
|
|
396
|
+
let context = Self::spawn(ContextConfig::default(), host)?;
|
|
397
|
+
let built = context.dispatch(
|
|
398
|
+
DispatchKind::Script,
|
|
399
|
+
|depth| Request::Snapshot { code, base, depth },
|
|
400
|
+
host,
|
|
401
|
+
);
|
|
402
|
+
context.begin_dispose();
|
|
403
|
+
let disposed = context.dispose(host);
|
|
404
|
+
let blob = built?;
|
|
405
|
+
disposed.map(|()| blob)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// Whether this thread is running one of this context's callbacks, where a
|
|
409
|
+
/// dispatch runs nested in the one that made the callback.
|
|
410
|
+
#[must_use]
|
|
411
|
+
pub fn in_callback(&self) -> bool {
|
|
412
|
+
self.session.in_callback(process::thread_token())
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// Dispatches open on this context, the outermost one included.
|
|
416
|
+
#[must_use]
|
|
417
|
+
pub fn depth(&self) -> u32 {
|
|
418
|
+
if self.session.owner.load(Ordering::Acquire) == 0 {
|
|
419
|
+
0
|
|
420
|
+
} else {
|
|
421
|
+
self.session.waits.load(Ordering::Relaxed).saturating_add(1)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// Terminates the running script, or the next one if none is running.
|
|
426
|
+
pub fn stop(&self) -> Result<(), EngineError> {
|
|
427
|
+
match self.held() {
|
|
428
|
+
Held::Block(_) if self.is_disposed() => Err(EngineError::Disposed),
|
|
429
|
+
Held::Block(block) => {
|
|
430
|
+
block.term.request(Interrupt::Stop);
|
|
431
|
+
Ok(())
|
|
432
|
+
}
|
|
433
|
+
Held::Disposed => Err(EngineError::Disposed),
|
|
434
|
+
Held::Orphaned(orphan) if orphan.disposed() => Err(EngineError::Disposed),
|
|
435
|
+
Held::Orphaned(_) => Ok(()),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Refuses new dispatches and terminates one in flight on another thread.
|
|
440
|
+
/// The caller then waits for that dispatch to finish before `dispose`.
|
|
441
|
+
pub fn begin_dispose(&self) {
|
|
442
|
+
if let Held::Block(block) = self.held() {
|
|
443
|
+
block.term.dispose();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// Shuts the runner down and returns once its isolate is gone. Expects
|
|
448
|
+
/// `begin_dispose` and no dispatch in flight; a second call is a no-op.
|
|
449
|
+
pub fn dispose(&self, host: &mut impl Host) -> Result<(), EngineError> {
|
|
450
|
+
let block = match self.held() {
|
|
451
|
+
Held::Block(block) => block,
|
|
452
|
+
Held::Disposed => return Ok(()),
|
|
453
|
+
// mini_racer refuses a single-threaded context a child inherited in use.
|
|
454
|
+
Held::Orphaned(orphan) if orphan.in_use && platform::mode() == Mode::SingleThreaded => {
|
|
455
|
+
return Err(EngineError::DisposeBusy);
|
|
456
|
+
}
|
|
457
|
+
Held::Orphaned(orphan) => {
|
|
458
|
+
orphan.disposed.store(true, Ordering::Release);
|
|
459
|
+
return Ok(());
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
if self.session.callbacks.load(Ordering::Relaxed) > 0 {
|
|
463
|
+
return Err(EngineError::DisposeBusy);
|
|
464
|
+
}
|
|
465
|
+
block.term.dispose();
|
|
466
|
+
let epoch = fork::epoch();
|
|
467
|
+
let result = Self::shut_down(block, host);
|
|
468
|
+
// A child that forked during the wait leaves the parent's descriptor
|
|
469
|
+
// alone, as it does every lock it inherited.
|
|
470
|
+
if fork::epoch() == epoch {
|
|
471
|
+
block.notify.release(&block.channel);
|
|
472
|
+
}
|
|
473
|
+
result
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
fn shut_down(block: &ControlBlock, host: &mut impl Host) -> Result<(), EngineError> {
|
|
477
|
+
match block.channel.request(Request::Dispose) {
|
|
478
|
+
Ok(()) => {}
|
|
479
|
+
Err(ChannelError::Closed) => return Ok(()),
|
|
480
|
+
Err(error) => return Err(protocol(error)),
|
|
481
|
+
}
|
|
482
|
+
let wait = WaitHandle::new(block);
|
|
483
|
+
loop {
|
|
484
|
+
host.wait_blocking(&wait);
|
|
485
|
+
if wait.forked() {
|
|
486
|
+
// The child finds the context disposing, so disposed.
|
|
487
|
+
return Ok(());
|
|
488
|
+
}
|
|
489
|
+
match block.channel.recv_ruby() {
|
|
490
|
+
Ok(Some(ToRuby::Response(response))) => return response.map(drop),
|
|
491
|
+
Err(ChannelError::Closed) => return Ok(()),
|
|
492
|
+
Ok(Some(ToRuby::Callback(_))) => {
|
|
493
|
+
let _ = block.channel.reply(CallbackReply::Abort);
|
|
494
|
+
}
|
|
495
|
+
Ok(None) => {}
|
|
496
|
+
Err(error) => return Err(protocol(error)),
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Answers without taking an inherited context over.
|
|
502
|
+
#[must_use]
|
|
503
|
+
pub fn is_disposed(&self) -> bool {
|
|
504
|
+
match self.lineage.find(fork::epoch()) {
|
|
505
|
+
Ok(lineage) => match &lineage.held {
|
|
506
|
+
Held::Block(block) => block.channel.is_closed() || block.term.is_disposing(),
|
|
507
|
+
Held::Disposed => true,
|
|
508
|
+
Held::Orphaned(orphan) => orphan.disposed(),
|
|
509
|
+
},
|
|
510
|
+
Err(inherited) => match &inherited.held {
|
|
511
|
+
Held::Block(block) => {
|
|
512
|
+
block.channel.is_closed()
|
|
513
|
+
|| block
|
|
514
|
+
.term
|
|
515
|
+
.inherited()
|
|
516
|
+
.is_some_and(|inherited| inherited.disposing)
|
|
517
|
+
}
|
|
518
|
+
Held::Disposed => true,
|
|
519
|
+
Held::Orphaned(orphan) => orphan.disposed(),
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// What this process holds of the context, taking it over first in a
|
|
525
|
+
// forked child.
|
|
526
|
+
// A process the context passed through unused (a daemon's intermediate
|
|
527
|
+
// child) gets no link: its fork left the block exactly as inherited, and
|
|
528
|
+
// the generation's quiesced flag already covers every fork on the way.
|
|
529
|
+
fn held(&self) -> &Held {
|
|
530
|
+
let epoch = fork::epoch();
|
|
531
|
+
match self.lineage.find(epoch) {
|
|
532
|
+
Ok(lineage) => &lineage.held,
|
|
533
|
+
Err(last) => {
|
|
534
|
+
&last
|
|
535
|
+
.next
|
|
536
|
+
.get_or_init(|| Box::new(self.inherit(&last.held, epoch)))
|
|
537
|
+
.held
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Only atomics and try-locks touch what was inherited.
|
|
543
|
+
fn inherit(&self, inherited: &Held, epoch: u64) -> Lineage {
|
|
544
|
+
let held = match inherited {
|
|
545
|
+
Held::Block(block) if block.channel.is_closed() => Held::Disposed,
|
|
546
|
+
Held::Block(block) => match block.term.inherited() {
|
|
547
|
+
Some(inherited) if inherited.disposing => Held::Disposed,
|
|
548
|
+
// A parent thread may have claimed the context and not sent yet.
|
|
549
|
+
Some(inherited) if self.session.owner.load(Ordering::Acquire) == 0 => {
|
|
550
|
+
adopt(block, inherited)
|
|
551
|
+
}
|
|
552
|
+
_ => orphaned(true, false),
|
|
553
|
+
},
|
|
554
|
+
Held::Disposed => Held::Disposed,
|
|
555
|
+
Held::Orphaned(orphan) => orphaned(orphan.in_use, orphan.disposed()),
|
|
556
|
+
};
|
|
557
|
+
Lineage {
|
|
558
|
+
epoch,
|
|
559
|
+
held,
|
|
560
|
+
next: OnceLock::new(),
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
fn dispatch(
|
|
565
|
+
&self,
|
|
566
|
+
kind: DispatchKind,
|
|
567
|
+
request: impl FnOnce(Depth) -> Request,
|
|
568
|
+
host: &mut impl Host,
|
|
569
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
570
|
+
let block = match self.held() {
|
|
571
|
+
Held::Block(block) => block,
|
|
572
|
+
Held::Disposed => return Err(EngineError::Disposed),
|
|
573
|
+
Held::Orphaned(orphan) if orphan.disposed() => return Err(EngineError::Disposed),
|
|
574
|
+
Held::Orphaned(_) => return Err(EngineError::Orphaned),
|
|
575
|
+
};
|
|
576
|
+
let thread = process::thread_token();
|
|
577
|
+
match self.session.owner.load(Ordering::Acquire) {
|
|
578
|
+
0 => {}
|
|
579
|
+
_ if self.session.in_callback(thread) => {
|
|
580
|
+
return self.nested(block, kind, request(Depth::Nested), host);
|
|
581
|
+
}
|
|
582
|
+
_ => return Err(EngineError::Busy),
|
|
583
|
+
}
|
|
584
|
+
let dispatch = block.term.begin(kind).map_err(|error| match error {
|
|
585
|
+
TermError::Disposing => EngineError::Disposed,
|
|
586
|
+
TermError::AlreadyArmed => EngineError::Busy,
|
|
587
|
+
})?;
|
|
588
|
+
self.session.owner.store(thread, Ordering::Release);
|
|
589
|
+
let owning = Owning(&self.session.owner);
|
|
590
|
+
let wait = WaitHandle::new(block);
|
|
591
|
+
let result = match block.channel.request(request(Depth::Outermost(dispatch))) {
|
|
592
|
+
Ok(()) => self.await_response(block, &wait, false, host),
|
|
593
|
+
Err(error) => Err(channel(error)),
|
|
594
|
+
};
|
|
595
|
+
drop(owning);
|
|
596
|
+
if wait.forked() {
|
|
597
|
+
// The runner and the watchdog that armed the dispatch are the
|
|
598
|
+
// parent's, and may have held its locks at the fork.
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
// A no-op unless the runner died before disarming it.
|
|
602
|
+
block.term.end(dispatch);
|
|
603
|
+
watchdog::clear(block, dispatch);
|
|
604
|
+
result
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// A script nested in an untimed dispatch gets a timeout of its own.
|
|
608
|
+
fn nested(
|
|
609
|
+
&self,
|
|
610
|
+
block: &Arc<ControlBlock>,
|
|
611
|
+
kind: DispatchKind,
|
|
612
|
+
request: Request,
|
|
613
|
+
host: &mut impl Host,
|
|
614
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
615
|
+
if block.term.is_disposing() {
|
|
616
|
+
return Err(EngineError::Disposed);
|
|
617
|
+
}
|
|
618
|
+
let timer = match (kind, block.config.timeout) {
|
|
619
|
+
(DispatchKind::Script, Some(timeout)) => {
|
|
620
|
+
block.term.time_nested().map(|id| (timeout, id))
|
|
621
|
+
}
|
|
622
|
+
_ => None,
|
|
623
|
+
};
|
|
624
|
+
if let Some((timeout, id)) = timer
|
|
625
|
+
&& let Err(error) = watchdog::schedule(timeout, block, id)
|
|
626
|
+
{
|
|
627
|
+
block.term.untime_nested(id);
|
|
628
|
+
return Err(error);
|
|
629
|
+
}
|
|
630
|
+
let wait = WaitHandle::new(block);
|
|
631
|
+
let result = block
|
|
632
|
+
.channel
|
|
633
|
+
.request(request)
|
|
634
|
+
.map_err(channel)
|
|
635
|
+
.and_then(|()| {
|
|
636
|
+
let _waiting = Counted::enter(&self.session.waits);
|
|
637
|
+
self.await_response(block, &wait, true, host)
|
|
638
|
+
});
|
|
639
|
+
if let Some((_, id)) = timer
|
|
640
|
+
&& !wait.forked()
|
|
641
|
+
{
|
|
642
|
+
watchdog::clear(block, id);
|
|
643
|
+
block.term.untime_nested(id);
|
|
644
|
+
}
|
|
645
|
+
result
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Once the dispatch is abandoned, callbacks still in flight are answered
|
|
649
|
+
// with `Abort` without running, until the runner responds. Abandoning a
|
|
650
|
+
// nested dispatch stops only its script, as mini_racer cancels only the
|
|
651
|
+
// nested call: the dispatch it runs in carries on once it has unwound. A
|
|
652
|
+
// dispose that lands while a callback runs ends the dispatch as disposed,
|
|
653
|
+
// as mini_racer's callback finds its context gone.
|
|
654
|
+
fn await_response(
|
|
655
|
+
&self,
|
|
656
|
+
block: &ControlBlock,
|
|
657
|
+
wait: &WaitHandle<'_>,
|
|
658
|
+
nested: bool,
|
|
659
|
+
host: &mut impl Host,
|
|
660
|
+
) -> Result<Vec<u8>, EngineError> {
|
|
661
|
+
let mut cancelled = false;
|
|
662
|
+
let mut disposed = false;
|
|
663
|
+
let abandon = |cancelled: &mut bool| {
|
|
664
|
+
*cancelled = true;
|
|
665
|
+
if nested {
|
|
666
|
+
block.term.interrupt();
|
|
667
|
+
} else if let Some(dispatch) = block.term.armed() {
|
|
668
|
+
block.term.request(Interrupt::Cancel(dispatch));
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
loop {
|
|
672
|
+
let outcome = if cancelled {
|
|
673
|
+
host.wait_blocking(wait);
|
|
674
|
+
WaitOutcome::Ready
|
|
675
|
+
} else {
|
|
676
|
+
host.wait(wait)
|
|
677
|
+
};
|
|
678
|
+
if wait.forked() {
|
|
679
|
+
return Err(EngineError::Orphaned);
|
|
680
|
+
}
|
|
681
|
+
if outcome == WaitOutcome::Cancel {
|
|
682
|
+
abandon(&mut cancelled);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
match block.channel.recv_ruby() {
|
|
686
|
+
Ok(Some(ToRuby::Response(response))) => {
|
|
687
|
+
if nested && cancelled {
|
|
688
|
+
block.term.release();
|
|
689
|
+
}
|
|
690
|
+
return if disposed {
|
|
691
|
+
Err(EngineError::Disposed)
|
|
692
|
+
} else {
|
|
693
|
+
response
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
Ok(Some(ToRuby::Callback(request))) => {
|
|
697
|
+
let reply = if cancelled {
|
|
698
|
+
CallbackReply::Abort
|
|
699
|
+
} else {
|
|
700
|
+
let _running = Counted::enter(&self.session.callbacks);
|
|
701
|
+
let reply = host.callback(&request);
|
|
702
|
+
if wait.forked() {
|
|
703
|
+
return Err(EngineError::Orphaned);
|
|
704
|
+
}
|
|
705
|
+
disposed = block.term.is_disposing();
|
|
706
|
+
if disposed {
|
|
707
|
+
CallbackReply::Abort
|
|
708
|
+
} else {
|
|
709
|
+
reply
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
if matches!(reply, CallbackReply::Abort) && !cancelled {
|
|
713
|
+
abandon(&mut cancelled);
|
|
714
|
+
}
|
|
715
|
+
block.channel.reply(reply).map_err(channel)?;
|
|
716
|
+
}
|
|
717
|
+
Ok(None) => {}
|
|
718
|
+
Err(error) => return Err(channel(error)),
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
impl Drop for ContextHandle {
|
|
725
|
+
// A finalizer never blocks: the runner sees the closed channel, drops its
|
|
726
|
+
// isolate on its own thread and exits.
|
|
727
|
+
// An inherited context this process never took over is left as it is: its
|
|
728
|
+
// locks may have been held by parent threads that do not exist here.
|
|
729
|
+
fn drop(&mut self) {
|
|
730
|
+
if let Ok(Lineage {
|
|
731
|
+
held: Held::Block(block),
|
|
732
|
+
..
|
|
733
|
+
}) = self.lineage.find(fork::epoch())
|
|
734
|
+
{
|
|
735
|
+
block.term.dispose();
|
|
736
|
+
block.channel.close();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
fn start(
|
|
742
|
+
block: &Arc<ControlBlock>,
|
|
743
|
+
generation: &'static Generation,
|
|
744
|
+
start: Start,
|
|
745
|
+
) -> Result<(), EngineError> {
|
|
746
|
+
// Registered before the runner starts, so a fork either waits for it or
|
|
747
|
+
// finds it waiting for the fork.
|
|
748
|
+
generation.register(block);
|
|
749
|
+
let runner_block = Arc::clone(block);
|
|
750
|
+
let stack = platform::stack_size_kb().map_or(MIN_RUNNER_STACK_BYTES, |kb| {
|
|
751
|
+
(kb.saturating_mul(1024).saturating_add(STACK_HEADROOM_BYTES)).max(MIN_RUNNER_STACK_BYTES)
|
|
752
|
+
});
|
|
753
|
+
std::thread::Builder::new()
|
|
754
|
+
.name("mini_racer_rs".to_owned())
|
|
755
|
+
.stack_size(stack)
|
|
756
|
+
.spawn(move || runner::run(&runner_block, generation, start))
|
|
757
|
+
.map_err(|e| EngineError::Internal(format!("spawn runner: {e}")))?;
|
|
758
|
+
Ok(())
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// A forked child takes over a context whose runner was idle, with nothing in
|
|
762
|
+
// flight, at a fork that waited for every runner; any other stays the parent's.
|
|
763
|
+
// A stop or memory cause left for the next script carries over, as in
|
|
764
|
+
// mini_racer.
|
|
765
|
+
fn adopt(inherited: &ControlBlock, carried: Inherited) -> Held {
|
|
766
|
+
if inherited.phase.get() != Phase::Deposited || !inherited.channel.idle() {
|
|
767
|
+
return orphaned(true, false);
|
|
768
|
+
}
|
|
769
|
+
let generation = fork::current();
|
|
770
|
+
if !generation.quiesced() {
|
|
771
|
+
return orphaned(false, false);
|
|
772
|
+
}
|
|
773
|
+
let Some(tasks) = inherited.tasks.inherited() else {
|
|
774
|
+
return orphaned(true, false);
|
|
775
|
+
};
|
|
776
|
+
let Some(isolate) = inherited
|
|
777
|
+
.custody
|
|
778
|
+
.try_lock()
|
|
779
|
+
.ok()
|
|
780
|
+
.and_then(|mut custody| custody.take())
|
|
781
|
+
else {
|
|
782
|
+
return orphaned(true, false);
|
|
783
|
+
};
|
|
784
|
+
let block = Arc::new(ControlBlock::with_tasks(inherited.config.clone(), tasks));
|
|
785
|
+
*lock(&block.custody) = Some(isolate);
|
|
786
|
+
block.term.adopt(carried);
|
|
787
|
+
match start(&block, generation, Start::Adopted) {
|
|
788
|
+
Ok(()) => Held::Block(block),
|
|
789
|
+
Err(_) => orphaned(true, false),
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
fn protocol(error: ChannelError) -> EngineError {
|
|
794
|
+
EngineError::Internal(format!("channel protocol: {error}"))
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
fn channel(error: ChannelError) -> EngineError {
|
|
798
|
+
match error {
|
|
799
|
+
ChannelError::Closed => EngineError::Disposed,
|
|
800
|
+
error => protocol(error),
|
|
801
|
+
}
|
|
802
|
+
}
|