mini_racer_rs 1.0.4 → 1.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a8339f7ec9dcc06bd136d68328c3b5a8acd48bcb59061fe0e1de97d8163e7a5
4
- data.tar.gz: fdb040a4e14239cce537be6724a36410432093b6f6ca0f62d6eec5830d32c6b5
3
+ metadata.gz: c1f337ddf902559ced22f49783b4f541c2ead54c00d1a72ee7a354e1e0b831fc
4
+ data.tar.gz: 45fe66cb3b1fd6af17aa4da11fa689d387a586eaab1ffd3fe0349fddc90bbec8
5
5
  SHA512:
6
- metadata.gz: e0fa4008ea43a6f4b7b23a5d324d8c58b659da2688243681b59ddb0af21b16fb54bb2677b9952f24381df1965c66ae47722f1c93d5e82d53b89748c2d7e8a60f
7
- data.tar.gz: 0b8bb09c4ca879533aa582fc800c9779b56326dc1c52c61d9d78a95132efa96f0898d596a550255d77839c6c51d09695dfe042021b5992141d5dcb3bbbcc57c8
6
+ metadata.gz: 924bb67c0d847c12570f983e86594d3ea432f76f206f73d725c3908b272634b5252699652a46231655e714ece2822942c474832b9618681d3bea3c120f7138b4
7
+ data.tar.gz: 8ead9bef3dac6010fe255d66e009583a7d1e2e8dc2c07aa655dbf21a3f9cda6b0416a74d676a28ff18e916a60f6c644d54e8265d3abe65c0927f5dc6870bcf89
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.5](https://github.com/andymai/mini_racer_rs/compare/v1.0.4...v1.0.5) (2026-10-19)
4
+
5
+
6
+ ### Performance
7
+
8
+ * **encode:** send Ruby strings to V8 in the form V8 stores them ([#93](https://github.com/andymai/mini_racer_rs/issues/93)) ([62f9c85](https://github.com/andymai/mini_racer_rs/commit/62f9c851917cb0ce7d760b5fc545aa014df0a53b))
9
+
3
10
  ## [1.0.4](https://github.com/andymai/mini_racer_rs/compare/v1.0.3...v1.0.4) (2026-10-19)
4
11
 
5
12
 
data/Cargo.lock CHANGED
@@ -458,7 +458,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
458
458
 
459
459
  [[package]]
460
460
  name = "mini_racer_engine"
461
- version = "1.0.4"
461
+ version = "1.0.5"
462
462
  dependencies = [
463
463
  "deno_core_icudata",
464
464
  "libc",
@@ -470,7 +470,7 @@ dependencies = [
470
470
 
471
471
  [[package]]
472
472
  name = "mini_racer_rs"
473
- version = "1.0.4"
473
+ version = "1.0.5"
474
474
  dependencies = [
475
475
  "magnus",
476
476
  "mini_racer_engine",
@@ -481,7 +481,7 @@ dependencies = [
481
481
 
482
482
  [[package]]
483
483
  name = "mini_racer_sync"
484
- version = "1.0.4"
484
+ version = "1.0.5"
485
485
  dependencies = [
486
486
  "loom",
487
487
  "thiserror",
@@ -489,7 +489,7 @@ dependencies = [
489
489
 
490
490
  [[package]]
491
491
  name = "mini_racer_wire"
492
- version = "1.0.4"
492
+ version = "1.0.5"
493
493
  dependencies = [
494
494
  "proptest",
495
495
  "thiserror",
data/Cargo.toml CHANGED
@@ -14,7 +14,7 @@ license = "MIT"
14
14
  publish = false
15
15
  repository = "https://github.com/andymai/mini_racer_rs"
16
16
  rust-version = "1.91"
17
- version = "1.0.4"
17
+ version = "1.0.5"
18
18
 
19
19
  [workspace.dependencies]
20
20
  deno_core_icudata = "=0.78.0"
@@ -14,8 +14,10 @@ mod spin;
14
14
  reason = "the engine's only FFI fence: symbols the linker routes into V8"
15
15
  )]
16
16
  mod sys;
17
+ mod text;
17
18
 
18
19
  pub use context::{ContextHandle, Host, Unparker, WaitHandle, WaitOutcome};
19
20
  pub use control::{CallRequest, CallbackReply, ContextConfig, JsText};
20
21
  pub use error::{EngineError, FlagError, InvalidSnapshot, JsError, JsErrorKind, TerminationCause};
21
22
  pub use snapshot::{SnapshotBlob, WeakSnapshotBlob};
23
+ pub use text::{Decoded, Transcoder};
@@ -4,6 +4,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
4
4
  use mini_racer_wire::{WRITE_VERSION, tag};
5
5
  use v8::ValueDeserializerHelper;
6
6
 
7
+ use crate::sys;
8
+
7
9
  // Deeper values are left to V8, which reads as deep as its stack allows.
8
10
  const MAX_DEPTH: u32 = 64;
9
11
  // Far below V8's own limit on an array's length, which it enforces itself.
@@ -48,16 +50,16 @@ pub const fn worth_building(objects: usize, properties: usize) -> bool {
48
50
  /// which its deserializer makes, element by element as a sparse array, so a
49
51
  /// function returning an argument costs far more than it need.
50
52
  ///
51
- /// A two-byte string is read by V8's own deserializer from its bytes alone.
52
- /// The whole request is `Unbuilt` for anything else this would not build as V8
53
- /// does or that V8 would refuse (a binary, a key that is not a string or an
54
- /// int32, a key given twice, an array inside itself, an object of more than
55
- /// `MAX_KEYS` keys, nesting past `MAX_DEPTH`), decided from the bytes before
56
- /// any value is made. A termination requested as it reads leaves it `Unbuilt`
57
- /// too: V8 acts on one before each value it reads, an array or object
58
- /// beginning as a value does, so this looks before the request and before
59
- /// each value. One requested before the dispatch refused it
60
- /// (`TermCell::refuses`).
53
+ /// A two-byte string whose units all fit Latin-1 is read by V8's own
54
+ /// deserializer from its bytes alone, which keeps it two-byte as V8's read of
55
+ /// the whole would. The whole request is `Unbuilt` for anything else this would
56
+ /// not build as V8 does or that V8 would refuse (a binary, a key that is not a
57
+ /// string or an int32, a key given twice, an array inside itself, an object of
58
+ /// more than `MAX_KEYS` keys, nesting past `MAX_DEPTH`), decided from the bytes
59
+ /// before any value is made. A termination requested as it reads leaves it
60
+ /// `Unbuilt` too: V8 acts on one before each value it reads, an array or object
61
+ /// beginning as a value does, so this looks before the request and before each
62
+ /// value. One requested before the dispatch refused it (`TermCell::refuses`).
61
63
  pub fn read<'s, 'b>(
62
64
  scope: &v8::PinScope<'s, '_>,
63
65
  bytes: &'b [u8],
@@ -74,6 +76,7 @@ pub fn read<'s, 'b>(
74
76
  objects: Vec::new(),
75
77
  elements: Vec::new(),
76
78
  shapes: Vec::new(),
79
+ units: Vec::new(),
77
80
  };
78
81
  match builder.call() {
79
82
  Some(call) => Read::Built(call),
@@ -119,6 +122,17 @@ impl<'b> Cursor<'b> {
119
122
  }
120
123
  }
121
124
 
125
+ // A value's tag, past the padding V8 skips, which aligns a two-byte
126
+ // string's units.
127
+ fn tag(&mut self) -> Option<u8> {
128
+ loop {
129
+ match self.byte()? {
130
+ tag::PADDING => {}
131
+ tag => return Some(tag),
132
+ }
133
+ }
134
+ }
135
+
122
136
  fn varint(&mut self) -> Option<u64> {
123
137
  let mut value = 0u64;
124
138
  for shift in (0..64).step_by(7) {
@@ -153,9 +167,9 @@ impl<'b> Cursor<'b> {
153
167
  .filter(|text| text.len() <= v8::String::MAX_LENGTH)
154
168
  }
155
169
 
156
- fn two_byte(&mut self) -> Option<()> {
157
- let len = self.text()?.len();
158
- (len.is_multiple_of(2) && len / 2 <= v8::String::MAX_LENGTH).then_some(())
170
+ fn two_byte(&mut self) -> Option<&'b [u8]> {
171
+ self.text()
172
+ .filter(|text| text.len().is_multiple_of(2) && text.len() / 2 <= v8::String::MAX_LENGTH)
159
173
  }
160
174
 
161
175
  // The length of `[path, ...arguments]`, past V8's header.
@@ -312,7 +326,7 @@ impl Check<'_> {
312
326
  }
313
327
 
314
328
  fn value(&mut self, depth: u32) -> Option<()> {
315
- match self.bytes.byte()? {
329
+ match self.bytes.tag()? {
316
330
  tag::INT32 => {
317
331
  self.bytes.varint_u32()?;
318
332
  }
@@ -323,7 +337,9 @@ impl Check<'_> {
323
337
  tag::UTF8_STRING | tag::ONE_BYTE_STRING => {
324
338
  self.bytes.string()?;
325
339
  }
326
- tag::TWO_BYTE_STRING => self.bytes.two_byte()?,
340
+ tag::TWO_BYTE_STRING => {
341
+ self.bytes.two_byte()?;
342
+ }
327
343
  tag::BIGINT => {
328
344
  self.bytes.bigint_bytes()?;
329
345
  }
@@ -393,6 +409,9 @@ struct Builder<'s, 'b, 'p, 'i> {
393
409
  // The names last made for each key position at each depth, so objects of
394
410
  // one shape share them instead of looking each up again.
395
411
  shapes: Vec<Vec<(Key<'b>, v8::Local<'s, v8::String>)>>,
412
+ // A two-byte string's units where the request's bytes hold them
413
+ // unaligned.
414
+ units: Vec<u16>,
396
415
  }
397
416
 
398
417
  // Reads again what `Check` accepted, so only a termination stops it, or V8's
@@ -419,7 +438,7 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
419
438
  self.unterminated()?;
420
439
  let scope = self.scope;
421
440
  let at = self.bytes;
422
- Some(match self.bytes.byte()? {
441
+ Some(match self.bytes.tag()? {
423
442
  tag::INT32 => v8::Integer::new(scope, zigzag(self.bytes.varint_u32()?)).into(),
424
443
  tag::DOUBLE => v8::Number::new(scope, self.bytes.double()?).into(),
425
444
  tag::NULL => v8::null(scope).into(),
@@ -459,17 +478,34 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
459
478
  tag::BEGIN_DENSE_ARRAY => self.array(depth.checked_sub(1)?)?,
460
479
  tag::BEGIN_OBJECT => self.object(depth.checked_sub(1)?)?,
461
480
  tag::TWO_BYTE_STRING => {
462
- self.bytes.two_byte()?;
463
- self.two_byte(at)?
481
+ let text = self.bytes.two_byte()?;
482
+ self.two_byte(text, at)?
464
483
  }
465
484
  _ => return None,
466
485
  })
467
486
  }
468
487
 
469
- // Reads the string from `at` up to the cursor with V8's own deserializer,
470
- // which keeps it two-byte, where V8's API makes a one-byte string of
471
- // whatever fits.
472
- fn two_byte(&mut self, at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
488
+ // V8's API makes a one-byte string of units that all fit Latin-1, where
489
+ // its deserializer keeps them two-byte, so only a string with a wider
490
+ // unit is made through the API, from the request's own bytes where the
491
+ // padding before them aligned them.
492
+ fn two_byte(&mut self, text: &[u8], at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
493
+ let pairs = text.as_chunks::<2>().0;
494
+ if !pairs.iter().any(|&[_, high]| high != 0) {
495
+ return self.deserialized(at);
496
+ }
497
+ let units = if let Some(units) = sys::units(text) {
498
+ units
499
+ } else {
500
+ self.units.clear();
501
+ self.units
502
+ .extend(pairs.iter().map(|&pair| u16::from_le_bytes(pair)));
503
+ &self.units
504
+ };
505
+ v8::String::new_from_two_byte(self.scope, units, v8::NewStringType::Normal).map(Into::into)
506
+ }
507
+
508
+ fn deserialized(&mut self, at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
473
509
  let len = at.rest.len().checked_sub(self.bytes.rest.len())?;
474
510
  let mut bytes = vec![tag::HEADER, u8::try_from(WRITE_VERSION).ok()?];
475
511
  bytes.extend_from_slice(at.rest.get(..len)?);
@@ -121,6 +121,43 @@ unsafe extern "C" fn release_two_byte(data: *mut u16, length: usize) {
121
121
  }
122
122
  }
123
123
 
124
+ /// The UTF-16 units `bytes` hold, in the host's order as V8's deserializer
125
+ /// reads them, without a copy; `None` when they do not start where a unit
126
+ /// can be read from or hold an odd byte.
127
+ #[must_use]
128
+ pub fn units(bytes: &[u8]) -> Option<&[u16]> {
129
+ // SAFETY: every bit pattern is a valid u16, and `align_to` hands out only
130
+ // the aligned middle of `bytes`, borrowed for as long as `bytes` is.
131
+ let (head, units, tail) = unsafe { bytes.align_to::<u16>() };
132
+ (head.is_empty() && tail.is_empty()).then_some(units)
133
+ }
134
+
135
+ /// Writes the Latin-1 bytes of `text` at the start of `out` when `text` is
136
+ /// valid UTF-8 and every character in it is at most U+00FF, and returns how
137
+ /// many; `None` otherwise, or when `out` is shorter than `text`.
138
+ pub fn utf8_to_latin1(text: &[u8], out: &mut [u8]) -> Option<usize> {
139
+ if out.len() < text.len() {
140
+ return None;
141
+ }
142
+ // SAFETY: simdutf writes at most one byte per byte of `text`, which `out`
143
+ // has room for.
144
+ let converted = unsafe { v8::simdutf::convert_utf8_to_latin1_with_errors(text, out) };
145
+ converted.is_ok().then_some(converted.count)
146
+ }
147
+
148
+ /// Writes the UTF-16 units of `text` at the start of `out` when `text` is
149
+ /// valid UTF-8, and returns how many; `None` otherwise, or when `out` is
150
+ /// shorter than `text`.
151
+ pub fn utf8_to_utf16(text: &[u8], out: &mut [u16]) -> Option<usize> {
152
+ if out.len() < text.len() {
153
+ return None;
154
+ }
155
+ // SAFETY: simdutf writes at most one unit per byte of `text`, which `out`
156
+ // has room for.
157
+ let converted = unsafe { v8::simdutf::convert_utf8_to_utf16le_with_errors(text, out) };
158
+ converted.is_ok().then_some(converted.count)
159
+ }
160
+
124
161
  /// The address V8 names `isolate` by when it posts the isolate a task.
125
162
  #[must_use]
126
163
  pub fn isolate_key(isolate: &v8::Isolate) -> usize {
@@ -0,0 +1,182 @@
1
+ use crate::sys;
2
+
3
+ // V8 decodes shorter UTF-8 faster than a conversion starts: converting all of
4
+ // it made arrays of short BMP and emoji strings about a tenth slower to send.
5
+ const SHORT: usize = 16;
6
+ // UTF-8 longer than this goes to V8 as mini_racer sends it. Decoded here, it
7
+ // takes a buffer of two bytes per byte and a request up to twice its length, both
8
+ // under the GVL: a 100 MB String with one character past Latin-1 would peak
9
+ // near 500 MB where its UTF-8 takes 200 MB. Below this, that extra memory
10
+ // stays within 24 MiB, and the values measured, up to 1 MB, all gain.
11
+ const LONG: usize = 8 << 20;
12
+ const WIDE_LEAD: u8 = 0xC4;
13
+
14
+ /// The characters of valid UTF-8 as V8 stores them once it has decoded it:
15
+ /// one byte each when every one is at most U+00FF, UTF-16 units otherwise.
16
+ pub enum Decoded<'a> {
17
+ OneByte(&'a [u8]),
18
+ TwoByte(&'a [u16]),
19
+ }
20
+
21
+ /// Decodes UTF-8 ahead of V8, which copies one-byte and two-byte text where
22
+ /// it would decode UTF-8 a byte at a time, into buffers kept for the next
23
+ /// text.
24
+ #[derive(Default)]
25
+ pub struct Transcoder {
26
+ one_byte: Vec<u8>,
27
+ two_byte: Vec<u16>,
28
+ }
29
+
30
+ impl Transcoder {
31
+ /// `text` as V8 would decode it; `None` when it is short enough for V8 to
32
+ /// decode as fast, past `LONG` bytes, or not valid UTF-8, whose
33
+ /// replacement characters are V8's to choose.
34
+ #[expect(
35
+ clippy::inline_always,
36
+ reason = "the encoder calls it for each non-ASCII value; outlined, it slowed short ones 12%"
37
+ )]
38
+ #[inline(always)]
39
+ pub fn decode(&mut self, text: &[u8]) -> Option<Decoded<'_>> {
40
+ if text.len() < SHORT || text.len() > LONG {
41
+ return None;
42
+ }
43
+ self.decode_long(text)
44
+ }
45
+
46
+ // One conversion each: in valid UTF-8 a byte from 0xC4 up starts a
47
+ // character past U+00FF, and text without one that does not convert to
48
+ // Latin-1 is not valid.
49
+ fn decode_long(&mut self, text: &[u8]) -> Option<Decoded<'_>> {
50
+ if text.iter().fold(0, |most, &byte| most.max(byte)) < WIDE_LEAD {
51
+ grow(&mut self.one_byte, text.len());
52
+ let count = sys::utf8_to_latin1(text, &mut self.one_byte)?;
53
+ return self.one_byte.get(..count).map(Decoded::OneByte);
54
+ }
55
+ grow(&mut self.two_byte, text.len());
56
+ let count = sys::utf8_to_utf16(text, &mut self.two_byte)?;
57
+ self.two_byte.get(..count).map(Decoded::TwoByte)
58
+ }
59
+ }
60
+
61
+ // Each has room for as many units as the text has bytes, which simdutf needs,
62
+ // and is made afresh as it grows: the allocator hands out a long one's zeroed
63
+ // pages untouched, where zeroing it in place would commit room for three
64
+ // times the units a CJK text decodes to.
65
+ fn grow<T: Copy + Default>(buffer: &mut Vec<T>, len: usize) {
66
+ if buffer.len() < len {
67
+ *buffer = vec![T::default(); len];
68
+ }
69
+ }
70
+
71
+ #[cfg(test)]
72
+ mod tests {
73
+ use super::{Decoded, LONG, SHORT, Transcoder};
74
+
75
+ // What V8 makes of valid UTF-8, worked out from its characters.
76
+ fn expected(text: &str) -> (Option<Vec<u8>>, Vec<u16>) {
77
+ let latin1 = text.chars().map(|c| u8::try_from(c).ok()).collect();
78
+ (latin1, text.encode_utf16().collect())
79
+ }
80
+
81
+ fn check(transcoder: &mut Transcoder, text: &str) {
82
+ let (latin1, utf16) = expected(text);
83
+ match (transcoder.decode(text.as_bytes()), latin1) {
84
+ (Some(Decoded::OneByte(bytes)), Some(latin1)) => assert_eq!(bytes, latin1, "{text:?}"),
85
+ (Some(Decoded::TwoByte(units)), None) => assert_eq!(units, utf16, "{text:?}"),
86
+ (None, _) => assert!(text.len() < SHORT, "{text:?} left to V8"),
87
+ _ => panic!("{text:?} decoded in the wrong form"),
88
+ }
89
+ }
90
+
91
+ #[test]
92
+ fn valid_text_decodes_to_the_units_v8_stores() {
93
+ let mut transcoder = Transcoder::default();
94
+ let pad = "the quick brown fox ";
95
+ for text in [
96
+ "caf\u{e9}",
97
+ "caf\u{e9} na\u{ef}ve Z\u{fc}rich cr\u{e8}me \u{ff}",
98
+ "\u{6771}\u{4eac} \u{5927}\u{962a} \u{540d}\u{53e4}\u{5c4b}",
99
+ "\u{1f600} party \u{1f389} fun \u{1f680} go \u{10ffff}",
100
+ "0123456789abcde\u{e9}",
101
+ "0123456789abc\u{e9}",
102
+ "0123456789abcd\u{e9}",
103
+ ] {
104
+ check(&mut transcoder, text);
105
+ }
106
+ let ranges = [
107
+ 0x80..0x100,
108
+ 0x100..0x800,
109
+ 0x800..0xD800,
110
+ 0xE000..0x1_0000,
111
+ 0x1_0000..0x11_0000,
112
+ ];
113
+ for range in ranges {
114
+ for start in range.clone().step_by(61) {
115
+ let text: String = (start..range.end.min(start + 23))
116
+ .filter_map(char::from_u32)
117
+ .collect();
118
+ check(&mut transcoder, &format!("{pad}{text}"));
119
+ check(&mut transcoder, &format!("{text}{pad}"));
120
+ }
121
+ }
122
+ let latin1: String = (0x80..0x100).filter_map(char::from_u32).collect();
123
+ check(&mut transcoder, &latin1.repeat(9));
124
+ check(&mut transcoder, &format!("{}\u{100}", latin1.repeat(9)));
125
+ }
126
+
127
+ // Values of a megabyte, the longest measured, are decoded; past `LONG`
128
+ // text goes as UTF-8.
129
+ #[test]
130
+ fn text_up_to_long_is_decoded_and_longer_is_left_to_v8() {
131
+ let mut transcoder = Transcoder::default();
132
+ let megabyte = "caf\u{e9} ".repeat((1 << 20) / 6);
133
+ assert!(matches!(
134
+ transcoder.decode(megabyte.as_bytes()),
135
+ Some(Decoded::OneByte(_))
136
+ ));
137
+ let megabyte = "\u{1f600} go ".repeat((1 << 20) / 8);
138
+ assert!(matches!(
139
+ transcoder.decode(megabyte.as_bytes()),
140
+ Some(Decoded::TwoByte(_))
141
+ ));
142
+ let wide = "\u{6771}";
143
+ let longest = format!("{wide}{}", "a".repeat(LONG - wide.len()));
144
+ assert_eq!(longest.len(), LONG);
145
+ match transcoder.decode(longest.as_bytes()) {
146
+ Some(Decoded::TwoByte(units)) => assert_eq!(units.len(), LONG - 2),
147
+ _ => panic!("text of `LONG` bytes is decoded"),
148
+ }
149
+ assert!(
150
+ transcoder
151
+ .decode(format!("{longest}a").as_bytes())
152
+ .is_none()
153
+ );
154
+ let latin1 = "\u{e9}".repeat(LONG / 2 + 1);
155
+ assert!(transcoder.decode(latin1.as_bytes()).is_none());
156
+ }
157
+
158
+ #[test]
159
+ fn invalid_or_short_text_is_left_to_v8() {
160
+ let mut transcoder = Transcoder::default();
161
+ let pad = b"the quick brown fox ";
162
+ for invalid in [
163
+ &b"\xff"[..],
164
+ b"\x80",
165
+ b"\xc0\x80",
166
+ b"\xc3",
167
+ b"\xed\xa0\x80",
168
+ b"\xed\xbf\xbf",
169
+ b"\xf4\x90\x80\x80",
170
+ b"\xf5\x80\x80\x80",
171
+ b"\xe0\x80\xaf",
172
+ b"\xf0\x9f\x98",
173
+ b"\xe6\x9d",
174
+ ] {
175
+ for text in [[pad, invalid].concat(), [invalid, pad].concat()] {
176
+ assert!(transcoder.decode(&text).is_none(), "{text:x?}");
177
+ }
178
+ }
179
+ assert!(transcoder.decode("caf\u{e9}".as_bytes()).is_none());
180
+ assert!(transcoder.decode(b"").is_none());
181
+ }
182
+ }
@@ -157,9 +157,14 @@ impl<'a> Writer<'a> {
157
157
  }
158
158
  self.out.push(tag::TWO_BYTE_STRING);
159
159
  self.len(bytes);
160
- self.out.reserve(bytes);
161
- for unit in units {
162
- self.out.extend_from_slice(&unit.to_le_bytes());
160
+ // Written into slots made for them at once, which the compiler turns
161
+ // into wide copies; a unit at a time ran ten times slower.
162
+ let start = self.out.len();
163
+ self.out.resize(start.saturating_add(bytes), 0);
164
+ if let Some(slots) = self.out.get_mut(start..) {
165
+ for (slot, unit) in slots.as_chunks_mut::<2>().0.iter_mut().zip(units) {
166
+ *slot = unit.to_le_bytes();
167
+ }
163
168
  }
164
169
  }
165
170
 
@@ -9,7 +9,7 @@ use magnus::{
9
9
  Error, Fixnum, Float, Module, Object, RArray, RClass, RHash, RModule, RObject, RString, Ruby,
10
10
  Time, Value,
11
11
  };
12
- use mini_racer_engine::CallRequest;
12
+ use mini_racer_engine::{CallRequest, Decoded, Transcoder};
13
13
  use mini_racer_wire::Writer;
14
14
 
15
15
  use crate::gc_root::{OnceRoot, Pins, StaticRoot};
@@ -56,6 +56,8 @@ impl From<Error> for EncodeError {
56
56
  enum Item {
57
57
  // A pinned value, by its index in the holder.
58
58
  Value(usize),
59
+ // A pinned key of a hash.
60
+ Key(usize),
59
61
  // An element read when its turn comes, as serialize1 reads them, so a
60
62
  // to_time that changes the array is seen.
61
63
  Element { array: usize, index: isize },
@@ -101,6 +103,7 @@ struct Encoder<'r, 'o> {
101
103
  // whether to build a call's arguments itself.
102
104
  objects: usize,
103
105
  properties: usize,
106
+ transcoder: Transcoder,
104
107
  }
105
108
 
106
109
  /// Writes `value` as `mini_racer`'s `serialize` does, with the object ids V8
@@ -131,7 +134,7 @@ pub fn call(
131
134
  let mut encoder = Encoder::new(ruby, &mut bytes);
132
135
  let count = length(args.len().saturating_add(1));
133
136
  encoder.writer.begin_array(count);
134
- encoder.string(name);
137
+ encoder.string(name, Text::Name);
135
138
  args.iter()
136
139
  .try_for_each(|&arg| encoder.top(arg))
137
140
  .map_err(|error| error.raise(unsupported))?;
@@ -153,6 +156,22 @@ impl EncodeError {
153
156
  }
154
157
  }
155
158
 
159
+ // How mini_racer sends a String, by its encoding's name.
160
+ enum Sent {
161
+ Utf8,
162
+ Latin1,
163
+ Utf16,
164
+ }
165
+
166
+ // A key or a call's name goes in the form mini_racer sends unless it is
167
+ // ASCII: the runner builds only calls whose keys are UTF-8 or ASCII, and one
168
+ // is seldom long enough to be worth decoding.
169
+ #[derive(Clone, Copy, PartialEq, Eq)]
170
+ enum Text {
171
+ Value,
172
+ Name,
173
+ }
174
+
156
175
  fn length(len: usize) -> u32 {
157
176
  u32::try_from(len).unwrap_or(u32::MAX)
158
177
  }
@@ -181,6 +200,7 @@ impl<'r, 'o> Encoder<'r, 'o> {
181
200
  seen: HashMap::default(),
182
201
  objects: 0,
183
202
  properties: 0,
203
+ transcoder: Transcoder::default(),
184
204
  }
185
205
  }
186
206
  }
@@ -214,6 +234,10 @@ impl Encoder<'_, '_> {
214
234
  Item::EndArray(length) => self.writer.end_array(length),
215
235
  Item::EndObject(count) => self.writer.end_object(count),
216
236
  Item::Value(index) => self.value(self.pinned(index))?,
237
+ Item::Key(index) => {
238
+ let key = self.pinned(index);
239
+ self.key(key, rb_raw::ruby_type(self.ruby, key));
240
+ }
217
241
  Item::Element { array, index } => {
218
242
  let element = RArray::from_value(self.pinned(array))
219
243
  .map(|array| array.entry::<Value>(index))
@@ -239,14 +263,30 @@ impl Encoder<'_, '_> {
239
263
  RubyType::True => self.writer.bool(true),
240
264
  RubyType::False => self.writer.bool(false),
241
265
  RubyType::String => {
242
- self.string(RString::from_value(value).unwrap_or_else(|| ruby.str_new("")));
266
+ let text = RString::from_value(value).unwrap_or_else(|| ruby.str_new(""));
267
+ self.string(text, Text::Value);
243
268
  }
244
- RubyType::Symbol => self.string(rb_raw::sym2str(ruby, value)),
269
+ RubyType::Symbol => self.string(rb_raw::sym2str(ruby, value), Text::Value),
245
270
  _ => return false,
246
271
  }
247
272
  true
248
273
  }
249
274
 
275
+ // A String, a Symbol or an Integer, the keys `hash` sends.
276
+ fn key(&mut self, key: Value, kind: RubyType) {
277
+ let ruby = self.ruby;
278
+ match kind {
279
+ RubyType::String => {
280
+ let text = RString::from_value(key).unwrap_or_else(|| ruby.str_new(""));
281
+ self.string(text, Text::Name);
282
+ }
283
+ RubyType::Symbol => self.string(rb_raw::sym2str(ruby, key), Text::Name),
284
+ _ => {
285
+ self.immediate(key, kind);
286
+ }
287
+ }
288
+ }
289
+
250
290
  fn value(&mut self, value: Value) -> Result<(), EncodeError> {
251
291
  let kind = rb_raw::ruby_type(self.ruby, value);
252
292
  if self.immediate(value, kind) {
@@ -338,7 +378,7 @@ impl Encoder<'_, '_> {
338
378
  );
339
379
  let item_kind = rb_raw::ruby_type(self.ruby, item);
340
380
  if keys_fit && pairs.is_empty() && immediate(item_kind) {
341
- self.immediate(key, key_kind);
381
+ self.key(key, key_kind);
342
382
  self.immediate(item, item_kind);
343
383
  written = written.saturating_add(1);
344
384
  } else {
@@ -357,7 +397,7 @@ impl Encoder<'_, '_> {
357
397
  self.work.push(Item::EndObject(count));
358
398
  for (key, item) in pairs.into_iter().rev() {
359
399
  self.work.push(Item::Value(item));
360
- self.work.push(Item::Value(key));
400
+ self.work.push(Item::Key(key));
361
401
  }
362
402
  Ok(())
363
403
  }
@@ -465,21 +505,43 @@ impl Encoder<'_, '_> {
465
505
 
466
506
  // mini_racer tells encodings apart by name. The ones it sends as UTF-8
467
507
  // that strings most often have are told by index, without the lookup.
468
- fn string(&mut self, text: RString) {
508
+ // Bytes it sends as UTF-8 that are all ASCII are the same characters as
509
+ // one-byte text, which V8 copies where it would decode UTF-8. A value's
510
+ // other valid UTF-8 goes as the one-byte or two-byte text V8 would decode
511
+ // it to.
512
+ fn string(&mut self, text: RString, kind: Text) {
469
513
  let ruby = self.ruby;
470
514
  let index = text.enc_get();
471
515
  let utf8 = index == ruby.utf8_encindex()
472
516
  || index == ruby.usascii_encindex()
473
517
  || index == ruby.ascii8bit_encindex();
474
- rb_raw::with_string_bytes(ruby, text, |bytes| {
475
- if utf8 {
476
- return self.writer.utf8(bytes);
477
- }
518
+ let sent = if utf8 {
519
+ Sent::Utf8
520
+ } else {
478
521
  match RbEncoding::from(index).name() {
479
- "ISO-8859-1" => self.writer.latin1(bytes),
480
- "UTF-16LE" => self.writer.utf16le(bytes),
481
- _ => self.writer.utf8(bytes),
522
+ "ISO-8859-1" => Sent::Latin1,
523
+ "UTF-16LE" => Sent::Utf16,
524
+ _ => Sent::Utf8,
525
+ }
526
+ };
527
+ let writer = &mut self.writer;
528
+ match sent {
529
+ Sent::Latin1 => rb_raw::with_string_bytes(ruby, text, |bytes| writer.latin1(bytes)),
530
+ Sent::Utf16 => rb_raw::with_string_bytes(ruby, text, |bytes| writer.utf16le(bytes)),
531
+ Sent::Utf8 => {
532
+ let ascii = rb_raw::cached_ascii(ruby, text);
533
+ let transcoder = (kind == Text::Value).then_some(&mut self.transcoder);
534
+ rb_raw::with_string_bytes(ruby, text, |bytes| {
535
+ if ascii.unwrap_or_else(|| bytes.is_ascii()) {
536
+ return writer.latin1(bytes);
537
+ }
538
+ match transcoder.and_then(|transcoder| transcoder.decode(bytes)) {
539
+ Some(Decoded::OneByte(latin1)) => writer.latin1(latin1),
540
+ Some(Decoded::TwoByte(units)) => writer.utf16_aligned(units),
541
+ None => writer.utf8(bytes),
542
+ }
543
+ });
482
544
  }
483
- });
545
+ }
484
546
  }
485
547
  }
@@ -1,5 +1,6 @@
1
1
  use std::ffi::{c_int, c_long, c_void};
2
2
 
3
+ use magnus::encoding::Coderange;
3
4
  use magnus::rb_sys::{AsRawId, AsRawValue, FromRawValue, protect};
4
5
  use magnus::value::{IntoId, LazyId, ReprValue};
5
6
  use magnus::{Error, RArray, RHash, RString, Ruby, Value};
@@ -343,6 +344,18 @@ pub fn sym2str(ruby: &Ruby, symbol: Value) -> RString {
343
344
  RString::from_value(name).unwrap_or_else(|| ruby.str_new(""))
344
345
  }
345
346
 
347
+ /// Whether every byte of `s` is ASCII, from the coderange Ruby keeps in the
348
+ /// String's flags; `None` while Ruby has not worked it out. Working it out
349
+ /// here would check every character of non-ASCII text one at a time, slower
350
+ /// than reading the bytes. Never 7-bit in an encoding that is not
351
+ /// ASCII-compatible.
352
+ pub fn cached_ascii(_ruby: &Ruby, s: RString) -> Option<bool> {
353
+ match s.enc_coderange() {
354
+ Coderange::Unknown => None,
355
+ known => Some(known == Coderange::SevenBit),
356
+ }
357
+ }
358
+
346
359
  /// Runs `read` over the string's bytes without copying them; `read` must not
347
360
  /// call Ruby, which could change or move the string.
348
361
  pub fn with_string_bytes<R>(_ruby: &Ruby, s: RString, read: impl FnOnce(&[u8]) -> R) -> R {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacerRs
4
- VERSION = "1.0.4"
4
+ VERSION = "1.0.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Aragon
@@ -64,6 +64,7 @@ files:
64
64
  - crates/mini_racer_engine/src/snapshot.rs
65
65
  - crates/mini_racer_engine/src/spin.rs
66
66
  - crates/mini_racer_engine/src/sys.rs
67
+ - crates/mini_racer_engine/src/text.rs
67
68
  - crates/mini_racer_sync/Cargo.toml
68
69
  - crates/mini_racer_sync/src/channel.rs
69
70
  - crates/mini_racer_sync/src/fork.rs