mini_racer 0.21.4 → 0.22.1

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: f2e290a77bd6b38286dc4e4224d71579a1e8e41bbb8fd70029584f518383ba7f
4
- data.tar.gz: 479d9793ade9c46075986dd53c0a7ebacf62e1afb199fc6983e5b7c4fb4e0faa
3
+ metadata.gz: 522742f60d9062d656ffab6c1eb7ead5ceb0eabb6918b9582e9eec32f1f8a4c8
4
+ data.tar.gz: 404097b19b0e243bcd995e54f2560dd8fa5097dcc94a9488b435242350bc6719
5
5
  SHA512:
6
- metadata.gz: dc69216098ec6076dc90cf731fc5fd229526a4c21d3383fdb94448aace72068f36fc6bf6910cb09d8f42bf57c6926bf1527a2fd0871c11c5b08df820643cc23e
7
- data.tar.gz: b7b4b851b4cbc9ad35c56e39db1d5c7ccc19b23dabd9dd87f084ba37f0a59fb584c833128827d58ecdb0c99f75969383333d3b304d00f816ba544e83e5436639
6
+ metadata.gz: 723b3777cf8085b76662a3f4ede9fcdd8d4d8b31c31346ed0a87af1092b6a0323579b4dd6c3ec45a3c06f6cecedda7a30b8e807e69ecceaf5c3bf2ab9ceabc07
7
+ data.tar.gz: 36e1ce3f381f3d4017c10d811805320af94fd026a81eca8bdc71e6fa0cecd58a23f4668883de3cf8810ce57e41e33e9e4560810d4621b9825104a22dd2076e6b
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ - 0.22.1 - 27-08-2026
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
+
6
+ - 0.22.0 - 12-08-2026
7
+ - 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`
8
+ - 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
9
+ - Fix a timeout that fires just after its own evaluation ends, where it would stop the next evaluation instead
10
+ - Fix a race introduced in 0.21.4 where a request sent right after a nested dispatch (e.g. `perform_microtask_checkpoint` or a nested `call` from an attached callback) could be dropped, deadlocking the context
11
+
1
12
  - 0.21.4 - 24-06-2026
2
13
  - Fix stale V8 termination state after interrupts/timeouts so contexts remain usable after cancelled evaluations
3
14
  - Let Ruby interrupts wake MiniRacer calls without immediately terminating V8, allowing signal traps and nested callbacks to unwind safely
data/README.md CHANGED
@@ -60,6 +60,10 @@ puts context.eval("array_and_hash()")
60
60
  # => {"a" => 1, "b" => [1, {"a" => 1}]}
61
61
  ```
62
62
 
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
+
63
67
  ### Return binary data from Ruby to JavaScript
64
68
 
65
69
  Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
@@ -348,6 +352,40 @@ Performance is slightly better than running `context.eval("hello('George')")` si
348
352
  * compilation of eval'd string is avoided
349
353
  * function arguments don't need to be converted to JSON
350
354
 
355
+ ### Promises: call_await and eval_await
356
+
357
+ `call_await` and `eval_await` work like `call` and `eval`, but when the result is a
358
+ Promise they block until it settles and return the settled value. A rejected
359
+ promise raises `MiniRacer::RuntimeError`, just like a synchronous `throw`:
360
+
361
+ ```ruby
362
+ context = MiniRacer::Context.new
363
+ context.eval("async function f(x) { await Promise.resolve(); return x * 2 }")
364
+ context.call_await("f", 21)
365
+ # => 42
366
+
367
+ context.eval_await("(async () => 6 * 7)()")
368
+ # => 42
369
+
370
+ context.eval("async function boom() { throw new Error('kaboom') }")
371
+ context.call_await("boom")
372
+ # => raises MiniRacer::RuntimeError (Error: kaboom)
373
+ ```
374
+
375
+ Non-Promise results pass through unchanged, so `call_await` is a drop-in
376
+ superset of `call` (same for `eval_await`/`eval`).
377
+
378
+ A promise that never settles blocks forever, just like an infinite loop. The
379
+ `timeout:` option and `Context#stop` both interrupt it, raising
380
+ `MiniRacer::ScriptTerminatedError`.
381
+
382
+ Calling `call_await` or `eval_await` recursively on the same context from an
383
+ attached Ruby callback is not supported and raises `MiniRacer::RuntimeError`.
384
+ V8 cannot run the nested microtask checkpoint needed to settle such a call.
385
+ Synchronous nested `call` and `eval` remain supported.
386
+
387
+ `call_await` and `eval_await` are not currently supported on TruffleRuby.
388
+
351
389
  ### Microtask checkpoints
352
390
 
353
391
  V8 drains its microtask queue (e.g. callbacks queued via `Promise.resolve().then(...)`) automatically when script execution returns to the embedder, so most code "just works":
@@ -484,6 +522,25 @@ gcc >= 12.2 and Xcode >= 13 are, at the time of writing, known to work.
484
522
  * Concurrent cause .... JRuby
485
523
  * Supports execjs
486
524
 
525
+ ## Community
526
+
527
+ Libraries and integrations built with MiniRacer:
528
+
529
+ - [Humid](https://github.com/thoughtbot/humid) — JavaScript server-side rendering helpers for Rails.
530
+ - [ExecJS](https://github.com/rails/execjs) — A common interface for executing JavaScript from Ruby, with a MiniRacer runtime.
531
+ - [Opal](https://github.com/opal/opal) — A Ruby-to-JavaScript compiler with a MiniRacer CLI runner.
532
+ - [Handlebars::Engine](https://github.com/gi/handlebars-ruby) — A complete Ruby interface to Handlebars.js.
533
+ - [Minibars](https://github.com/combinaut/minibars) — A lightweight Handlebars.rb replacement built on MiniRacer.
534
+ - [Rtlcss](https://github.com/discourse/rtlcss) — Runs rtlcss from Ruby to convert stylesheets between LTR and RTL.
535
+ - [MessageFormat](https://github.com/discourse/messageformat) — Compiles MessageFormat messages from Ruby using `@messageformat/core`.
536
+ - [feelin](https://github.com/ekzo-dev/ruby-feelin) — Evaluates DMN FEEL expressions from Ruby.
537
+ - [parse-css](https://github.com/camertron/parse-css) — Parses CSS, including nested CSS, through the JavaScript parse-css library.
538
+
539
+ ### Notable users
540
+
541
+ - [Discourse](https://github.com/discourse/discourse) — A large Rails application using MiniRacer for JavaScript compilation, processing, and sandboxed evaluation.
542
+ - [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler) — Uses MiniRacer in its JavaScript build tooling.
543
+
487
544
  ## Contributing
488
545
 
489
546
  Bug reports and pull requests are welcome on GitHub at https://github.com/rubyjs/mini_racer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -62,6 +62,8 @@ static inline void rb_thread_lock_native_thread(void)
62
62
 
63
63
  #define countof(x) (sizeof(x) / sizeof(*(x)))
64
64
  #define endof(x) ((x) + countof(x))
65
+ #define BIGINT_STACK_WORDS 64
66
+ #define BIGINT_MAX_BYTES (16 * 1024 * 1024)
65
67
 
66
68
  // mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|,
67
69
  // from then on read-only and accessible without holding locks
@@ -152,7 +154,8 @@ typedef struct Context
152
154
  struct {
153
155
  pthread_mutex_t mtx;
154
156
  pthread_cond_t cv;
155
- int cancel;
157
+ int active; // dispatch thread only
158
+ int cancel; // protected by |mtx|
156
159
  } wd; // watchdog
157
160
  Barrier early_init, late_init;
158
161
  } Context;
@@ -352,33 +355,32 @@ static void des_date(void *arg, double v)
352
355
  put(arg, rb_time_new(sec, usec));
353
356
  }
354
357
 
355
- // note: v8 stores bigints in 1's complement, ruby in 2's complement,
356
- // 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
357
359
  static void des_bigint(void *arg, const void *p, size_t n, int sign)
358
360
  {
359
361
  VALUE v;
360
- size_t i;
361
362
  DesCtx *c;
362
- unsigned long *a, t, limbs[65]; // +1 to suppress sign extension
363
+ int flags;
363
364
 
364
365
  c = arg;
365
366
  if (*c->err)
366
367
  return;
367
- 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) {
368
373
  snprintf(c->err, sizeof(c->err), "bigint too big");
369
374
  return;
370
375
  }
371
- a = limbs;
372
- t = 0;
373
- for (i = 0; i < n; a++, i += sizeof(*a)) {
374
- memcpy(a, (char *)p + i, sizeof(*a));
375
- 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);
376
383
  }
377
- if (t >> 63)
378
- *a++ = 0; // suppress sign extension
379
- v = rb_big_unpack(limbs, a-limbs);
380
- if (sign < 0)
381
- v = rb_funcall(v, rb_intern("-@"), 0);
382
384
  put(c, v);
383
385
  }
384
386
 
@@ -579,12 +581,42 @@ static void add_string(Ser *s, VALUE v)
579
581
  return ser_string(s, p, n);
580
582
  }
581
583
 
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
+
582
616
  static int serialize1(Ser *s, VALUE refs, VALUE v)
583
617
  {
584
- unsigned long limbs[64];
585
618
  VALUE a, t, id;
586
619
  size_t i, n;
587
- int sign;
588
620
 
589
621
  if (*s->err)
590
622
  return -1;
@@ -669,15 +701,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
669
701
  ser_bool(s, 0);
670
702
  break;
671
703
  case T_BIGNUM:
672
- // note: v8 stores bigints in 1's complement, ruby in 2's complement,
673
- // so we have to take additional steps to ensure correct conversion
674
- memset(limbs, 0, sizeof(limbs));
675
- sign = rb_big_sign(v) ? 1 : -1;
676
- if (sign < 0)
677
- v = rb_big_mul(v, LONG2FIX(-1));
678
- rb_big_pack(v, limbs, countof(limbs));
679
- ser_bigint(s, limbs, countof(limbs), sign);
680
- break;
704
+ return serialize_bigint(s, v);
681
705
  case T_FIXNUM:
682
706
  ser_int(s, FIX2LONG(v));
683
707
  break;
@@ -772,7 +796,7 @@ static void *v8_watchdog(void *arg)
772
796
  if (c->wd.cancel)
773
797
  break;
774
798
  if (deadline_exceeded(deadline)) {
775
- v8_terminate_execution(c->pst);
799
+ v8_terminate_watchdog(c->pst);
776
800
  break;
777
801
  }
778
802
  }
@@ -786,8 +810,14 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
786
810
  pthread_t thr;
787
811
  int r;
788
812
 
789
- r = -1;
790
- if (c->timeout > 0 && (r = pthread_create(&thr, NULL, v8_watchdog, c))) {
813
+ if (c->timeout <= 0 || c->wd.active) {
814
+ func(c->pst, p, n);
815
+ return;
816
+ }
817
+ c->wd.active = 1;
818
+ r = pthread_create(&thr, NULL, v8_watchdog, c);
819
+ if (r) {
820
+ c->wd.active = 0;
791
821
  fprintf(stderr, "mini_racer: watchdog: pthread_create: %s\n", strerror(r));
792
822
  fflush(stderr);
793
823
  }
@@ -799,7 +829,9 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
799
829
  pthread_cond_signal(&c->wd.cv);
800
830
  pthread_mutex_unlock(&c->wd.mtx);
801
831
  pthread_join(thr, NULL);
832
+ v8_cancel_watchdog_termination(c->pst);
802
833
  c->wd.cancel = 0;
834
+ c->wd.active = 0;
803
835
  }
804
836
 
805
837
  static void dispatch1(Context *c, const uint8_t *p, size_t n)
@@ -810,7 +842,9 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
810
842
  switch (*p) {
811
843
  case 'A': return v8_attach(c->pst, p+1, n-1);
812
844
  case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
845
+ case 'D': return v8_timedwait(c, p+1, n-1, v8_call_await);
813
846
  case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
847
+ case 'F': return v8_timedwait(c, p+1, n-1, v8_eval_await);
814
848
  case 'H': return v8_heap_snapshot(c->pst);
815
849
  case 'M': return v8_perform_microtask_checkpoint(c->pst);
816
850
  case 'P': return v8_pump_message_loop(c->pst);
@@ -888,12 +922,12 @@ void v8_dispatch(Context *c)
888
922
  pthread_mutex_unlock(&c->mtx);
889
923
  }
890
924
 
891
- // only called when inside v8_call, v8_eval, or v8_pump_message_loop
925
+ // only called when inside v8_call, v8_eval (and their await variants),
926
+ // or v8_pump_message_loop
892
927
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
893
928
  {
894
929
  pthread_mutex_lock(&c->mtx);
895
930
  buf_reset(&c->v8_req);
896
- buf_reset(&c->req);
897
931
  if (c->res.len)
898
932
  c->res_ready = 1;
899
933
  pthread_cond_signal(&c->cv);
@@ -947,6 +981,8 @@ static VALUE deserialize1(DesCtx *d, const uint8_t *p, size_t n)
947
981
 
948
982
  if (des(&err, p, n, d))
949
983
  rb_raise(runtime_error, "%s", err);
984
+ if (*d->err)
985
+ rb_raise(runtime_error, "%s", d->err);
950
986
  if (d->tos != d->stack) // should not happen
951
987
  rb_raise(runtime_error, "parse stack not empty");
952
988
  return d->tos->a;
@@ -1009,7 +1045,7 @@ static void *rendezvous_callback(void *arg)
1009
1045
  goto fail;
1010
1046
  }
1011
1047
  ser_init1(&s, 'c'); // callback reply
1012
- if (serialize(&s, r)) { // should not happen
1048
+ if (serialize(&s, r)) {
1013
1049
  c->exception = rb_exc_new_cstr(internal_error, s.err);
1014
1050
  ser_reset(&s);
1015
1051
  goto fail;
@@ -1655,7 +1691,7 @@ static VALUE context_stop(VALUE self)
1655
1691
  return Qnil;
1656
1692
  }
1657
1693
 
1658
- static VALUE context_call(int argc, VALUE *argv, VALUE self)
1694
+ static VALUE context_call_common(int argc, VALUE *argv, VALUE self, char op)
1659
1695
  {
1660
1696
  VALUE name, args;
1661
1697
  VALUE a, e;
@@ -1666,8 +1702,8 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1666
1702
  rb_scan_args(argc, argv, "1*", &name, &args);
1667
1703
  Check_Type(name, T_STRING);
1668
1704
  rb_ary_unshift(args, name);
1669
- // request is (C)all, [name, args...] array
1670
- ser_init1(&s, 'C');
1705
+ // request is (C)all or (D) call_await, [name, args...] array
1706
+ ser_init1(&s, op);
1671
1707
  if (serialize(&s, args)) {
1672
1708
  ser_reset(&s);
1673
1709
  rb_raise(runtime_error, "Context.call: %s", s.err);
@@ -1679,7 +1715,17 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1679
1715
  return rb_ary_pop(a);
1680
1716
  }
1681
1717
 
1682
- static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1718
+ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1719
+ {
1720
+ return context_call_common(argc, argv, self, 'C');
1721
+ }
1722
+
1723
+ static VALUE context_call_await(int argc, VALUE *argv, VALUE self)
1724
+ {
1725
+ return context_call_common(argc, argv, self, 'D');
1726
+ }
1727
+
1728
+ static VALUE context_eval_common(int argc, VALUE *argv, VALUE self, char op)
1683
1729
  {
1684
1730
  VALUE a, e, source, filename, kwargs;
1685
1731
  Context *c;
@@ -1694,8 +1740,8 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1694
1740
  if (NIL_P(filename))
1695
1741
  filename = rb_str_new_cstr("<eval>");
1696
1742
  Check_Type(filename, T_STRING);
1697
- // request is (E)val, [filename, source] array
1698
- ser_init1(&s, 'E');
1743
+ // request is (E)val or (F) eval_await, [filename, source] array
1744
+ ser_init1(&s, op);
1699
1745
  ser_array_begin(&s, 2);
1700
1746
  add_string(&s, filename);
1701
1747
  add_string(&s, source);
@@ -1707,6 +1753,16 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1707
1753
  return rb_ary_pop(a);
1708
1754
  }
1709
1755
 
1756
+ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1757
+ {
1758
+ return context_eval_common(argc, argv, self, 'E');
1759
+ }
1760
+
1761
+ static VALUE context_eval_await(int argc, VALUE *argv, VALUE self)
1762
+ {
1763
+ return context_eval_common(argc, argv, self, 'F');
1764
+ }
1765
+
1710
1766
  static VALUE context_heap_stats(VALUE self)
1711
1767
  {
1712
1768
  VALUE a, h, k, v;
@@ -2147,7 +2203,9 @@ void Init_mini_racer_extension(void)
2147
2203
  rb_define_method(c, "dispose", context_dispose, 0);
2148
2204
  rb_define_method(c, "stop", context_stop, 0);
2149
2205
  rb_define_method(c, "call", context_call, -1);
2206
+ rb_define_method(c, "call_await", context_call_await, -1);
2150
2207
  rb_define_method(c, "eval", context_eval, -1);
2208
+ rb_define_method(c, "eval_await", context_eval_await, -1);
2151
2209
  rb_define_method(c, "heap_stats", context_heap_stats, 0);
2152
2210
  rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
2153
2211
  rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);