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,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
+ }