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,339 @@
|
|
|
1
|
+
use std::cell::Cell;
|
|
2
|
+
use std::os::fd::{AsRawFd, IntoRawFd};
|
|
3
|
+
use std::sync::OnceLock;
|
|
4
|
+
use std::time::Duration;
|
|
5
|
+
|
|
6
|
+
use magnus::error::ErrorType;
|
|
7
|
+
use magnus::gc::Marker;
|
|
8
|
+
use magnus::value::ReprValue;
|
|
9
|
+
use magnus::{Class, Error, Exception, Integer, RArray, RString, Ruby, Value, kwargs};
|
|
10
|
+
use mini_racer_engine::process::fork;
|
|
11
|
+
use mini_racer_engine::{
|
|
12
|
+
CallbackReply, EngineError, Host, JsError, JsErrorKind, JsText, WaitHandle, WaitOutcome,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
use crate::encode;
|
|
16
|
+
use crate::gc_root::OwnedRoot;
|
|
17
|
+
use crate::{context, decode, errors, rb_raw};
|
|
18
|
+
|
|
19
|
+
struct SpinBudget {
|
|
20
|
+
gvl: Duration,
|
|
21
|
+
nogvl: Duration,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Unstable tuning knob: MINI_RACER_RS_SPIN_US=<with GVL>,<without GVL>.
|
|
25
|
+
fn budget() -> &'static SpinBudget {
|
|
26
|
+
static BUDGET: OnceLock<SpinBudget> = OnceLock::new();
|
|
27
|
+
BUDGET.get_or_init(|| {
|
|
28
|
+
let setting = std::env::var("MINI_RACER_RS_SPIN_US").unwrap_or_default();
|
|
29
|
+
let parts: Vec<Option<u64>> = setting
|
|
30
|
+
.split(',')
|
|
31
|
+
.map(|part| part.trim().parse().ok())
|
|
32
|
+
.collect();
|
|
33
|
+
let at = |i: usize, default: u64| {
|
|
34
|
+
Duration::from_micros(parts.get(i).copied().flatten().unwrap_or(default))
|
|
35
|
+
};
|
|
36
|
+
SpinBudget {
|
|
37
|
+
gvl: at(0, 20),
|
|
38
|
+
nogvl: at(1, 50),
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// What a context's callbacks run: its attached callables by id, and where
|
|
44
|
+
/// the exception the latest callback raised is kept, as `mini_racer` keeps one
|
|
45
|
+
/// per context and clears it after every dispatch, nested ones included.
|
|
46
|
+
pub struct Callbacks<'r> {
|
|
47
|
+
pub callables: RArray,
|
|
48
|
+
pub raised: &'r OwnedRoot<Value>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Waits for a runner from a Ruby thread: spins holding the GVL, then parks
|
|
52
|
+
/// the fiber under a fiber scheduler, or else releases the GVL, spins and
|
|
53
|
+
/// parks the thread. A Ruby interrupt wakes the park; traps run and the wait
|
|
54
|
+
/// goes on, while an exception (Timeout, Thread#raise, #kill) cancels the
|
|
55
|
+
/// dispatch and is raised once the engine has its response.
|
|
56
|
+
pub struct RubyHost<'r> {
|
|
57
|
+
ruby: &'r Ruby,
|
|
58
|
+
pending: Option<Error>,
|
|
59
|
+
callbacks: Option<Callbacks<'r>>,
|
|
60
|
+
// The context's IO on a notifier descriptor of its own, which waits under
|
|
61
|
+
// a fiber scheduler need; a host without one blocks the thread.
|
|
62
|
+
waiter: Option<&'r Waiter>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// The IO a fiber scheduler waits on for one context's runner, and the fork
|
|
66
|
+
/// epoch it was made in: a forked child that takes the context over gives it
|
|
67
|
+
/// a runner of its own, whose descriptor that IO does not read.
|
|
68
|
+
#[derive(Default)]
|
|
69
|
+
pub struct Waiter {
|
|
70
|
+
io: OwnedRoot<Value>,
|
|
71
|
+
epoch: Cell<u64>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl Waiter {
|
|
75
|
+
pub fn mark(&self, marker: &Marker) {
|
|
76
|
+
self.io.mark(marker);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub fn take(&self, ruby: &Ruby) -> Option<Value> {
|
|
80
|
+
self.io.take(ruby)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<'r> RubyHost<'r> {
|
|
85
|
+
pub const fn new(ruby: &'r Ruby) -> Self {
|
|
86
|
+
Self {
|
|
87
|
+
ruby,
|
|
88
|
+
pending: None,
|
|
89
|
+
callbacks: None,
|
|
90
|
+
waiter: None,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub const fn with_callbacks(ruby: &'r Ruby, callbacks: Callbacks<'r>) -> Self {
|
|
95
|
+
Self {
|
|
96
|
+
ruby,
|
|
97
|
+
pending: None,
|
|
98
|
+
callbacks: Some(callbacks),
|
|
99
|
+
waiter: None,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[must_use]
|
|
104
|
+
pub const fn waiting_on(mut self, waiter: &'r Waiter) -> Self {
|
|
105
|
+
self.waiter = Some(waiter);
|
|
106
|
+
self
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The Ruby exception that cancelled the dispatch outranks whatever the
|
|
110
|
+
/// engine returned.
|
|
111
|
+
pub fn finish<T>(self, result: Result<T, EngineError>) -> Result<T, Error> {
|
|
112
|
+
// mini_racer raises an interrupt before it clears the kept exception,
|
|
113
|
+
// so a later rethrow of that error still raises it.
|
|
114
|
+
if let Some(pending) = self.pending {
|
|
115
|
+
return Err(pending);
|
|
116
|
+
}
|
|
117
|
+
let raised = self
|
|
118
|
+
.callbacks
|
|
119
|
+
.and_then(|callbacks| callbacks.raised.take(self.ruby))
|
|
120
|
+
.and_then(Exception::from_value);
|
|
121
|
+
match (result, raised) {
|
|
122
|
+
(
|
|
123
|
+
Err(EngineError::Js(JsError {
|
|
124
|
+
kind: JsErrorKind::HostException,
|
|
125
|
+
..
|
|
126
|
+
})),
|
|
127
|
+
Some(raised),
|
|
128
|
+
) => Err(Error::from(raised)),
|
|
129
|
+
(result, _) => result.map_err(|error| errors::engine(self.ruby, error)),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Under a fiber scheduler only this fiber waits: the runner's answer makes
|
|
134
|
+
// the context's notifier readable and the scheduler resumes the fiber,
|
|
135
|
+
// while whatever the scheduler raises into it (a timeout, a stop) cancels
|
|
136
|
+
// the dispatch, even when the answer came in meanwhile. None means no
|
|
137
|
+
// notifier is to be had, so the thread waits instead.
|
|
138
|
+
fn wait_in_fiber(&mut self, wait: &WaitHandle<'_>) -> Option<WaitOutcome> {
|
|
139
|
+
let io = self.waiter_io(wait)?;
|
|
140
|
+
loop {
|
|
141
|
+
let mut raised = None;
|
|
142
|
+
wait.park_notified(|| raised = wait_readable(io).err())
|
|
143
|
+
.ok()?;
|
|
144
|
+
if let Some(error) = raised {
|
|
145
|
+
self.pending = Some(error);
|
|
146
|
+
return Some(WaitOutcome::Cancel);
|
|
147
|
+
}
|
|
148
|
+
if wait.ready() {
|
|
149
|
+
return Some(WaitOutcome::Ready);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Made once per context and fork epoch, and again if Ruby code closed it;
|
|
155
|
+
// the descriptor is Ruby's own copy, so closing it never touches the
|
|
156
|
+
// engine's.
|
|
157
|
+
fn waiter_io(&self, wait: &WaitHandle<'_>) -> Option<Value> {
|
|
158
|
+
let ruby = self.ruby;
|
|
159
|
+
let waiter = self.waiter?;
|
|
160
|
+
let epoch = fork::epoch();
|
|
161
|
+
if let Some(io) = waiter.io.get(ruby)
|
|
162
|
+
&& waiter.epoch.get() == epoch
|
|
163
|
+
&& !io.funcall::<_, _, bool>("closed?", ()).unwrap_or(true)
|
|
164
|
+
{
|
|
165
|
+
return Some(io);
|
|
166
|
+
}
|
|
167
|
+
let fd = wait.notifier().ok()?;
|
|
168
|
+
let io: Value = ruby
|
|
169
|
+
.class_io()
|
|
170
|
+
.funcall("for_fd", (fd.as_raw_fd(), kwargs!("autoclose" => true)))
|
|
171
|
+
.ok()?;
|
|
172
|
+
let _owned_by_io = fd.into_raw_fd();
|
|
173
|
+
waiter.io.set(ruby, io);
|
|
174
|
+
waiter.epoch.set(epoch);
|
|
175
|
+
Some(io)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Keeps the first exception the scheduler raises for after the dispatch,
|
|
179
|
+
// then leaves the rest of the wait to the thread, so a scheduler that
|
|
180
|
+
// keeps raising cannot spin here.
|
|
181
|
+
fn wait_blocking_in_fiber(&mut self, wait: &WaitHandle<'_>) {
|
|
182
|
+
let Some(io) = self.waiter_io(wait) else {
|
|
183
|
+
return;
|
|
184
|
+
};
|
|
185
|
+
while !wait.ready() {
|
|
186
|
+
let mut raised = None;
|
|
187
|
+
if wait
|
|
188
|
+
.park_notified(|| raised = wait_readable(io).err())
|
|
189
|
+
.is_err()
|
|
190
|
+
{
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if let Some(error) = raised {
|
|
194
|
+
self.pending.get_or_insert(error);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A captured throw resumes through Ruby's error info, which any protected
|
|
201
|
+
// call in between would clear, so nothing runs Ruby code until it does.
|
|
202
|
+
fn jumping(&self) -> bool {
|
|
203
|
+
self.pending
|
|
204
|
+
.as_ref()
|
|
205
|
+
.is_some_and(|pending| matches!(pending.error_type(), ErrorType::Jump(_)))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// mini_racer's order: the id comes off the end of the arguments, and a
|
|
209
|
+
// reply it cannot send raises its internal error with the encoder's text.
|
|
210
|
+
fn run(&self, request: &[u8]) -> Result<Vec<u8>, Error> {
|
|
211
|
+
let ruby = self.ruby;
|
|
212
|
+
let bad = || errors::runtime(ruby, "bad callback id");
|
|
213
|
+
let args = RArray::from_value(decode::value(ruby, request)?).ok_or_else(bad)?;
|
|
214
|
+
let id = Integer::from_value(args.pop::<Value>()?).ok_or_else(bad)?;
|
|
215
|
+
let id = isize::try_from(id.to_i64()?).map_err(|_| bad())?;
|
|
216
|
+
let callables = self
|
|
217
|
+
.callbacks
|
|
218
|
+
.as_ref()
|
|
219
|
+
.map(|callbacks| callbacks.callables)
|
|
220
|
+
.ok_or_else(bad)?;
|
|
221
|
+
if id < 0 || id.cast_unsigned() >= callables.len() {
|
|
222
|
+
return Err(bad());
|
|
223
|
+
}
|
|
224
|
+
let callable = callables.entry::<Value>(id)?;
|
|
225
|
+
let result = rb_raw::call_with_array(ruby, callable, args)?;
|
|
226
|
+
encode::encode(ruby, result, |message| errors::internal(ruby, message))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn wait_readable(io: Value) -> Result<(), Error> {
|
|
231
|
+
let _: Value = io.funcall("wait_readable", ())?;
|
|
232
|
+
Ok(())
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn exception(ruby: &Ruby, error: &Error) -> Option<Exception> {
|
|
236
|
+
match error.error_type() {
|
|
237
|
+
ErrorType::Jump(_) => None,
|
|
238
|
+
ErrorType::Exception(exception) => Some(*exception),
|
|
239
|
+
ErrorType::Error(class, message) => class
|
|
240
|
+
.new_instance((ruby.str_new(message),))
|
|
241
|
+
.map_or_else(|error| exception(ruby, &error), Some),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// What V8 throws for a raising callback: the exception's to_s, in its own
|
|
246
|
+
// encoding, or mini_racer's fallback text.
|
|
247
|
+
fn message(ruby: &Ruby, exception: Exception) -> JsText {
|
|
248
|
+
exception
|
|
249
|
+
.funcall::<_, _, RString>("to_s", ())
|
|
250
|
+
.ok()
|
|
251
|
+
.and_then(|text| context::js_text(ruby, text).ok())
|
|
252
|
+
.unwrap_or_else(|| JsText::from("Ruby exception"))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
impl Host for RubyHost<'_> {
|
|
256
|
+
fn wait(&mut self, wait: &WaitHandle<'_>) -> WaitOutcome {
|
|
257
|
+
let budget = budget();
|
|
258
|
+
if wait.spin(budget.gvl) {
|
|
259
|
+
return WaitOutcome::Ready;
|
|
260
|
+
}
|
|
261
|
+
if rb_raw::fiber_scheduler(self.ruby)
|
|
262
|
+
&& let Some(outcome) = self.wait_in_fiber(wait)
|
|
263
|
+
{
|
|
264
|
+
return outcome;
|
|
265
|
+
}
|
|
266
|
+
let unparker = wait.unparker();
|
|
267
|
+
loop {
|
|
268
|
+
let parked = rb_raw::nogvl(
|
|
269
|
+
self.ruby,
|
|
270
|
+
|| {
|
|
271
|
+
if !wait.spin(budget.nogvl) {
|
|
272
|
+
wait.park();
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
&move || unparker.unpark(),
|
|
276
|
+
);
|
|
277
|
+
if wait.ready() {
|
|
278
|
+
return WaitOutcome::Ready;
|
|
279
|
+
}
|
|
280
|
+
if parked.as_ref().is_some_and(Result::is_err) {
|
|
281
|
+
self.pending = Some(errors::internal(self.ruby, "panic while waiting"));
|
|
282
|
+
return WaitOutcome::Cancel;
|
|
283
|
+
}
|
|
284
|
+
if let Err(error) = rb_raw::check_ints(self.ruby) {
|
|
285
|
+
self.pending = Some(error);
|
|
286
|
+
return WaitOutcome::Cancel;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Interrupts cannot end this wait, but they still run: a pending one would
|
|
292
|
+
// otherwise make every rb_nogvl return at once and keep the GVL held. An
|
|
293
|
+
// exception they raise is kept and raised once the dispatch is over.
|
|
294
|
+
fn wait_blocking(&mut self, wait: &WaitHandle<'_>) {
|
|
295
|
+
if !self.jumping() && rb_raw::fiber_scheduler(self.ruby) {
|
|
296
|
+
self.wait_blocking_in_fiber(wait);
|
|
297
|
+
}
|
|
298
|
+
if self.jumping() {
|
|
299
|
+
while !wait.ready() {
|
|
300
|
+
wait.park();
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
while !wait.ready() {
|
|
305
|
+
let parked = rb_raw::nogvl(
|
|
306
|
+
self.ruby,
|
|
307
|
+
|| {
|
|
308
|
+
while !wait.ready() {
|
|
309
|
+
wait.park();
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
&|| {},
|
|
313
|
+
);
|
|
314
|
+
if parked.is_none()
|
|
315
|
+
&& let Err(error) = rb_raw::check_ints(self.ruby)
|
|
316
|
+
{
|
|
317
|
+
self.pending.get_or_insert(error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// A throw or Thread#kill out of the callable aborts the dispatch and
|
|
323
|
+
// resumes once it is over.
|
|
324
|
+
fn callback(&mut self, request: &[u8]) -> CallbackReply {
|
|
325
|
+
let error = match self.run(request) {
|
|
326
|
+
Ok(reply) => return CallbackReply::Value(reply),
|
|
327
|
+
Err(error) => error,
|
|
328
|
+
};
|
|
329
|
+
let Some(exception) = exception(self.ruby, &error) else {
|
|
330
|
+
self.pending.get_or_insert(error);
|
|
331
|
+
return CallbackReply::Abort;
|
|
332
|
+
};
|
|
333
|
+
let message = message(self.ruby, exception);
|
|
334
|
+
if let Some(callbacks) = &self.callbacks {
|
|
335
|
+
callbacks.raised.set(self.ruby, exception.as_value());
|
|
336
|
+
}
|
|
337
|
+
CallbackReply::Raised(message)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
mod context;
|
|
2
|
+
mod decode;
|
|
3
|
+
mod encode;
|
|
4
|
+
mod errors;
|
|
5
|
+
mod fork;
|
|
6
|
+
mod gc_root;
|
|
7
|
+
mod guard;
|
|
8
|
+
mod host;
|
|
9
|
+
mod options;
|
|
10
|
+
mod platform;
|
|
11
|
+
#[allow(
|
|
12
|
+
unsafe_code,
|
|
13
|
+
reason = "the only module that calls rb-sys directly (invariant I6)"
|
|
14
|
+
)]
|
|
15
|
+
mod rb_raw;
|
|
16
|
+
mod snapshot;
|
|
17
|
+
|
|
18
|
+
use magnus::{Error, Module, Ruby};
|
|
19
|
+
|
|
20
|
+
macro_rules! export {
|
|
21
|
+
(method $f:path, $($arity:tt)+) => {
|
|
22
|
+
magnus::method!($f, $($arity)+)
|
|
23
|
+
};
|
|
24
|
+
(function $f:path, $($arity:tt)+) => {
|
|
25
|
+
magnus::function!($f, $($arity)+)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
pub(crate) use export;
|
|
29
|
+
|
|
30
|
+
#[magnus::init(name = "mini_racer_rs")]
|
|
31
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
32
|
+
let racer = ruby.define_module("MiniRacer")?;
|
|
33
|
+
let rs = ruby.define_module("MiniRacerRs")?;
|
|
34
|
+
rs.const_set("NATIVE_VERSION", env!("CARGO_PKG_VERSION"))?;
|
|
35
|
+
rs.const_set("V8_VERSION", mini_racer_engine::process::v8_version())?;
|
|
36
|
+
racer.define_class("JavaScriptFunction", ruby.class_object())?;
|
|
37
|
+
gc_root::init(ruby, rs)?;
|
|
38
|
+
errors::init(ruby)?;
|
|
39
|
+
platform::init(ruby, racer)?;
|
|
40
|
+
context::init(ruby, racer)?;
|
|
41
|
+
snapshot::init(ruby, racer)?;
|
|
42
|
+
fork::init(ruby, rs)?;
|
|
43
|
+
Ok(())
|
|
44
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
use std::time::Duration;
|
|
2
|
+
|
|
3
|
+
use mini_racer_engine::{ContextConfig, InvalidSnapshot, SnapshotBlob};
|
|
4
|
+
|
|
5
|
+
/// An option value, as far as the rules below need to see it.
|
|
6
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
7
|
+
pub enum OptionValue {
|
|
8
|
+
Fixnum(i64),
|
|
9
|
+
Nil,
|
|
10
|
+
False,
|
|
11
|
+
/// A `MiniRacer::Snapshot`, by the blob it shares or why that blob is
|
|
12
|
+
/// refused; given only for `snapshot:`.
|
|
13
|
+
Snapshot(Result<SnapshotBlob, InvalidSnapshot>),
|
|
14
|
+
/// Anything else, by the name Ruby's type errors give it.
|
|
15
|
+
Other(String),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl OptionValue {
|
|
19
|
+
// Ruby's type errors name nil and false by value.
|
|
20
|
+
fn type_name(&self) -> String {
|
|
21
|
+
match self {
|
|
22
|
+
Self::Fixnum(_) => "Integer".to_owned(),
|
|
23
|
+
Self::Nil => "nil".to_owned(),
|
|
24
|
+
Self::False => "false".to_owned(),
|
|
25
|
+
Self::Snapshot(_) => "MiniRacer::Snapshot".to_owned(),
|
|
26
|
+
Self::Other(name) => name.clone(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
|
32
|
+
pub enum OptionError {
|
|
33
|
+
#[error("wrong argument type {0} (expected Integer)")]
|
|
34
|
+
Type(String),
|
|
35
|
+
#[error("wrong argument type {0} (expected mini_racer/snapshot)")]
|
|
36
|
+
SnapshotType(String),
|
|
37
|
+
#[error("bad {0}")]
|
|
38
|
+
Range(&'static str),
|
|
39
|
+
#[error("bad keyword: {0}")]
|
|
40
|
+
Keyword(String),
|
|
41
|
+
#[error(transparent)]
|
|
42
|
+
InvalidSnapshot(InvalidSnapshot),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const INT32_MAX: i64 = i32::MAX as i64;
|
|
46
|
+
const UINT32_MAX: i64 = u32::MAX as i64;
|
|
47
|
+
|
|
48
|
+
fn fixnum(value: &OptionValue) -> Result<i64, OptionError> {
|
|
49
|
+
match value {
|
|
50
|
+
OptionValue::Fixnum(n) => Ok(*n),
|
|
51
|
+
other => Err(OptionError::Type(other.type_name())),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn within(
|
|
56
|
+
value: &OptionValue,
|
|
57
|
+
name: &'static str,
|
|
58
|
+
range: impl std::ops::RangeBounds<i64>,
|
|
59
|
+
) -> Result<i64, OptionError> {
|
|
60
|
+
let n = fixnum(value)?;
|
|
61
|
+
if range.contains(&n) {
|
|
62
|
+
Ok(n)
|
|
63
|
+
} else {
|
|
64
|
+
Err(OptionError::Range(name))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// `Context.new` keywords in the order given; `mini_racer` 0.22.1 checks them
|
|
69
|
+
/// last to first and raises on the first bad one.
|
|
70
|
+
pub fn configure(
|
|
71
|
+
options: impl DoubleEndedIterator<Item = (String, OptionValue)>,
|
|
72
|
+
) -> Result<ContextConfig, OptionError> {
|
|
73
|
+
let mut config = ContextConfig::default();
|
|
74
|
+
let mut refused = None;
|
|
75
|
+
for (key, value) in options.rev() {
|
|
76
|
+
match key.as_str() {
|
|
77
|
+
"timeout" => {
|
|
78
|
+
let ms = within(&value, "timeout", 0..=INT32_MAX)?;
|
|
79
|
+
config.timeout = u64::try_from(ms)
|
|
80
|
+
.ok()
|
|
81
|
+
.filter(|ms| *ms > 0)
|
|
82
|
+
.map(Duration::from_millis);
|
|
83
|
+
}
|
|
84
|
+
"marshal_stack_depth" => {
|
|
85
|
+
fixnum(&value)?;
|
|
86
|
+
}
|
|
87
|
+
"ensure_gc_after_idle" => {
|
|
88
|
+
let ms = within(&value, "ensure_gc_after_idle", 0..=INT32_MAX)?;
|
|
89
|
+
config.gc_after_idle = u64::try_from(ms)
|
|
90
|
+
.ok()
|
|
91
|
+
.filter(|ms| *ms > 0)
|
|
92
|
+
.map(Duration::from_millis);
|
|
93
|
+
}
|
|
94
|
+
"max_memory" => {
|
|
95
|
+
let bytes = within(&value, "max_memory", 0..UINT32_MAX)?;
|
|
96
|
+
config.max_memory = u64::try_from(bytes).ok().filter(|bytes| *bytes > 0);
|
|
97
|
+
}
|
|
98
|
+
"snapshot" => match value {
|
|
99
|
+
OptionValue::Nil => {}
|
|
100
|
+
// mini_racer treats an empty blob as no snapshot.
|
|
101
|
+
OptionValue::Snapshot(Ok(blob)) => {
|
|
102
|
+
config.snapshot = (!blob.is_empty()).then_some(blob);
|
|
103
|
+
}
|
|
104
|
+
// mini_racer's V8 reads the blob only once every option passes.
|
|
105
|
+
OptionValue::Snapshot(Err(invalid)) => refused = Some(invalid),
|
|
106
|
+
other => return Err(OptionError::SnapshotType(other.type_name())),
|
|
107
|
+
},
|
|
108
|
+
// mini_racer turns it on for any value but nil and false.
|
|
109
|
+
"verbose_exceptions" => {
|
|
110
|
+
config.verbose_exceptions = !matches!(value, OptionValue::Nil | OptionValue::False);
|
|
111
|
+
}
|
|
112
|
+
_ => return Err(OptionError::Keyword(key)),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
refused.map_or(Ok(config), |invalid| {
|
|
116
|
+
Err(OptionError::InvalidSnapshot(invalid))
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[cfg(test)]
|
|
121
|
+
mod tests {
|
|
122
|
+
use super::*;
|
|
123
|
+
|
|
124
|
+
fn configure_pairs(pairs: &[(&str, OptionValue)]) -> Result<ContextConfig, OptionError> {
|
|
125
|
+
configure(
|
|
126
|
+
pairs
|
|
127
|
+
.iter()
|
|
128
|
+
.map(|(key, value)| ((*key).to_owned(), value.clone())),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn message(pairs: &[(&str, OptionValue)]) -> Option<String> {
|
|
133
|
+
configure_pairs(pairs).err().map(|e| e.to_string())
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn timeout_is_milliseconds_and_zero_means_none() {
|
|
138
|
+
let config = configure_pairs(&[("timeout", OptionValue::Fixnum(250))]);
|
|
139
|
+
assert_eq!(
|
|
140
|
+
config.map(|c| c.timeout),
|
|
141
|
+
Ok(Some(Duration::from_millis(250)))
|
|
142
|
+
);
|
|
143
|
+
let config = configure_pairs(&[("timeout", OptionValue::Fixnum(0))]);
|
|
144
|
+
assert_eq!(config.map(|c| c.timeout), Ok(None));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn range_and_type_errors_match_mini_racer() {
|
|
149
|
+
assert_eq!(
|
|
150
|
+
message(&[("timeout", OptionValue::Fixnum(-1))]),
|
|
151
|
+
Some("bad timeout".to_owned())
|
|
152
|
+
);
|
|
153
|
+
assert_eq!(
|
|
154
|
+
configure_pairs(&[("timeout", OptionValue::Fixnum(INT32_MAX + 1))]).err(),
|
|
155
|
+
Some(OptionError::Range("timeout"))
|
|
156
|
+
);
|
|
157
|
+
assert_eq!(
|
|
158
|
+
message(&[("timeout", OptionValue::Nil)]),
|
|
159
|
+
Some("wrong argument type nil (expected Integer)".to_owned())
|
|
160
|
+
);
|
|
161
|
+
assert_eq!(
|
|
162
|
+
message(&[(
|
|
163
|
+
"marshal_stack_depth",
|
|
164
|
+
OptionValue::Other("String".to_owned())
|
|
165
|
+
)]),
|
|
166
|
+
Some("wrong argument type String (expected Integer)".to_owned())
|
|
167
|
+
);
|
|
168
|
+
assert!(configure_pairs(&[("marshal_stack_depth", OptionValue::Fixnum(-5))]).is_ok());
|
|
169
|
+
assert_eq!(
|
|
170
|
+
message(&[("max_memory", OptionValue::Fixnum(UINT32_MAX))]),
|
|
171
|
+
Some("bad max_memory".to_owned())
|
|
172
|
+
);
|
|
173
|
+
assert_eq!(
|
|
174
|
+
message(&[("ensure_gc_after_idle", OptionValue::False)]),
|
|
175
|
+
Some("wrong argument type false (expected Integer)".to_owned())
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn max_memory_is_bytes_and_zero_means_none() {
|
|
181
|
+
let config = configure_pairs(&[("max_memory", OptionValue::Fixnum(200_000_000))]);
|
|
182
|
+
assert_eq!(config.map(|c| c.max_memory), Ok(Some(200_000_000)));
|
|
183
|
+
let config = configure_pairs(&[("max_memory", OptionValue::Fixnum(0))]);
|
|
184
|
+
assert_eq!(config.map(|c| c.max_memory), Ok(None));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[test]
|
|
188
|
+
fn ensure_gc_after_idle_is_milliseconds_and_zero_means_none() {
|
|
189
|
+
let config = configure_pairs(&[("ensure_gc_after_idle", OptionValue::Fixnum(25))]);
|
|
190
|
+
assert_eq!(
|
|
191
|
+
config.map(|c| c.gc_after_idle),
|
|
192
|
+
Ok(Some(Duration::from_millis(25)))
|
|
193
|
+
);
|
|
194
|
+
let config = configure_pairs(&[("ensure_gc_after_idle", OptionValue::Fixnum(0))]);
|
|
195
|
+
assert_eq!(config.map(|c| c.gc_after_idle), Ok(None));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn keys_are_checked_last_to_first() {
|
|
200
|
+
let pairs = [
|
|
201
|
+
("timeout", OptionValue::Other("String".to_owned())),
|
|
202
|
+
("bogus", OptionValue::Fixnum(1)),
|
|
203
|
+
];
|
|
204
|
+
assert_eq!(message(&pairs), Some("bad keyword: bogus".to_owned()));
|
|
205
|
+
let pairs = [
|
|
206
|
+
("bogus", OptionValue::Fixnum(1)),
|
|
207
|
+
("timeout", OptionValue::Other("String".to_owned())),
|
|
208
|
+
];
|
|
209
|
+
assert_eq!(
|
|
210
|
+
configure_pairs(&pairs).err(),
|
|
211
|
+
Some(OptionError::Type("String".to_owned()))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn verbose_exceptions_is_any_value_but_nil_and_false() {
|
|
217
|
+
for (value, verbose) in [
|
|
218
|
+
(OptionValue::Nil, false),
|
|
219
|
+
(OptionValue::False, false),
|
|
220
|
+
(OptionValue::Other("TrueClass".to_owned()), true),
|
|
221
|
+
(OptionValue::Other("String".to_owned()), true),
|
|
222
|
+
(OptionValue::Fixnum(0), true),
|
|
223
|
+
] {
|
|
224
|
+
let config = configure_pairs(&[("verbose_exceptions", value)]);
|
|
225
|
+
assert_eq!(config.map(|c| c.verbose_exceptions), Ok(verbose));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn a_snapshot_must_be_one_and_nil_or_an_empty_one_is_none() {
|
|
231
|
+
let empty = configure_pairs(&[(
|
|
232
|
+
"snapshot",
|
|
233
|
+
OptionValue::Snapshot(Ok(SnapshotBlob::default())),
|
|
234
|
+
)]);
|
|
235
|
+
assert_eq!(empty.map(|config| config.snapshot), Ok(None));
|
|
236
|
+
let nil = configure_pairs(&[("snapshot", OptionValue::Nil)]);
|
|
237
|
+
assert_eq!(nil.map(|config| config.snapshot), Ok(None));
|
|
238
|
+
assert_eq!(
|
|
239
|
+
configure_pairs(&[("snapshot", OptionValue::False)]).map(drop),
|
|
240
|
+
Err(OptionError::SnapshotType("false".to_owned()))
|
|
241
|
+
);
|
|
242
|
+
assert_eq!(
|
|
243
|
+
configure_pairs(&[(
|
|
244
|
+
"snapshot",
|
|
245
|
+
OptionValue::Other("mini_racer/context".to_owned())
|
|
246
|
+
)])
|
|
247
|
+
.map(drop),
|
|
248
|
+
Err(OptionError::SnapshotType("mini_racer/context".to_owned()))
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn a_refused_blob_raises_only_once_every_option_passes() {
|
|
254
|
+
let refused = OptionValue::Snapshot(Err(InvalidSnapshot::Version));
|
|
255
|
+
assert_eq!(
|
|
256
|
+
configure_pairs(&[("snapshot", refused.clone())]).err(),
|
|
257
|
+
Some(OptionError::InvalidSnapshot(InvalidSnapshot::Version))
|
|
258
|
+
);
|
|
259
|
+
for pairs in [
|
|
260
|
+
[("timeout", OptionValue::Nil), ("snapshot", refused.clone())],
|
|
261
|
+
[("snapshot", refused), ("timeout", OptionValue::Nil)],
|
|
262
|
+
] {
|
|
263
|
+
assert_eq!(
|
|
264
|
+
message(&pairs),
|
|
265
|
+
Some("wrong argument type nil (expected Integer)".to_owned())
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
use magnus::scan_args::scan_args;
|
|
2
|
+
use magnus::value::ReprValue;
|
|
3
|
+
use magnus::{Error, Module, Object, RArray, RHash, RModule, RString, Ruby, TryConvert, Value};
|
|
4
|
+
use mini_racer_engine::process::platform;
|
|
5
|
+
|
|
6
|
+
use crate::{errors, export, rb_raw};
|
|
7
|
+
|
|
8
|
+
pub fn init(ruby: &Ruby, racer: RModule) -> Result<(), Error> {
|
|
9
|
+
let class = racer.define_class("Platform", ruby.class_object())?;
|
|
10
|
+
class.define_singleton_method("set_flags!", export!(function set_flags, -1))?;
|
|
11
|
+
Ok(())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fn set_flags(ruby: &Ruby, args: &[Value]) -> Result<(), Error> {
|
|
15
|
+
let args = scan_args::<(), (), RArray, (), RHash, ()>(args)?;
|
|
16
|
+
for name in args.splat {
|
|
17
|
+
push(ruby, name, None)?;
|
|
18
|
+
}
|
|
19
|
+
let mut pairs = Vec::new();
|
|
20
|
+
args.keywords.foreach(|name: Value, value: Value| {
|
|
21
|
+
pairs.push((name, value));
|
|
22
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
23
|
+
})?;
|
|
24
|
+
for (name, value) in pairs {
|
|
25
|
+
push(ruby, name, Some(value))?;
|
|
26
|
+
}
|
|
27
|
+
Ok(())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn to_s(ruby: &Ruby, value: Value) -> Result<Vec<u8>, Error> {
|
|
31
|
+
let string = RString::try_convert(value.funcall::<_, _, Value>("to_s", ())?)?;
|
|
32
|
+
Ok(rb_raw::string_bytes(ruby, string))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn push(ruby: &Ruby, name: Value, value: Option<Value>) -> Result<(), Error> {
|
|
36
|
+
let name = to_s(ruby, name)?;
|
|
37
|
+
let value = match value {
|
|
38
|
+
Some(value) if !value.is_nil() => Some(to_s(ruby, value)?),
|
|
39
|
+
_ => None,
|
|
40
|
+
};
|
|
41
|
+
platform::push_flag(&name, value.as_deref()).map_err(|e| errors::flag(ruby, e))
|
|
42
|
+
}
|