mini_racer_rs 1.0.0 → 1.0.2

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.
@@ -1,3 +1,4 @@
1
+ use std::borrow::Cow;
1
2
  use std::collections::hash_map::RandomState;
2
3
  use std::hash::BuildHasher;
3
4
  use std::panic::{AssertUnwindSafe, catch_unwind};
@@ -11,12 +12,15 @@ use mini_racer_sync::{
11
12
  use mini_racer_wire::Writer;
12
13
  use v8::ValueDeserializerHelper;
13
14
 
14
- use crate::control::{CallbackReply, ControlBlock, Depth, IsolateTerminator, JsText, Request};
15
+ use crate::control::{
16
+ CallRequest, CallbackReply, ControlBlock, Depth, IsolateTerminator, JsText, Request,
17
+ };
15
18
  use crate::fast_calls::{self, Hoisted, Shape};
16
- use crate::process::code_cache::{Building, Plan};
19
+ use crate::process::code_cache::{Building, Key, Plan};
17
20
  use crate::process::fork::Generation;
18
21
  use crate::process::platform::{self, Mode};
19
22
  use crate::process::watchdog;
23
+ use crate::request::{self, Read};
20
24
  use crate::serialize::{self, Filter, Tc};
21
25
  use crate::spin::spin_until;
22
26
  use crate::{EngineError, JsError, JsErrorKind};
@@ -258,10 +262,9 @@ fn serve(block: &Arc<ControlBlock>, generation: &'static Generation) -> bool {
258
262
  ));
259
263
  isolate.set_slot(Arc::clone(&runtime));
260
264
  // Installed only once the context exists, so nothing terminates its setup.
261
- block.term.install(IsolateTerminator::new(
262
- isolate.thread_safe_handle(),
263
- &block.tasks,
264
- ));
265
+ block
266
+ .term
267
+ .install(IsolateTerminator::new(block, isolate.thread_safe_handle()));
265
268
  let shared = match sys::share(isolate) {
266
269
  Ok(shared) => shared,
267
270
  Err(refused) => {
@@ -301,10 +304,9 @@ fn adopt(
301
304
  locker.set_slot(Arc::clone(&runtime));
302
305
  runtime
303
306
  };
304
- block.term.install(IsolateTerminator::new(
305
- shared.thread_safe_handle(),
306
- &block.tasks,
307
- ));
307
+ block
308
+ .term
309
+ .install(IsolateTerminator::new(block, shared.thread_safe_handle()));
308
310
  let _ = block.isolate.set(shared.thread_safe_handle());
309
311
  serve_isolate(block, shared, runtime)
310
312
  }
@@ -773,10 +775,12 @@ fn evaluate(
773
775
  .code_cache()
774
776
  .plan(filename, source, runtime.snapshot)
775
777
  } else {
776
- Plan::Compile
778
+ Plan::Compile(None)
777
779
  };
778
780
  let too_large = || EngineError::Internal("source too large".to_owned());
779
- let source = string(tc, source).ok_or_else(too_large)?;
781
+ let source = shared_source(tc, runtime, plan.key(), source)
782
+ .or_else(|| string(tc, source))
783
+ .ok_or_else(too_large)?;
780
784
  let name = string(tc, filename).ok_or_else(too_large)?;
781
785
  let origin = serialize::origin(tc, name.into());
782
786
  let (script, make) = compile(tc, runtime, source, &origin, plan);
@@ -792,6 +796,25 @@ fn evaluate(
792
796
  Ok(reply)
793
797
  }
794
798
 
799
+ // A large script the process has seen before reads its source from the copy
800
+ // every context compiling it shares, outside the V8 heap. `max_memory:`
801
+ // measures the heap, where mini_racer keeps the source, so a context with it
802
+ // keeps its own copy.
803
+ fn shared_source<'s>(
804
+ scope: &v8::PinScope<'s, '_>,
805
+ runtime: &Runtime,
806
+ key: Option<&Key>,
807
+ source: &JsText,
808
+ ) -> Option<v8::Local<'s, v8::String>> {
809
+ if runtime.block.config.max_memory.is_some() {
810
+ return None;
811
+ }
812
+ let sources = runtime.generation.sources();
813
+ let string = sys::external_string(scope, &sources.units(key?, source)?)?;
814
+ sources.read();
815
+ Some(string)
816
+ }
817
+
795
818
  fn settle<'s>(
796
819
  tc: &mut Tc<'s, '_, '_, '_>,
797
820
  runtime: &Runtime,
@@ -1043,7 +1066,7 @@ fn compile<'s>(
1043
1066
  plan: Plan,
1044
1067
  ) -> (Option<v8::Local<'s, v8::Script>>, Option<Building>) {
1045
1068
  let (entry, key) = match plan {
1046
- Plan::Compile => return (v8::Script::compile(scope, source, Some(origin)), None),
1069
+ Plan::Compile(_) => return (v8::Script::compile(scope, source, Some(origin)), None),
1047
1070
  Plan::Make(building) => {
1048
1071
  return (
1049
1072
  v8::Script::compile(scope, source, Some(origin)),
@@ -1265,19 +1288,36 @@ fn read_request<'s>(
1265
1288
  Some((request, path))
1266
1289
  }
1267
1290
 
1291
+ enum Arguments<'s> {
1292
+ Built(Vec<v8::Local<'s, v8::Value>>),
1293
+ Read(v8::Local<'s, v8::Object>),
1294
+ }
1295
+
1268
1296
  // mini_racer's order: everything from the path walk on fails as a runtime
1269
1297
  // error. Each dotted segment but the last becomes an object, and the function
1270
1298
  // is called on its parent.
1271
1299
  fn invoke(
1272
1300
  tc: &mut Tc<'_, '_, '_, '_>,
1273
1301
  runtime: &Runtime,
1274
- request: &[u8],
1302
+ request: &CallRequest,
1275
1303
  (depth, awaited): (Depth, bool),
1276
1304
  ) -> Result<Vec<u8>, EngineError> {
1277
1305
  let context = tc.get_current_context();
1278
1306
  let failed = |tc: &Tc<'_, '_, '_, '_>| failure(tc, JsErrorKind::Runtime);
1279
- let (request, path) =
1280
- read_request(tc, request).ok_or_else(|| failure(tc, JsErrorKind::Internal))?;
1307
+ let built = if request::worth_building(request.objects, request.properties) {
1308
+ request::read(tc, &request.bytes, &runtime.block.termination_requested)
1309
+ } else {
1310
+ Read::Unbuilt
1311
+ };
1312
+ let (path, arguments) = match built {
1313
+ Read::Built(call) => (call.path, Arguments::Built(call.args)),
1314
+ Read::Failed => return Err(failure(tc, JsErrorKind::Internal)),
1315
+ Read::Unbuilt => {
1316
+ let (request, path) = read_request(tc, &request.bytes)
1317
+ .ok_or_else(|| failure(tc, JsErrorKind::Internal))?;
1318
+ (Cow::Owned(path), Arguments::Read(request))
1319
+ }
1320
+ };
1281
1321
  let mut receiver = context.global(tc);
1282
1322
  let mut keys = path.split('.').peekable();
1283
1323
  let mut function = None;
@@ -1296,11 +1336,17 @@ fn invoke(
1296
1336
  tc.throw_exception(exception);
1297
1337
  return Err(failed(tc));
1298
1338
  };
1299
- let count = v8::Local::<v8::Array>::try_from(request).map_or(0, |array| array.length());
1300
- let mut args = Vec::new();
1301
- for index in 1..count {
1302
- args.push(request.get_index(tc, index).ok_or_else(|| failed(tc))?);
1303
- }
1339
+ let args = match arguments {
1340
+ Arguments::Built(args) => args,
1341
+ Arguments::Read(request) => {
1342
+ let count = v8::Local::<v8::Array>::try_from(request).map_or(0, |array| array.length());
1343
+ let mut args = Vec::new();
1344
+ for index in 1..count {
1345
+ args.push(request.get_index(tc, index).ok_or_else(|| failed(tc))?);
1346
+ }
1347
+ args
1348
+ }
1349
+ };
1304
1350
  let value = function
1305
1351
  .call(tc, receiver.into(), &args)
1306
1352
  .ok_or_else(|| failed(tc))?;
@@ -1372,7 +1418,7 @@ fn trampoline<'s>(
1372
1418
  };
1373
1419
  let outcome = catch_unwind(AssertUnwindSafe(|| callback(scope, &runtime, &args, rv)));
1374
1420
  if outcome.is_err() {
1375
- scope.terminate_execution();
1421
+ terminate(scope, &runtime);
1376
1422
  runtime.block.channel.close();
1377
1423
  }
1378
1424
  }
@@ -1406,7 +1452,7 @@ fn callback<'s>(
1406
1452
  return;
1407
1453
  };
1408
1454
  if runtime.block.channel.callback(bytes).is_err() {
1409
- scope.terminate_execution();
1455
+ terminate(scope, runtime);
1410
1456
  return;
1411
1457
  }
1412
1458
  loop {
@@ -1424,13 +1470,13 @@ fn callback<'s>(
1424
1470
  handle(scope, runtime, request)
1425
1471
  };
1426
1472
  if runtime.block.channel.respond(response).is_err() {
1427
- scope.terminate_execution();
1473
+ terminate(scope, runtime);
1428
1474
  return;
1429
1475
  }
1430
1476
  continue;
1431
1477
  }
1432
1478
  None => {
1433
- scope.terminate_execution();
1479
+ terminate(scope, runtime);
1434
1480
  return;
1435
1481
  }
1436
1482
  };
@@ -1448,7 +1494,7 @@ fn callback<'s>(
1448
1494
  scope.throw_exception(exception);
1449
1495
  }
1450
1496
  if !runtime.block.term.is_settled() {
1451
- scope.terminate_execution();
1497
+ terminate(scope, runtime);
1452
1498
  }
1453
1499
  }
1454
1500
  // mini_racer throws a raising callback's exception even with a
@@ -1463,11 +1509,11 @@ fn callback<'s>(
1463
1509
  *lock(&runtime.raised) = Some(v8::Global::new(scope, exception));
1464
1510
  scope.throw_exception(exception);
1465
1511
  if terminating {
1466
- scope.terminate_execution();
1512
+ terminate(scope, runtime);
1467
1513
  }
1468
1514
  }
1469
1515
  _ if terminating => {
1470
- scope.terminate_execution();
1516
+ terminate(scope, runtime);
1471
1517
  }
1472
1518
  // As mini_racer reads the reply: a termination pending for this
1473
1519
  // script makes the read throw it, and the script ends here.
@@ -1485,6 +1531,16 @@ fn callback<'s>(
1485
1531
  }
1486
1532
  }
1487
1533
 
1534
+ // Sets the flag a runner's own reads consult before V8 is asked to terminate,
1535
+ // as the termination cell's requests do.
1536
+ fn terminate(isolate: &v8::Isolate, runtime: &Runtime) {
1537
+ runtime
1538
+ .block
1539
+ .termination_requested
1540
+ .store(true, Ordering::Release);
1541
+ isolate.terminate_execution();
1542
+ }
1543
+
1488
1544
  fn error<'s>(scope: &v8::PinScope<'s, '_>, message: &JsText) -> v8::Local<'s, v8::Value> {
1489
1545
  let message = string(scope, message)
1490
1546
  .or_else(|| v8::String::new(scope, "Ruby exception"))
@@ -1,6 +1,6 @@
1
- use mini_racer_wire::marker;
1
+ use mini_racer_wire::{Writer, marker};
2
2
 
3
- use crate::snapshot;
3
+ use crate::{snapshot, sys};
4
4
  use v8::ValueSerializerHelper;
5
5
 
6
6
  // mini_racer's filter (mini_racer_v8.cc, MIT, Copyright (c) 2016-2019 the
@@ -59,6 +59,9 @@ pub type Tc<'a, 'b, 'c, 'i> = v8::PinnedRef<'a, v8::TryCatch<'b, 'c, v8::HandleS
59
59
  pub struct Filter {
60
60
  pub context: v8::Global<v8::Context>,
61
61
  function: v8::Global<v8::Function>,
62
+ // The format version V8's serializer writes, which a reply written
63
+ // without it carries too.
64
+ version: u32,
62
65
  }
63
66
 
64
67
  pub fn origin<'s>(
@@ -85,7 +88,7 @@ pub fn contexts(
85
88
  .flatten()
86
89
  .unwrap_or_else(|| v8::Context::new(scope, v8::ContextOptions::default()));
87
90
  let user = v8::Context::new(scope, v8::ContextOptions::default());
88
- let function = {
91
+ let (function, version) = {
89
92
  let scope = &mut v8::ContextScope::new(scope, safe);
90
93
  let source = v8::String::new(scope, FILTER_SOURCE)?;
91
94
  let name = v8::String::new(scope, "safe_context_script.js")?;
@@ -97,17 +100,32 @@ pub fn contexts(
97
100
  let global = user.global(scope).into();
98
101
  let filter = setup.call(scope, receiver, &[global]);
99
102
  safe.use_default_security_token();
100
- v8::Local::<v8::Function>::try_from(filter?).ok()?
103
+ let filter = v8::Local::<v8::Function>::try_from(filter?).ok()?;
104
+ (filter, wire_version(scope)?)
101
105
  };
102
106
  Some((
103
107
  v8::Global::new(scope, user),
104
108
  Filter {
105
109
  context: v8::Global::new(scope, safe),
106
110
  function: v8::Global::new(scope, function),
111
+ version,
107
112
  },
108
113
  ))
109
114
  }
110
115
 
116
+ fn wire_version(scope: &v8::PinScope<'_, '_>) -> Option<u32> {
117
+ let bytes = write(scope, v8::undefined(scope).into())?;
118
+ let (_, header) = bytes.split_first()?;
119
+ let mut version = 0u32;
120
+ for (shift, byte) in (0..32).step_by(7).zip(header) {
121
+ version |= u32::from(byte & 0x7f).checked_shl(shift)?;
122
+ if byte & 0x80 == 0 {
123
+ return Some(version);
124
+ }
125
+ }
126
+ None
127
+ }
128
+
111
129
  /// What `mini_racer` does to a result before serializing it: proxies give way
112
130
  /// to their targets, symbols to their descriptions, functions to the marker
113
131
  /// its Ruby side reads as `JavaScriptFunction`, and weak collections and
@@ -189,6 +207,46 @@ impl v8::ValueSerializerImpl for Delegate {
189
207
  }
190
208
  }
191
209
 
210
+ // The bytes V8's serializer writes for a primitive, written without it: for
211
+ // these values it checks for no termination and runs no script, and the
212
+ // microtasks it would run as it returns ran as the script returned. `None` for
213
+ // anything else, and for a NaN, whose bits V8 copies unchanged.
214
+ fn primitive(
215
+ isolate: &mut v8::Isolate,
216
+ version: u32,
217
+ value: v8::Local<'_, v8::Value>,
218
+ ) -> Option<Vec<u8>> {
219
+ let mut out = Vec::new();
220
+ let mut writer = Writer::versioned(&mut out, version);
221
+ if sys::is_smi(&value) {
222
+ let small = v8::Local::<v8::Int32>::try_from(value).ok()?;
223
+ writer.integer(small.value().into());
224
+ } else if let Ok(number) = v8::Local::<v8::Number>::try_from(value) {
225
+ let number = number.value();
226
+ if number.is_nan() {
227
+ return None;
228
+ }
229
+ writer.number(number);
230
+ } else if let Ok(string) = v8::Local::<v8::String>::try_from(value) {
231
+ let view = v8::ValueView::new(isolate, string);
232
+ match view.data() {
233
+ v8::ValueViewData::OneByte(bytes) => writer.latin1(bytes),
234
+ v8::ValueViewData::TwoByte(units) => writer.utf16_aligned(units),
235
+ }
236
+ } else if value.is_undefined() {
237
+ writer.undefined();
238
+ } else if value.is_null() {
239
+ writer.null();
240
+ } else if value.is_true() {
241
+ writer.bool(true);
242
+ } else if value.is_false() {
243
+ writer.bool(false);
244
+ } else {
245
+ return None;
246
+ }
247
+ Some(out)
248
+ }
249
+
192
250
  /// Serializes `value` with no filter retry; `None` leaves an exception pending.
193
251
  pub fn write(scope: &v8::PinScope<'_, '_>, value: v8::Local<'_, v8::Value>) -> Option<Vec<u8>> {
194
252
  let context = scope.get_current_context();
@@ -213,6 +271,9 @@ pub fn reply(
213
271
  // mini_racer serializes in a TryCatch of its own, which is not verbose, and
214
272
  // rethrows from it; V8 never reports a rethrown exception.
215
273
  tc.set_verbose(false);
274
+ if let Some(bytes) = primitive(tc, filter.version, value) {
275
+ return Some(bytes);
276
+ }
216
277
  if let Some(bytes) = write(tc, value) {
217
278
  return Some(bytes);
218
279
  }
@@ -1,7 +1,10 @@
1
- use std::ffi::c_void;
1
+ use std::ffi::{c_char, c_void};
2
2
  use std::ptr::NonNull;
3
+ use std::sync::Arc;
3
4
  use std::time::Duration;
4
5
 
6
+ use crate::process::sources::Units;
7
+
5
8
  // The prebuilt V8 archive marks every heap mapping MADV_DONTFORK, so a forked
6
9
  // child would lose the isolate's memory. `-Wl,--wrap=madvise` (build.rs) routes
7
10
  // V8's calls here; this drops that one advice and forwards the rest unchanged.
@@ -47,6 +50,77 @@ pub fn startup_data(blob: &crate::SnapshotBlob) -> v8::StartupData {
47
50
  v8::StartupData::from(bytes)
48
51
  }
49
52
 
53
+ /// A string V8 reads from `units` in place. It holds a reference to them,
54
+ /// which V8 hands back through `release_one_byte` or `release_two_byte` once,
55
+ /// on whatever thread it runs that on: when the string dies in a collection
56
+ /// or its isolate's teardown, or when V8 internalizes it while it lives and
57
+ /// gives the units to the internalized string, to release when that one dies,
58
+ /// or releases them at once if that string has its own (`MigrateExternalString`
59
+ /// in V8's `string.cc`). `None` past V8's length limit.
60
+ pub fn external_string<'s>(
61
+ scope: &v8::PinScope<'s, '_>,
62
+ units: &Units,
63
+ ) -> Option<v8::Local<'s, v8::String>> {
64
+ // V8 refuses a resource past its limit without disposing of it, which
65
+ // would keep the reference for good, so it never gets one.
66
+ match units {
67
+ Units::OneByte(bytes) if bytes.len() <= v8::String::MAX_LENGTH => {
68
+ let data = Arc::into_raw(Arc::clone(bytes));
69
+ // SAFETY: the bytes stay where they are, unchanged, while the
70
+ // reference made here lives; V8 only reads them, and hands the
71
+ // reference back through `release_one_byte`, with this pointer and
72
+ // length, once the string dies.
73
+ unsafe {
74
+ v8::String::new_external_onebyte_raw(
75
+ scope,
76
+ data.cast::<c_char>().cast_mut(),
77
+ bytes.len(),
78
+ release_one_byte,
79
+ )
80
+ }
81
+ }
82
+ Units::TwoByte(units) if units.len() <= v8::String::MAX_LENGTH => {
83
+ let data = Arc::into_raw(Arc::clone(units));
84
+ // SAFETY: as for one byte a unit, through `release_two_byte`.
85
+ unsafe {
86
+ v8::String::new_external_twobyte_raw(
87
+ scope,
88
+ data.cast::<u16>().cast_mut(),
89
+ units.len(),
90
+ release_two_byte,
91
+ )
92
+ }
93
+ }
94
+ Units::OneByte(_) | Units::TwoByte(_) => None,
95
+ }
96
+ }
97
+
98
+ // Each runs once per string `external_string` made, and only releases its
99
+ // reference: the list in `process::sources` holds another for as long as any
100
+ // string reads them, so the units are never freed here.
101
+ unsafe extern "C" fn release_one_byte(data: *mut c_char, length: usize) {
102
+ let released = std::panic::catch_unwind(|| {
103
+ let bytes = std::ptr::slice_from_raw_parts(data.cast::<u8>().cast_const(), length);
104
+ // SAFETY: V8 passes back the pointer and length `external_string`
105
+ // gave it, once, so this releases the one reference it made.
106
+ unsafe { Arc::decrement_strong_count(bytes) };
107
+ });
108
+ if released.is_err() {
109
+ std::process::abort();
110
+ }
111
+ }
112
+
113
+ unsafe extern "C" fn release_two_byte(data: *mut u16, length: usize) {
114
+ let released = std::panic::catch_unwind(|| {
115
+ let units = std::ptr::slice_from_raw_parts(data.cast_const(), length);
116
+ // SAFETY: as in `release_one_byte`.
117
+ unsafe { Arc::decrement_strong_count(units) };
118
+ });
119
+ if released.is_err() {
120
+ std::process::abort();
121
+ }
122
+ }
123
+
50
124
  /// The address V8 names `isolate` by when it posts the isolate a task.
51
125
  #[must_use]
52
126
  pub fn isolate_key(isolate: &v8::Isolate) -> usize {
@@ -58,6 +132,22 @@ pub fn isolate_key(isolate: &v8::Isolate) -> usize {
58
132
  pointer.addr()
59
133
  }
60
134
 
135
+ // V8 tags a small integer's word with a clear low bit and a heap object's with
136
+ // a set one.
137
+ const SMI_TAG_MASK: usize = 1;
138
+
139
+ /// Whether `value` is a small integer, which V8's serializer writes as an
140
+ /// int32; a heap number it writes as a double, even one holding an int32.
141
+ #[must_use]
142
+ pub const fn is_smi(value: &v8::Value) -> bool {
143
+ let slot: *const v8::Value = value;
144
+ // SAFETY: rusty_v8 builds V8 without direct handles, so a reference to a
145
+ // value is the address of its handle slot, one aligned tagged word that
146
+ // lives as long as the reference.
147
+ let tagged = unsafe { slot.cast::<usize>().read() };
148
+ tagged & SMI_TAG_MASK == 0
149
+ }
150
+
61
151
  /// Has V8 report each garbage collection on `isolate`, so its runner can
62
152
  /// check the heap against `max_memory`.
63
153
  pub fn watch_heap(isolate: &mut v8::Isolate) {
@@ -20,7 +20,7 @@ pub use write::{WriteError, Writer};
20
20
  pub const READ_VERSIONS: RangeInclusive<u32> = 13..=16;
21
21
  pub const WRITE_VERSION: u32 = 15;
22
22
 
23
- mod tag {
23
+ pub mod tag {
24
24
  pub const HEADER: u8 = 0xFF;
25
25
  pub const PADDING: u8 = 0x00;
26
26
  pub const VERIFY_OBJECT_COUNT: u8 = b'?';
@@ -22,10 +22,16 @@ pub struct Writer<'a> {
22
22
  impl<'a> Writer<'a> {
23
23
  /// Starts a frame: clears `out` and writes the header.
24
24
  pub fn new(out: &'a mut Vec<u8>) -> Self {
25
+ Self::versioned(out, WRITE_VERSION)
26
+ }
27
+
28
+ /// Starts a frame under another format version, such as the one V8's own
29
+ /// serializer writes, for bytes that must match its output.
30
+ pub fn versioned(out: &'a mut Vec<u8>, version: u32) -> Self {
25
31
  out.clear();
26
32
  out.push(tag::HEADER);
27
33
  let mut writer = Self { out, next_id: 0 };
28
- writer.varint(WRITE_VERSION.into());
34
+ writer.varint(version.into());
29
35
  writer
30
36
  }
31
37
 
@@ -131,6 +137,32 @@ impl<'a> Writer<'a> {
131
137
  self.out.extend_from_slice(bytes);
132
138
  }
133
139
 
140
+ /// A two-byte string as V8's serializer writes one: after a padding byte
141
+ /// wherever its characters would otherwise start at an odd offset.
142
+ pub fn utf16_aligned(&mut self, units: &[u16]) {
143
+ let bytes = units.len().saturating_mul(2);
144
+ let mut length_bytes = 1usize;
145
+ let mut rest = bytes >> 7;
146
+ while rest > 0 {
147
+ length_bytes = length_bytes.saturating_add(1);
148
+ rest >>= 7;
149
+ }
150
+ let start = self
151
+ .out
152
+ .len()
153
+ .saturating_add(1)
154
+ .saturating_add(length_bytes);
155
+ if !start.is_multiple_of(2) {
156
+ self.out.push(tag::PADDING);
157
+ }
158
+ self.out.push(tag::TWO_BYTE_STRING);
159
+ self.len(bytes);
160
+ self.out.reserve(bytes);
161
+ for unit in units {
162
+ self.out.extend_from_slice(&unit.to_le_bytes());
163
+ }
164
+ }
165
+
134
166
  /// Milliseconds since the Unix epoch; non-finite times become NaN.
135
167
  pub fn date(&mut self, millis: f64) -> u32 {
136
168
  self.out.push(tag::DATE);
@@ -298,11 +298,13 @@ impl Context {
298
298
 
299
299
  fn invoke(ruby: &Ruby, rb_self: &Self, args: &[Value], awaited: bool) -> Result<Value, Error> {
300
300
  let handle = rb_self.handle(ruby)?;
301
- let args = scan_args::<(Value,), (), RArray, (), (), ()>(args)?;
302
- let name = rb_raw::expect_string(ruby, args.required.0)?;
303
- let request = args.splat;
304
- request.unshift(name)?;
305
- let request = encode::encode(ruby, request.as_value(), |message| {
301
+ let Some((&name, args)) = args.split_first() else {
302
+ // The arity error mini_racer's rb_scan_args raises.
303
+ scan_args::<(Value,), (), RArray, (), (), ()>(args)?;
304
+ return Err(errors::internal(ruby, "no function name"));
305
+ };
306
+ let name = rb_raw::expect_string(ruby, name)?;
307
+ let request = encode::call(ruby, name, args, |message| {
306
308
  errors::runtime(ruby, [b"Context.call: ".as_slice(), &message].concat())
307
309
  })?;
308
310
  let bytes = rb_self.dispatch(ruby, |host| {
@@ -87,10 +87,11 @@ struct Pending {
87
87
  }
88
88
 
89
89
  // The visitor and its frames live on the native stack for the whole decode,
90
- // where Ruby's GC finds them (I3); `refs` holds every value V8 gave an id.
90
+ // where Ruby's GC finds them (I3); `refs` holds every value V8 gave an id,
91
+ // made at the first, so a result without one allocates none.
91
92
  struct RubyVisitor<'r> {
92
93
  ruby: &'r Ruby,
93
- refs: RArray,
94
+ refs: Option<RArray>,
94
95
  pending: Pending,
95
96
  }
96
97
 
@@ -359,12 +360,18 @@ impl Visitor for RubyVisitor<'_> {
359
360
  }
360
361
 
361
362
  fn register(&mut self, id: u32, value: Value) -> Result<(), Error> {
363
+ let ruby = self.ruby;
362
364
  self.refs
365
+ .get_or_insert_with(|| ruby.ary_new())
363
366
  .store(isize::try_from(id).unwrap_or(isize::MAX), value)
364
367
  }
365
368
 
369
+ // The decoder refers back only to an id it saw registered.
366
370
  fn back_ref(&mut self, id: u32) -> Result<Value, Error> {
367
- self.refs.entry(isize::try_from(id).unwrap_or(isize::MAX))
371
+ self.refs.map_or_else(
372
+ || self.bad_state(),
373
+ |refs| refs.entry(isize::try_from(id).unwrap_or(isize::MAX)),
374
+ )
368
375
  }
369
376
 
370
377
  fn push_int32s(&mut self, frame: &mut Frame, values: &[i32]) -> Result<(), Error> {
@@ -416,7 +423,7 @@ const RESULT_LIMITS: Limits = Limits {
416
423
  pub fn value(ruby: &Ruby, bytes: &[u8]) -> Result<Value, Error> {
417
424
  let mut visitor = RubyVisitor {
418
425
  ruby,
419
- refs: ruby.ary_new(),
426
+ refs: None,
420
427
  pending: Pending {
421
428
  target: None,
422
429
  values: [ruby.qnil().as_value(); BATCH],