Skip to content

Commit 554923f

Browse files
authored
FIX: support clang 22+ compilation (#434)
* FIX: support clang 22+ compilation stop relying on mismatched uint64_t / unsigned long * FIX: support large bigint conversion Use Ruby's integer packing APIs to preserve bigint magnitudes and signs across architectures. Handle values up to 16 MiB with bounded temporary storage, reject oversized values cleanly, and cover large Ruby and JavaScript round trips. * correct CI on Mac
1 parent 6ca86db commit 554923f

5 files changed

Lines changed: 167 additions & 37 deletions

File tree

CHANGELOG

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
- vNext
2+
- Fix building with Clang 22+ and big-endian bigint serialization by making bigint serialization byte-oriented instead of relying on native `uint64_t`/`unsigned long` representations
3+
- Fix Ruby integers at or above 512 bits being silently truncated when passed to JavaScript, and large JavaScript bigints producing an invalid internal value when returned to Ruby
4+
- Support Ruby and JavaScript bigints up to a 16 MiB magnitude, using allocation-free conversion for common sizes and bounded dynamic storage for larger values
5+
16
- 0.22.0 - 12-08-2026
27
- Add `Context#call_await` and `Context#eval_await`: like `call`/`eval` but block until a returned Promise settles and return the settled value; rejections raise `MiniRacer::RuntimeError`
38
- Fix a `call` or `eval` made from a Ruby callback taking the timeout or `stop` meant for the evaluation around it, which then kept running

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ puts context.eval("array_and_hash()")
6060
# => {"a" => 1, "b" => [1, {"a" => 1}]}
6161
```
6262

63+
Ruby `Integer` and JavaScript `BigInt` values are converted exactly up to a
64+
16 MiB magnitude (about 134 million bits). Larger individual values are
65+
rejected with a serialization error rather than truncated.
66+
6367
### Return binary data from Ruby to JavaScript
6468

6569
Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:

ext/mini_racer_extension/mini_racer_extension.c

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ static inline void rb_thread_lock_native_thread(void)
6262

6363
#define countof(x) (sizeof(x) / sizeof(*(x)))
6464
#define endof(x) ((x) + countof(x))
65+
#define BIGINT_STACK_WORDS 64
66+
#define BIGINT_MAX_BYTES (16 * 1024 * 1024)
6567

6668
// mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|,
6769
// from then on read-only and accessible without holding locks
@@ -353,33 +355,32 @@ static void des_date(void *arg, double v)
353355
put(arg, rb_time_new(sec, usec));
354356
}
355357

356-
// note: v8 stores bigints in 1's complement, ruby in 2's complement,
357-
// so we have to take additional steps to ensure correct conversion
358+
// note: v8 stores bigints as a sign plus little-endian 64-bit magnitude words
358359
static void des_bigint(void *arg, const void *p, size_t n, int sign)
359360
{
360361
VALUE v;
361-
size_t i;
362362
DesCtx *c;
363-
unsigned long *a, t, limbs[65]; // +1 to suppress sign extension
363+
int flags;
364364

365365
c = arg;
366366
if (*c->err)
367367
return;
368-
if (n > sizeof(limbs) - sizeof(*limbs)) {
368+
if (n % sizeof(uint64_t)) {
369+
snprintf(c->err, sizeof(c->err), "bad bigint");
370+
return;
371+
}
372+
if (n > BIGINT_MAX_BYTES) {
369373
snprintf(c->err, sizeof(c->err), "bigint too big");
370374
return;
371375
}
372-
a = limbs;
373-
t = 0;
374-
for (i = 0; i < n; a++, i += sizeof(*a)) {
375-
memcpy(a, (char *)p + i, sizeof(*a));
376-
t = *a;
376+
if (n == 0) {
377+
v = INT2FIX(0);
378+
} else {
379+
flags = INTEGER_PACK_LITTLE_ENDIAN;
380+
if (sign < 0)
381+
flags |= INTEGER_PACK_NEGATIVE;
382+
v = rb_integer_unpack(p, n/sizeof(uint64_t), sizeof(uint64_t), 0, flags);
377383
}
378-
if (t >> 63)
379-
*a++ = 0; // suppress sign extension
380-
v = rb_big_unpack(limbs, a-limbs);
381-
if (sign < 0)
382-
v = rb_funcall(v, rb_intern("-@"), 0);
383384
put(c, v);
384385
}
385386

@@ -580,12 +581,42 @@ static void add_string(Ser *s, VALUE v)
580581
return ser_string(s, p, n);
581582
}
582583

584+
// Keep small values allocation-free while allowing large values up to a
585+
// deliberate per-value limit that bounds temporary conversion storage.
586+
static int serialize_bigint(Ser *s, VALUE v)
587+
{
588+
uint64_t stack_words[BIGINT_STACK_WORDS];
589+
uint64_t *words;
590+
size_t nwords, nbytes;
591+
int packed;
592+
593+
nwords = rb_absint_numwords(v, 64, NULL);
594+
if (nwords == (size_t)-1 || nwords > BIGINT_MAX_BYTES/sizeof(*words))
595+
return bail(&s->err, "bigint too big");
596+
nbytes = nwords * sizeof(*words);
597+
words = stack_words;
598+
if (nwords > countof(stack_words)) {
599+
words = malloc(nbytes);
600+
if (!words)
601+
return bail(&s->err, "out of memory");
602+
}
603+
packed = rb_integer_pack(v, words, nwords, sizeof(*words), 0,
604+
INTEGER_PACK_LITTLE_ENDIAN);
605+
if (packed < -1 || packed > 1) {
606+
if (words != stack_words)
607+
free(words);
608+
return bail(&s->err, "bigint too big");
609+
}
610+
ser_bigint(s, words, nbytes, packed < 0 ? -1 : 1);
611+
if (words != stack_words)
612+
free(words);
613+
return *s->err ? -1 : 0;
614+
}
615+
583616
static int serialize1(Ser *s, VALUE refs, VALUE v)
584617
{
585-
unsigned long limbs[64];
586618
VALUE a, t, id;
587619
size_t i, n;
588-
int sign;
589620

590621
if (*s->err)
591622
return -1;
@@ -670,15 +701,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
670701
ser_bool(s, 0);
671702
break;
672703
case T_BIGNUM:
673-
// note: v8 stores bigints in 1's complement, ruby in 2's complement,
674-
// so we have to take additional steps to ensure correct conversion
675-
memset(limbs, 0, sizeof(limbs));
676-
sign = rb_big_sign(v) ? 1 : -1;
677-
if (sign < 0)
678-
v = rb_big_mul(v, LONG2FIX(-1));
679-
rb_big_pack(v, limbs, countof(limbs));
680-
ser_bigint(s, limbs, countof(limbs), sign);
681-
break;
704+
return serialize_bigint(s, v);
682705
case T_FIXNUM:
683706
ser_int(s, FIX2LONG(v));
684707
break;
@@ -958,6 +981,8 @@ static VALUE deserialize1(DesCtx *d, const uint8_t *p, size_t n)
958981

959982
if (des(&err, p, n, d))
960983
rb_raise(runtime_error, "%s", err);
984+
if (*d->err)
985+
rb_raise(runtime_error, "%s", d->err);
961986
if (d->tos != d->stack) // should not happen
962987
rb_raise(runtime_error, "parse stack not empty");
963988
return d->tos->a;
@@ -1020,7 +1045,7 @@ static void *rendezvous_callback(void *arg)
10201045
goto fail;
10211046
}
10221047
ser_init1(&s, 'c'); // callback reply
1023-
if (serialize(&s, r)) { // should not happen
1048+
if (serialize(&s, r)) {
10241049
c->exception = rb_exc_new_cstr(internal_error, s.err);
10251050
ser_reset(&s);
10261051
goto fail;

ext/mini_racer_extension/serde.c

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -243,33 +243,38 @@ static void ser_num(Ser *s, double v)
243243
}
244244
}
245245

246-
// ser_bigint: |n| is in bytes, not quadwords
247-
static void ser_bigint(Ser *s, const uint64_t *p, size_t n, int sign)
246+
// ser_bigint: |p| points to |n| bytes, interpreted as little-endian
247+
// 64-bit words. Keep the interface byte-oriented so callers don't need to
248+
// expose a concrete word type.
249+
static void ser_bigint(Ser *s, const void *p, size_t n, int sign)
248250
{
251+
const uint8_t *bytes;
252+
249253
if (*s->err)
250254
return;
251255
if (n % 8) {
252256
snprintf(s->err, sizeof(s->err), "bad bigint");
253257
return;
254258
}
259+
bytes = p;
255260
w_byte(s, 'Z');
256261
// chop off high all-zero words
257-
n /= 8;
258-
while (n--)
259-
if (p[n])
260-
break;
261-
if (n == (size_t)-1) {
262+
while (n > 0 && bytes[n-1] == 0)
263+
n--;
264+
if (n == 0) {
262265
w_byte(s, 0); // normalized zero
263266
} else {
264-
n = 8*n + 8;
267+
n = (n + 7) & ~(size_t)7;
265268
w_varint(s, 2*n + (sign < 0));
266-
w(s, p, n);
269+
w(s, bytes, n);
267270
}
268271
}
269272

270273
static void ser_int(Ser *s, int64_t v)
271274
{
275+
uint8_t bytes[8];
272276
uint64_t t;
277+
size_t i;
273278
int sign;
274279

275280
if (*s->err)
@@ -279,8 +284,10 @@ static void ser_int(Ser *s, int64_t v)
279284
if (v <= INT64_MAX/1024)
280285
return ser_num(s, v);
281286
t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
287+
for (i = 0; i < sizeof(bytes); i++)
288+
bytes[i] = t >> (8*i);
282289
sign = v < 0 ? -1 : 1;
283-
ser_bigint(s, &t, sizeof(t), sign);
290+
ser_bigint(s, bytes, sizeof(bytes), sign);
284291
} else {
285292
w_byte(s, 'I');
286293
w_zigzag(s, v);

test/mini_racer_test.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,95 @@ def test_large_integer
16811681
end
16821682
end
16831683

1684+
def test_fixnum_bigint_serialization
1685+
if RUBY_ENGINE == "truffleruby"
1686+
skip "C extension is not used on TruffleRuby"
1687+
end
1688+
1689+
[-(2**62), (2**62) - 1].each do |integer|
1690+
context = MiniRacer::Context.new
1691+
context.attach("test", proc { integer })
1692+
1693+
assert_equal "bigint", context.eval("typeof test()")
1694+
assert_equal integer.to_s, context.eval("test().toString()")
1695+
assert_equal integer, context.eval("test()")
1696+
end
1697+
end
1698+
1699+
def test_large_bigint_serialization_uses_all_packed_limbs
1700+
if RUBY_ENGINE == "truffleruby"
1701+
skip "C extension is not used on TruffleRuby"
1702+
end
1703+
1704+
[
1705+
(2**64) - 1,
1706+
-((2**64) - 1),
1707+
2**64,
1708+
-(2**64),
1709+
(2**128) + (2**64) + 12_345,
1710+
-((2**128) + (2**64) + 12_345),
1711+
2**512,
1712+
-((2**512) + 1),
1713+
(2**1024) + (2**512) + 1,
1714+
-((2**1024) + (2**512) + 1),
1715+
(2**4095) + (2**2048) + 17,
1716+
-((2**4095) + (2**2048) + 17),
1717+
(2**4096) + (2**2048) + 17,
1718+
-((2**4096) + (2**2048) + 17),
1719+
(2**32_768) + (2**16_384) + 17,
1720+
-((2**32_768) + (2**16_384) + 17)
1721+
].each do |big_int|
1722+
context = MiniRacer::Context.new
1723+
context.attach("test", proc { big_int })
1724+
1725+
assert_equal "bigint", context.eval("typeof test()")
1726+
assert_equal big_int.to_s, context.eval("test().toString()")
1727+
assert_equal big_int, context.eval("test()")
1728+
end
1729+
end
1730+
1731+
def test_v8_bigint_deserialization_handles_zero_and_large_nested_values
1732+
if RUBY_ENGINE == "truffleruby"
1733+
skip "C extension is not used on TruffleRuby"
1734+
end
1735+
1736+
context = MiniRacer::Context.new
1737+
expected = (2**32_768) + (2**16_384) + 17
1738+
1739+
assert_equal 0, context.eval("0n")
1740+
assert_equal((2**64) - 1, context.eval("(2n ** 64n) - 1n"))
1741+
assert_equal expected, context.eval("(2n ** 32768n) + (2n ** 16384n) + 17n")
1742+
assert_equal(
1743+
-expected,
1744+
context.eval("-((2n ** 32768n) + (2n ** 16384n) + 17n)")
1745+
)
1746+
assert_equal [expected],
1747+
context.eval("[(2n ** 32768n) + (2n ** 16384n) + 17n]")
1748+
end
1749+
1750+
def test_bigint_bridge_rejects_values_larger_than_dynamic_limit
1751+
if RUBY_ENGINE == "truffleruby"
1752+
skip "C extension is not used on TruffleRuby"
1753+
end
1754+
1755+
context = MiniRacer::Context.new
1756+
max_bigint_bytes = 16 * 1024 * 1024 # BIGINT_MAX_BYTES in the C extension
1757+
first_rejected_bit = max_bigint_bytes * 8
1758+
too_big = 1 << first_rejected_bit
1759+
context.attach("test", proc { too_big })
1760+
1761+
error = assert_raises(MiniRacer::InternalError) { context.eval("test()") }
1762+
assert_equal "bigint too big", error.message
1763+
assert_equal 2, context.eval("1 + 1")
1764+
1765+
error =
1766+
assert_raises(MiniRacer::RuntimeError) do
1767+
context.eval("1n << #{first_rejected_bit}n")
1768+
end
1769+
assert_equal "bigint too big", error.message
1770+
assert_equal 2, context.eval("1 + 1")
1771+
end
1772+
16841773
def test_uint8array_is_converted_to_string
16851774
context = MiniRacer::Context.new
16861775
result = context.eval("new Uint8Array([0, 1, 2, 3])")

0 commit comments

Comments
 (0)