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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/AI-DISCLOSURE.md +13 -0
  3. data/CHANGELOG.md +45 -0
  4. data/COMPATIBILITY.md +235 -0
  5. data/Cargo.lock +1263 -0
  6. data/Cargo.toml +62 -0
  7. data/LICENSE +21 -0
  8. data/README.md +254 -0
  9. data/THIRD_PARTY_NOTICES.md +4501 -0
  10. data/crates/mini_racer_engine/Cargo.toml +25 -0
  11. data/crates/mini_racer_engine/build.rs +13 -0
  12. data/crates/mini_racer_engine/src/context.rs +802 -0
  13. data/crates/mini_racer_engine/src/control.rs +239 -0
  14. data/crates/mini_racer_engine/src/error.rs +88 -0
  15. data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
  16. data/crates/mini_racer_engine/src/lib.rs +20 -0
  17. data/crates/mini_racer_engine/src/notify.rs +107 -0
  18. data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
  19. data/crates/mini_racer_engine/src/process/fork.rs +261 -0
  20. data/crates/mini_racer_engine/src/process/platform.rs +194 -0
  21. data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
  22. data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
  23. data/crates/mini_racer_engine/src/process/workers.rs +165 -0
  24. data/crates/mini_racer_engine/src/process.rs +69 -0
  25. data/crates/mini_racer_engine/src/runner.rs +1560 -0
  26. data/crates/mini_racer_engine/src/serialize.rs +253 -0
  27. data/crates/mini_racer_engine/src/snapshot.rs +221 -0
  28. data/crates/mini_racer_engine/src/spin.rs +23 -0
  29. data/crates/mini_racer_engine/src/sys.rs +566 -0
  30. data/crates/mini_racer_sync/Cargo.toml +18 -0
  31. data/crates/mini_racer_sync/src/channel.rs +621 -0
  32. data/crates/mini_racer_sync/src/fork.rs +116 -0
  33. data/crates/mini_racer_sync/src/lib.rs +20 -0
  34. data/crates/mini_racer_sync/src/lock.rs +49 -0
  35. data/crates/mini_racer_sync/src/parker.rs +44 -0
  36. data/crates/mini_racer_sync/src/primitives.rs +10 -0
  37. data/crates/mini_racer_sync/src/tasks.rs +345 -0
  38. data/crates/mini_racer_sync/src/termination.rs +1234 -0
  39. data/crates/mini_racer_sync/src/workers.rs +283 -0
  40. data/crates/mini_racer_wire/Cargo.toml +18 -0
  41. data/crates/mini_racer_wire/src/decode.rs +926 -0
  42. data/crates/mini_racer_wire/src/lib.rs +67 -0
  43. data/crates/mini_racer_wire/src/marker.rs +43 -0
  44. data/crates/mini_racer_wire/src/text.rs +106 -0
  45. data/crates/mini_racer_wire/src/write.rs +187 -0
  46. data/ext/mini_racer_rs/Cargo.toml +26 -0
  47. data/ext/mini_racer_rs/build.rs +16 -0
  48. data/ext/mini_racer_rs/extconf.rb +78 -0
  49. data/ext/mini_racer_rs/src/context.rs +485 -0
  50. data/ext/mini_racer_rs/src/decode.rs +450 -0
  51. data/ext/mini_racer_rs/src/encode.rs +333 -0
  52. data/ext/mini_racer_rs/src/errors.rs +156 -0
  53. data/ext/mini_racer_rs/src/fork.rs +31 -0
  54. data/ext/mini_racer_rs/src/gc_root.rs +117 -0
  55. data/ext/mini_racer_rs/src/guard.rs +7 -0
  56. data/ext/mini_racer_rs/src/host.rs +339 -0
  57. data/ext/mini_racer_rs/src/lib.rs +44 -0
  58. data/ext/mini_racer_rs/src/options.rs +269 -0
  59. data/ext/mini_racer_rs/src/platform.rs +42 -0
  60. data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
  61. data/ext/mini_racer_rs/src/snapshot.rs +149 -0
  62. data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
  63. data/lib/mini_racer/shared.rb +420 -0
  64. data/lib/mini_racer/truffleruby.rb +439 -0
  65. data/lib/mini_racer/version.rb +6 -0
  66. data/lib/mini_racer.rb +3 -0
  67. data/lib/mini_racer_rs/internal_error.rb +7 -0
  68. data/lib/mini_racer_rs/version.rb +5 -0
  69. data/lib/mini_racer_rs/vocabulary.rb +105 -0
  70. data/lib/mini_racer_rs.rb +33 -0
  71. data/licenses/mini_racer.txt +21 -0
  72. data/licenses/rust/SOURCES.tsv +6 -0
  73. data/licenses/rust/std/COPYRIGHT +34 -0
  74. data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
  75. data/licenses/rust/std/LICENSE-APACHE +176 -0
  76. data/licenses/rust/std/LICENSE-MIT +25 -0
  77. data/licenses/v8/SOURCES.tsv +20 -0
  78. data/licenses/v8/abseil-cpp/LICENSE +203 -0
  79. data/licenses/v8/chromium-build/LICENSE +27 -0
  80. data/licenses/v8/components.tsv +29 -0
  81. data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
  82. data/licenses/v8/fast_float/LICENSE-MIT +27 -0
  83. data/licenses/v8/highway/LICENSE +371 -0
  84. data/licenses/v8/icu/LICENSE +568 -0
  85. data/licenses/v8/inspector_protocol/LICENSE +27 -0
  86. data/licenses/v8/libc++/LICENSE.TXT +311 -0
  87. data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
  88. data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
  89. data/licenses/v8/rapidhash/LICENSE +34 -0
  90. data/licenses/v8/simdutf/LICENSE +18 -0
  91. data/licenses/v8/siphash/LICENSE +116 -0
  92. data/licenses/v8/utf8-decoder/LICENSE +19 -0
  93. data/licenses/v8/v8/LICENSE +61 -0
  94. data/licenses/v8/v8/LICENSE.fdlibm +6 -0
  95. data/licenses/v8/v8/LICENSE.strongtalk +29 -0
  96. data/licenses/v8/v8/LICENSE.v8 +26 -0
  97. 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
+ }