mini_racer 0.21.0 → 0.21.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cd44fe0925a58e264712d933d805af54b517d31cdbefc26455e7e139e234279
4
- data.tar.gz: 17190bc083b885c838b0dfc3424c3fc08fb02df354972a3007a292ddf70afc22
3
+ metadata.gz: 70ca9828687a7fcdb44048eb1a3e9ad2fe3f38082492504fc22563fe94fa524a
4
+ data.tar.gz: 61719146ed1430235269e80a21a60ef59b98f22f470cda13820074f9cecea818
5
5
  SHA512:
6
- metadata.gz: 9cdd4cdf20f9be0e3ceb34daa48f7f1eeb93c4c11eada10c23e2c3e3dbd82cf7a13c8102e67140cc15c028c51a468c6e74ac518c76050cadceb33aa99fd71b7d
7
- data.tar.gz: c7110aabb771e18bccd85283696e8f811970cf8d03bb0ef1f573a84f2fa7da2ef1014b5138513b09d80825de96ddebe7e6316f79efbe8adecbd1ff202b14e4d5
6
+ metadata.gz: 944e4a0e403b134e0206c6d6381efa3d9f120af9cfce18f61fc85c3eef5e6a077c64a457142d347041059a6b76f24990dbd15c17691bbcb3306ff1ea877983a4
7
+ data.tar.gz: f3003a61146634c76ad28ab99be4d6e96b34490522edb13607b03fcf9354fce135844947af3d23f9d37cc012a26ba4c280c8d4b19d410d47bcebc753cdc6c34f
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ - 0.21.2 - 11-06-2026
2
+ - Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
3
+ - Fix native memory leaks in `Context#heap_snapshot`/`Context#write_heap_snapshot`; thanks to Pranjali Thakur from depthfirst.com
4
+ - Fix large integral JavaScript numbers wrapping to negative Ruby integers; thanks to Pranjali Thakur from depthfirst.com
5
+ - Fix Ruby callback exceptions with embedded NUL bytes permanently deadlocking a context; thanks to Pranjali Thakur from depthfirst.com
6
+ - Preserve embedded NUL bytes in JavaScript exception messages and attached function names, and reject unsafe V8 flags containing NUL bytes or overly long values
7
+
8
+ - 0.21.1 - 25-05-2026
9
+ - Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
10
+ - Stop and join the reusable `:single_threaded` runner when contexts are disposed
11
+ - Document `:single_threaded` fork-safety requirements for pre-fork contexts
12
+
1
13
  - 0.21.0 - 16-04-2026
2
14
  - Add MiniRacer::Binary for returning Uint8Array to JavaScript from attached Ruby callbacks
3
15
 
data/README.md CHANGED
@@ -139,6 +139,15 @@ Since 0.6.1 mini_racer does support V8 single threaded platform mode which shoul
139
139
  MiniRacer::Platform.set_flags!(:single_threaded)
140
140
  ```
141
141
 
142
+ When using pre-fork `MiniRacer::Context` objects in `:single_threaded` mode,
143
+ ensure the process only forks while MiniRacer is quiescent: no thread may be
144
+ evaluating JavaScript, calling into a context, disposing/freeing a context,
145
+ running a Ruby callback from JavaScript, or otherwise using MiniRacer at the
146
+ instant of `fork`. In multi-threaded applications, guard all MiniRacer context
147
+ operations and the `fork` itself with the same application-level lock. Forking
148
+ while a MiniRacer operation is in progress can leave inherited pthread mutexes
149
+ in an unusable state in the child process.
150
+
142
151
  If you want to ensure your application does not leak memory after fork either:
143
152
 
144
153
  1. Ensure no `MiniRacer::Context` objects are created in the master process; or
@@ -339,6 +348,38 @@ Performance is slightly better than running `context.eval("hello('George')")` si
339
348
  * compilation of eval'd string is avoided
340
349
  * function arguments don't need to be converted to JSON
341
350
 
351
+ ### Microtask checkpoints
352
+
353
+ 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":
354
+
355
+ ```ruby
356
+ context = MiniRacer::Context.new
357
+ context.eval(<<~JS)
358
+ let x = 0;
359
+ Promise.resolve().then(() => x = 99);
360
+ JS
361
+ context.eval("x")
362
+ # => 99
363
+ ```
364
+
365
+ When JavaScript invokes a Ruby callback synchronously and you need queued microtasks to drain mid-execution — e.g. for spec-compliant ordering across a chain of synchronous `dispatchEvent` listeners — call `context.perform_microtask_checkpoint` from the callback:
366
+
367
+ ```ruby
368
+ context = MiniRacer::Context.new
369
+ context.attach("drain", -> { context.perform_microtask_checkpoint })
370
+ context.eval(<<~JS)
371
+ globalThis.log = [];
372
+ Promise.resolve().then(() => log.push("microtask"));
373
+ log.push("before");
374
+ drain();
375
+ log.push("after");
376
+ JS
377
+ context.eval("log")
378
+ # => ["before", "microtask", "after"]
379
+ ```
380
+
381
+ Without `drain()` the order would be `["before", "after", "microtask"]` because the microtask only runs once the outermost script returns. `perform_microtask_checkpoint` is a thin wrapper over V8's `MicrotasksScope::PerformCheckpoint`.
382
+
342
383
  ## Performance
343
384
 
344
385
  The `bench` folder contains benchmark.
@@ -4,6 +4,7 @@
4
4
  #include <stdlib.h>
5
5
  #include <string.h>
6
6
  #include <pthread.h>
7
+ #include <unistd.h>
7
8
  #include <math.h>
8
9
 
9
10
  #if defined(__linux__) && !defined(__GLIBC__)
@@ -136,6 +137,9 @@ typedef struct Context
136
137
  VALUE exception; // pending exception or Qnil
137
138
  Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv|
138
139
  Buf snapshot;
140
+ pthread_t single_threaded_thr;
141
+ pid_t single_threaded_pid;
142
+ int single_threaded_thr_started;
139
143
  // |rr_mtx| stands for "recursive ruby mutex"; it's used to exclude
140
144
  // other ruby threads but allow reentrancy from the same ruby thread
141
145
  // (think ruby->js->ruby->js calls)
@@ -312,13 +316,19 @@ static void des_bool(void *arg, int v)
312
316
 
313
317
  static void des_int(void *arg, int64_t v)
314
318
  {
315
- put(arg, LONG2FIX(v));
319
+ put(arg, LL2NUM((LONG_LONG)v));
316
320
  }
317
321
 
318
322
  static void des_num(void *arg, double v)
319
323
  {
320
- if (isfinite(v) && v == trunc(v) && v >= INT64_MIN && v <= INT64_MAX) {
321
- put(arg, LONG2FIX(v));
324
+ if (isfinite(v) && v == trunc(v)) {
325
+ // INT64_MAX is not exactly representable as a double: it rounds up to
326
+ // 2^63, which would let 2^63 through and make the cast undefined.
327
+ if (v >= -0x1p63 && v < 0x1p63) {
328
+ put(arg, LL2NUM((LONG_LONG)v));
329
+ } else {
330
+ put(arg, rb_dbl2big(v));
331
+ }
322
332
  } else {
323
333
  put(arg, DBL2NUM(v));
324
334
  }
@@ -360,19 +370,8 @@ static void des_bigint(void *arg, const void *p, size_t n, int sign)
360
370
  if (t >> 63)
361
371
  *a++ = 0; // suppress sign extension
362
372
  v = rb_big_unpack(limbs, a-limbs);
363
- if (sign < 0) {
364
- // rb_big_unpack returns T_FIXNUM for smallish bignums
365
- switch (TYPE(v)) {
366
- case T_BIGNUM:
367
- v = rb_big_mul(v, LONG2FIX(-1));
368
- break;
369
- case T_FIXNUM:
370
- v = LONG2FIX(-1 * FIX2LONG(v));
371
- break;
372
- default:
373
- abort();
374
- }
375
- }
373
+ if (sign < 0)
374
+ v = rb_funcall(v, rb_intern("-@"), 0);
376
375
  put(c, v);
377
376
  }
378
377
 
@@ -806,6 +805,7 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
806
805
  case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
807
806
  case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
808
807
  case 'H': return v8_heap_snapshot(c->pst);
808
+ case 'M': return v8_perform_microtask_checkpoint(c->pst);
809
809
  case 'P': return v8_pump_message_loop(c->pst);
810
810
  case 'S': return v8_heap_stats(c->pst);
811
811
  case 'T': return v8_snapshot(c->pst, p+1, n-1);
@@ -868,18 +868,10 @@ void v8_dispatch(Context *c)
868
868
  // only called when inside v8_call, v8_eval, or v8_pump_message_loop
869
869
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
870
870
  {
871
- struct rendezvous_nogvl *args;
872
-
873
871
  buf_reset(&c->req);
874
- if (single_threaded) {
875
- assert(*c->res.buf == 'c'); // js -> ruby callback
876
- args = &(struct rendezvous_nogvl){c, &c->req, &c->res};
877
- rb_thread_call_with_gvl(rendezvous_callback, args);
878
- } else {
879
- pthread_cond_signal(&c->cv);
880
- while (!c->req.len)
881
- pthread_cond_wait(&c->cv, &c->mtx);
882
- }
872
+ pthread_cond_signal(&c->cv);
873
+ while (!c->req.len)
874
+ pthread_cond_wait(&c->cv, &c->mtx);
883
875
  buf_reset(&c->res);
884
876
  *p = c->req.buf;
885
877
  *n = c->req.len;
@@ -940,6 +932,7 @@ static VALUE rendezvous_callback_do(VALUE arg)
940
932
  Context *c;
941
933
  DesCtx d;
942
934
  Buf *b;
935
+ long id;
943
936
 
944
937
  a = (void *)arg;
945
938
  b = a->res;
@@ -949,7 +942,12 @@ static VALUE rendezvous_callback_do(VALUE arg)
949
942
  DesCtx_init(&d);
950
943
  args = deserialize1(&d, b->buf+1, b->len-1); // skip 'c' marker
951
944
  func = rb_ary_pop(args); // callback id
952
- func = rb_ary_entry(c->procs, FIX2LONG(func));
945
+ if (!RB_INTEGER_TYPE_P(func))
946
+ rb_raise(runtime_error, "bad callback id");
947
+ id = NUM2LONG(func);
948
+ if (id < 0 || id >= RARRAY_LEN(c->procs))
949
+ rb_raise(runtime_error, "bad callback id");
950
+ func = rb_ary_entry(c->procs, id);
953
951
  return rb_funcall2(func, rb_intern("call"), RARRAY_LENINT(args), RARRAY_PTR(args));
954
952
  }
955
953
 
@@ -985,16 +983,51 @@ fail:
985
983
  ser_init0(&s); // ruby exception pending
986
984
  w_byte(&s, 'e'); // send ruby error message to v8 thread
987
985
  r = rb_funcall(c->exception, rb_intern("to_s"), 0);
988
- err = StringValueCStr(r);
986
+ err = StringValuePtr(r);
989
987
  if (err)
990
- w(&s, err, strlen(err));
988
+ w(&s, err, RSTRING_LEN(r));
991
989
  goto out;
992
990
  }
993
991
 
992
+ static void *single_threaded_runner(void *arg)
993
+ {
994
+ Context *c;
995
+
996
+ c = arg;
997
+ pthread_mutex_lock(&c->mtx);
998
+ for (;;) {
999
+ while (!c->req.len && atomic_load(&c->quit) < 1)
1000
+ pthread_cond_wait(&c->cv, &c->mtx);
1001
+ if (atomic_load(&c->quit) >= 1)
1002
+ break;
1003
+ v8_single_threaded_enter(c->pst, c, dispatch);
1004
+ pthread_cond_signal(&c->cv);
1005
+ }
1006
+ pthread_mutex_unlock(&c->mtx);
1007
+ return NULL;
1008
+ }
1009
+
1010
+ static int single_threaded_runner_start(Context *c)
1011
+ {
1012
+ pid_t pid;
1013
+ int r;
1014
+
1015
+ pid = getpid();
1016
+ if (c->single_threaded_thr_started && c->single_threaded_pid == pid)
1017
+ return 0;
1018
+ c->single_threaded_thr_started = 0;
1019
+ c->single_threaded_pid = pid;
1020
+ r = pthread_create(&c->single_threaded_thr, NULL, single_threaded_runner, c);
1021
+ if (!r)
1022
+ c->single_threaded_thr_started = 1;
1023
+ return r;
1024
+ }
1025
+
994
1026
  static inline void *rendezvous_nogvl(void *arg)
995
1027
  {
996
1028
  struct rendezvous_nogvl *a;
997
1029
  Context *c;
1030
+ int r;
998
1031
 
999
1032
  a = arg;
1000
1033
  c = a->context;
@@ -1010,7 +1043,16 @@ next:
1010
1043
  assert(c->res.len == 0);
1011
1044
  buf_move(a->req, &c->req); // v8 thread takes ownership of req
1012
1045
  if (single_threaded) {
1013
- v8_single_threaded_enter(c->pst, c, dispatch);
1046
+ r = single_threaded_runner_start(c);
1047
+ if (r) {
1048
+ buf_move(&c->req, a->req);
1049
+ pthread_mutex_unlock(&c->mtx);
1050
+ c->depth--;
1051
+ pthread_mutex_unlock(&c->rr_mtx);
1052
+ return (void *)(intptr_t)r;
1053
+ }
1054
+ pthread_cond_signal(&c->cv);
1055
+ do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1014
1056
  } else {
1015
1057
  pthread_cond_signal(&c->cv);
1016
1058
  do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
@@ -1019,6 +1061,7 @@ next:
1019
1061
  pthread_mutex_unlock(&c->mtx);
1020
1062
  if (*a->res->buf == 'c') { // js -> ruby callback?
1021
1063
  rb_thread_call_with_gvl(rendezvous_callback, a);
1064
+ buf_reset(a->res);
1022
1065
  goto next;
1023
1066
  }
1024
1067
  c->depth--;
@@ -1028,12 +1071,16 @@ next:
1028
1071
 
1029
1072
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1030
1073
  {
1074
+ void *r;
1075
+
1031
1076
  if (atomic_load(&c->quit)) {
1032
1077
  buf_reset(req);
1033
1078
  rb_raise(context_disposed_error, "disposed context");
1034
1079
  }
1035
- rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1036
- NULL, NULL, 0);
1080
+ r = rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1081
+ NULL, NULL, 0);
1082
+ if (r)
1083
+ rb_raise(runtime_error, "pthread_create: %s", strerror((int)(intptr_t)r));
1037
1084
  }
1038
1085
 
1039
1086
  // send request to & receive reply from v8 thread; takes ownership of |req|
@@ -1070,16 +1117,35 @@ static VALUE rendezvous(Context *c, Buf *req)
1070
1117
  return rendezvous1(c, req, &d);
1071
1118
  }
1072
1119
 
1120
+ static void raise_exception_with_message(VALUE klass, VALUE e)
1121
+ {
1122
+ long n;
1123
+ VALUE message;
1124
+
1125
+ if (NIL_P(e))
1126
+ return;
1127
+ e = StringValue(e);
1128
+ n = RSTRING_LEN(e);
1129
+ if (n == 0 || RSTRING_PTR(e)[0] == NO_ERROR)
1130
+ return;
1131
+ message = rb_str_subseq(e, 1, n - 1);
1132
+ rb_exc_raise(rb_exc_new_str(klass, message));
1133
+ }
1134
+
1073
1135
  static void handle_exception(VALUE e)
1074
1136
  {
1075
1137
  const char *s;
1076
1138
  VALUE klass;
1139
+ long n;
1077
1140
 
1078
1141
  if (NIL_P(e))
1079
1142
  return;
1080
1143
  e = StringValue(e);
1081
- s = StringValueCStr(e);
1082
- switch (*s) {
1144
+ n = RSTRING_LEN(e);
1145
+ if (n == 0)
1146
+ return;
1147
+ s = RSTRING_PTR(e);
1148
+ switch ((unsigned char)*s) {
1083
1149
  case NO_ERROR:
1084
1150
  return;
1085
1151
  case INTERNAL_ERROR:
@@ -1098,9 +1164,9 @@ static void handle_exception(VALUE e)
1098
1164
  klass = terminated_error;
1099
1165
  break;
1100
1166
  default:
1101
- rb_raise(internal_error, "bad error class %02x", *s);
1167
+ rb_raise(internal_error, "bad error class %02x", (unsigned char)*s);
1102
1168
  }
1103
- rb_enc_raise(rb_enc_get(e), klass, "%s", s+1);
1169
+ raise_exception_with_message(klass, e);
1104
1170
  }
1105
1171
 
1106
1172
  static VALUE context_alloc(VALUE klass)
@@ -1190,7 +1256,16 @@ static void *context_free_thread_do(void *arg)
1190
1256
  Context *c;
1191
1257
 
1192
1258
  c = arg;
1193
- v8_single_threaded_dispose(c->pst);
1259
+ if (single_threaded && c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1260
+ pthread_mutex_lock(&c->mtx);
1261
+ atomic_store(&c->quit, 2);
1262
+ pthread_cond_signal(&c->cv);
1263
+ pthread_mutex_unlock(&c->mtx);
1264
+ pthread_join(c->single_threaded_thr, NULL);
1265
+ }
1266
+ if (c->pst)
1267
+ v8_single_threaded_dispose(c->pst);
1268
+ pthread_mutex_lock(&c->mtx);
1194
1269
  context_destroy(c);
1195
1270
  return NULL;
1196
1271
  }
@@ -1263,13 +1338,17 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1263
1338
  Context *c;
1264
1339
  VALUE e;
1265
1340
  Ser s;
1341
+ long id;
1266
1342
 
1267
1343
  TypedData_Get_Struct(self, Context, &context_type, c);
1344
+ id = RARRAY_LEN(c->procs);
1345
+ if (id > INT32_MAX)
1346
+ rb_raise(runtime_error, "too many callbacks");
1268
1347
  // request is (A)ttach, [name, id] array
1269
1348
  ser_init1(&s, 'A');
1270
1349
  ser_array_begin(&s, 2);
1271
1350
  add_string(&s, name);
1272
- ser_int(&s, RARRAY_LENINT(c->procs));
1351
+ ser_int(&s, id);
1273
1352
  ser_array_end(&s, 2);
1274
1353
  rb_ary_push(c->procs, proc);
1275
1354
  // response is an exception or undefined
@@ -1284,8 +1363,18 @@ static void *context_dispose_do(void *arg)
1284
1363
 
1285
1364
  c = arg;
1286
1365
  if (single_threaded) {
1366
+ pthread_mutex_lock(&c->mtx);
1367
+ while (c->req.len || c->res.len)
1368
+ pthread_cond_wait(&c->cv, &c->mtx);
1287
1369
  atomic_store(&c->quit, 1); // disposed
1288
- // intentionally a no-op for now
1370
+ if (c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1371
+ pthread_cond_signal(&c->cv);
1372
+ pthread_mutex_unlock(&c->mtx);
1373
+ pthread_join(c->single_threaded_thr, NULL);
1374
+ pthread_mutex_lock(&c->mtx);
1375
+ c->single_threaded_thr_started = 0;
1376
+ }
1377
+ pthread_mutex_unlock(&c->mtx);
1289
1378
  } else {
1290
1379
  pthread_mutex_lock(&c->mtx);
1291
1380
  while (c->req.len || c->res.len)
@@ -1393,6 +1482,20 @@ static VALUE context_heap_stats(VALUE self)
1393
1482
  return h;
1394
1483
  }
1395
1484
 
1485
+ static VALUE buf_reset_ensure(VALUE arg)
1486
+ {
1487
+ buf_reset((Buf *)arg);
1488
+ return Qnil;
1489
+ }
1490
+
1491
+ static VALUE heap_snapshot_to_str(VALUE arg)
1492
+ {
1493
+ Buf *res;
1494
+
1495
+ res = (Buf *)arg;
1496
+ return rb_utf8_str_new((char *)res->buf, res->len);
1497
+ }
1498
+
1396
1499
  static VALUE context_heap_snapshot(VALUE self)
1397
1500
  {
1398
1501
  Buf req, res;
@@ -1402,7 +1505,19 @@ static VALUE context_heap_snapshot(VALUE self)
1402
1505
  buf_init(&req);
1403
1506
  buf_putc(&req, 'H'); // (H)eap snapshot, returns plain bytes
1404
1507
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1405
- return rb_utf8_str_new((char *)res.buf, res.len);
1508
+ return rb_ensure(heap_snapshot_to_str, (VALUE)&res,
1509
+ buf_reset_ensure, (VALUE)&res);
1510
+ }
1511
+
1512
+ static VALUE context_perform_microtask_checkpoint(VALUE self)
1513
+ {
1514
+ Context *c;
1515
+ Buf b;
1516
+
1517
+ TypedData_Get_Struct(self, Context, &context_type, c);
1518
+ buf_init(&b);
1519
+ buf_putc(&b, 'M'); // (M)icrotask checkpoint, returns nil
1520
+ return rendezvous(c, &b); // takes ownership of |b|
1406
1521
  }
1407
1522
 
1408
1523
  static VALUE context_pump_message_loop(VALUE self)
@@ -1425,12 +1540,14 @@ static VALUE context_low_memory_notification(VALUE self)
1425
1540
  buf_init(&req);
1426
1541
  buf_putc(&req, 'L'); // (L)ow memory notification, returns nothing
1427
1542
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1543
+ buf_reset(&res);
1428
1544
  return Qnil;
1429
1545
  }
1430
1546
 
1431
1547
  static int platform_set_flag1(VALUE k, VALUE v)
1432
1548
  {
1433
- char *p, *q, buf[256];
1549
+ char *p, *q, *r, buf[256];
1550
+ long pn, vn, len;
1434
1551
  int ok;
1435
1552
 
1436
1553
  k = rb_funcall(k, rb_intern("to_s"), 0);
@@ -1440,12 +1557,40 @@ static int platform_set_flag1(VALUE k, VALUE v)
1440
1557
  Check_Type(v, T_STRING);
1441
1558
  }
1442
1559
  p = RSTRING_PTR(k);
1443
- if (!strncmp(p, "--", 2))
1560
+ pn = RSTRING_LEN(k);
1561
+ if (memchr(p, '\0', pn))
1562
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1563
+ if (pn >= 2 && p[0] == '-' && p[1] == '-') {
1444
1564
  p += 2;
1565
+ pn -= 2;
1566
+ }
1445
1567
  if (NIL_P(v)) {
1446
- snprintf(buf, sizeof(buf), "--%s", p);
1568
+ len = 2 + pn;
1569
+ if (len >= (long)sizeof(buf))
1570
+ rb_raise(rb_eArgError, "flag too long");
1571
+ q = buf;
1572
+ *q++ = '-';
1573
+ *q++ = '-';
1574
+ memcpy(q, p, pn);
1575
+ q += pn;
1576
+ *q = '\0';
1447
1577
  } else {
1448
- snprintf(buf, sizeof(buf), "--%s=%s", p, RSTRING_PTR(v));
1578
+ q = RSTRING_PTR(v);
1579
+ vn = RSTRING_LEN(v);
1580
+ if (memchr(q, '\0', vn))
1581
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1582
+ len = 3 + pn + vn;
1583
+ if (len >= (long)sizeof(buf))
1584
+ rb_raise(rb_eArgError, "flag too long");
1585
+ r = buf;
1586
+ *r++ = '-';
1587
+ *r++ = '-';
1588
+ memcpy(r, p, pn);
1589
+ r += pn;
1590
+ *r++ = '=';
1591
+ memcpy(r, q, vn);
1592
+ r += vn;
1593
+ *r = '\0';
1449
1594
  }
1450
1595
  p = buf;
1451
1596
  pthread_mutex_lock(&flags_mtx);
@@ -1658,8 +1803,7 @@ static VALUE snapshot_initialize(int argc, VALUE *argv, VALUE self)
1658
1803
  a = rendezvous1(c, &s.b, &d);
1659
1804
  e = rb_ary_pop(a);
1660
1805
  context_dispose(cv);
1661
- if (*RSTRING_PTR(e))
1662
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1806
+ raise_exception_with_message(snapshot_error, e);
1663
1807
  ss->blob = rb_ary_pop(a);
1664
1808
  return Qnil;
1665
1809
  }
@@ -1689,8 +1833,7 @@ static VALUE snapshot_warmup(VALUE self, VALUE arg)
1689
1833
  a = rendezvous1(c, &s.b, &d);
1690
1834
  e = rb_ary_pop(a);
1691
1835
  context_dispose(cv);
1692
- if (*RSTRING_PTR(e))
1693
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1836
+ raise_exception_with_message(snapshot_error, e);
1694
1837
  ss->blob = rb_ary_pop(a);
1695
1838
  return self;
1696
1839
  }
@@ -1760,6 +1903,7 @@ void Init_mini_racer_extension(void)
1760
1903
  rb_define_method(c, "eval", context_eval, -1);
1761
1904
  rb_define_method(c, "heap_stats", context_heap_stats, 0);
1762
1905
  rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
1906
+ rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
1763
1907
  rb_define_method(c, "pump_message_loop", context_pump_message_loop, 0);
1764
1908
  rb_define_method(c, "low_memory_notification", context_low_memory_notification, 0);
1765
1909
  rb_define_alloc_func(c, context_alloc);
@@ -5,11 +5,12 @@
5
5
  #include <memory>
6
6
  #include <vector>
7
7
  #include <cassert>
8
+ #include <cstdarg>
8
9
  #include <cstdio>
9
10
  #include <cstdint>
10
11
  #include <cstdlib>
11
12
  #include <cstring>
12
- #include <vector>
13
+ #include <limits>
13
14
 
14
15
  // note: the filter function gets called inside the safe context,
15
16
  // i.e., the context that has not been tampered with by user JS
@@ -123,6 +124,106 @@ struct Serialized
123
124
  }
124
125
  };
125
126
 
127
+ void append_bytes(std::vector<char>& out, const char *p, size_t n)
128
+ {
129
+ out.insert(out.end(), p, p + n);
130
+ }
131
+
132
+ void append_literal(std::vector<char>& out, const char *s)
133
+ {
134
+ append_bytes(out, s, strlen(s));
135
+ }
136
+
137
+ bool append_utf8(std::vector<char>& out, const v8::String::Utf8Value& s)
138
+ {
139
+ if (!*s) return false;
140
+ append_bytes(out, *s, s.length());
141
+ return true;
142
+ }
143
+
144
+ void append_format(std::vector<char>& out, const char *fmt, ...)
145
+ {
146
+ char buf[128];
147
+ va_list ap;
148
+ va_start(ap, fmt);
149
+ int n = vsnprintf(buf, sizeof(buf), fmt, ap);
150
+ va_end(ap);
151
+ if (n <= 0) return;
152
+ if (static_cast<size_t>(n) < sizeof(buf)) {
153
+ append_bytes(out, buf, n);
154
+ return;
155
+ }
156
+
157
+ std::vector<char> tmp(static_cast<size_t>(n) + 1);
158
+ va_start(ap, fmt);
159
+ vsnprintf(tmp.data(), tmp.size(), fmt, ap);
160
+ va_end(ap);
161
+ append_bytes(out, tmp.data(), n);
162
+ }
163
+
164
+ v8::Local<v8::String> string_from_bytes(v8::Isolate *isolate, const std::vector<char>& bytes)
165
+ {
166
+ v8::Local<v8::String> s;
167
+ auto type = v8::NewStringType::kNormal;
168
+ if (bytes.size() <= static_cast<size_t>(std::numeric_limits<int>::max()) &&
169
+ v8::String::NewFromUtf8(isolate, bytes.data(), type,
170
+ static_cast<int>(bytes.size())).ToLocal(&s)) {
171
+ return s;
172
+ }
173
+
174
+ char fallback[] = {
175
+ bytes.empty() ? static_cast<char>(INTERNAL_ERROR) : bytes[0],
176
+ 'u', 'n', 'e', 'x', 'p', 'e', 'c', 't', 'e', 'd', ' ', 'e', 'r', 'r', 'o', 'r'
177
+ };
178
+ if (v8::String::NewFromUtf8(isolate, fallback, type, static_cast<int>(sizeof(fallback))).ToLocal(&s)) return s;
179
+ return v8::String::Empty(isolate);
180
+ }
181
+
182
+ void set_error_message(std::vector<char>& out, int cause, const char *message)
183
+ {
184
+ out.clear();
185
+ out.push_back(static_cast<char>(cause));
186
+ append_literal(out, message);
187
+ }
188
+
189
+ bool set_error_message(std::vector<char>& out, int cause, const v8::String::Utf8Value& message)
190
+ {
191
+ out.clear();
192
+ out.push_back(static_cast<char>(cause));
193
+ return append_utf8(out, message);
194
+ }
195
+
196
+ void set_fallback_error(State& st, v8::TryCatch *try_catch, int cause,
197
+ std::vector<char>& out)
198
+ {
199
+ v8::String::Utf8Value s(st.isolate, try_catch->Exception());
200
+ const char *message = *s ? nullptr : "unexpected failure";
201
+ if (cause == MEMORY_ERROR) message = "out of memory";
202
+ if (cause == TERMINATED_ERROR) message = "terminated";
203
+ if (message) {
204
+ set_error_message(out, cause, message);
205
+ } else if (!set_error_message(out, cause, s)) {
206
+ set_error_message(out, cause, "unexpected failure");
207
+ }
208
+ }
209
+
210
+ bool read_path_key(State& st, const char *&p, const char *pe,
211
+ v8::Local<v8::String> *key, bool *last)
212
+ {
213
+ const char *dot;
214
+ size_t n;
215
+
216
+ if (p > pe) return false;
217
+ dot = static_cast<const char*>(memchr(p, '.', pe - p));
218
+ *last = (dot == nullptr);
219
+ n = *last ? static_cast<size_t>(pe - p) : static_cast<size_t>(dot - p);
220
+ if (n > static_cast<size_t>(std::numeric_limits<int>::max())) return false;
221
+ auto type = v8::NewStringType::kNormal;
222
+ if (!v8::String::NewFromUtf8(st.isolate, p, type, static_cast<int>(n)).ToLocal(key)) return false;
223
+ p = *last ? pe : dot + 1;
224
+ return true;
225
+ }
226
+
126
227
  bool bubble_up_ruby_exception(State& st, v8::TryCatch *try_catch)
127
228
  {
128
229
  auto exception = try_catch->Exception();
@@ -249,35 +350,40 @@ v8::Local<v8::Value> sanitize(State& st, v8::Local<v8::Value> v)
249
350
 
250
351
  v8::Local<v8::String> to_error(State& st, v8::TryCatch *try_catch, int cause)
251
352
  {
252
- v8::Local<v8::Value> t;
253
- char buf[1024];
353
+ std::vector<char> buf;
254
354
 
255
- *buf = '\0';
256
355
  if (cause == NO_ERROR) {
257
- // nothing to do
356
+ return v8::String::Empty(st.isolate);
258
357
  } else if (cause == PARSE_ERROR) {
259
358
  auto message = try_catch->Message();
260
359
  v8::String::Utf8Value s(st.isolate, message->Get());
261
360
  v8::String::Utf8Value name(st.isolate, message->GetScriptResourceName());
262
- if (!*s || !*name) goto fallback;
263
- auto line = message->GetLineNumber(st.context).FromMaybe(0);
264
- auto column = message->GetStartColumn(st.context).FromMaybe(0);
265
- snprintf(buf, sizeof(buf), "%c%s at %s:%d:%d", cause, *s, *name, line, column);
266
- } else if (try_catch->StackTrace(st.context).ToLocal(&t)) {
267
- v8::String::Utf8Value s(st.isolate, t);
268
- if (!*s) goto fallback;
269
- snprintf(buf, sizeof(buf), "%c%s", cause, *s);
361
+ if (*s && *name) {
362
+ buf.push_back(static_cast<char>(cause));
363
+ append_utf8(buf, s);
364
+ append_literal(buf, " at ");
365
+ append_utf8(buf, name);
366
+ auto line = message->GetLineNumber(st.context).FromMaybe(0);
367
+ auto column = message->GetStartColumn(st.context).FromMaybe(0);
368
+ append_format(buf, ":%d:%d", line, column);
369
+ } else {
370
+ set_fallback_error(st, try_catch, cause, buf);
371
+ }
270
372
  } else {
271
- fallback:
272
- v8::String::Utf8Value s(st.isolate, try_catch->Exception());
273
- const char *message = *s ? *s : "unexpected failure";
274
- if (cause == MEMORY_ERROR) message = "out of memory";
275
- if (cause == TERMINATED_ERROR) message = "terminated";
276
- snprintf(buf, sizeof(buf), "%c%s", cause, message);
373
+ v8::Local<v8::Value> t;
374
+ if (try_catch->StackTrace(st.context).ToLocal(&t)) {
375
+ v8::String::Utf8Value s(st.isolate, t);
376
+ if (*s) {
377
+ buf.push_back(static_cast<char>(cause));
378
+ append_utf8(buf, s);
379
+ } else {
380
+ set_fallback_error(st, try_catch, cause, buf);
381
+ }
382
+ } else {
383
+ set_fallback_error(st, try_catch, cause, buf);
384
+ }
277
385
  }
278
- v8::Local<v8::String> s;
279
- if (v8::String::NewFromUtf8(st.isolate, buf).ToLocal(&s)) return s;
280
- return v8::String::Empty(st.isolate);
386
+ return string_from_bytes(st.isolate, buf);
281
387
  }
282
388
 
283
389
  extern "C" void v8_global_init(void)
@@ -445,23 +551,19 @@ extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n)
445
551
  v8::Local<v8::String> name;
446
552
  if (!name_v->ToString(st.context).ToLocal(&name)) goto fail;
447
553
  int32_t id;
448
- if (!id_v->Int32Value(st.context).To(&id)) goto fail;
449
- Callback *cb = new Callback{pst, id};
450
- st.callbacks.push_back(cb);
451
- v8::Local<v8::External> ext = v8::External::New(st.isolate, cb);
452
- v8::Local<v8::Function> function;
453
- if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function)) goto fail;
554
+ if (!id_v->IsInt32()) goto fail;
555
+ id = id_v.As<v8::Int32>()->Value();
454
556
  // support foo.bar.baz paths
455
557
  v8::String::Utf8Value path(st.isolate, name);
456
558
  if (!*path) goto fail;
559
+ const char *p = *path;
560
+ const char *pe = p + path.length();
457
561
  v8::Local<v8::Object> obj = st.context->Global();
458
562
  v8::Local<v8::String> key;
459
- for (const char *p = *path;;) {
460
- size_t n = strcspn(p, ".");
461
- auto type = v8::NewStringType::kNormal;
462
- if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail;
463
- if (p[n] == '\0') break;
464
- p += n + 1;
563
+ for (;;) {
564
+ bool last;
565
+ if (!read_path_key(st, p, pe, &key, &last)) goto fail;
566
+ if (last) break;
465
567
  v8::Local<v8::Value> val;
466
568
  if (!obj->Get(st.context, key).ToLocal(&val)) goto fail;
467
569
  if (!val->IsObject() && !val->IsFunction()) {
@@ -470,6 +572,11 @@ extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n)
470
572
  }
471
573
  obj = val.As<v8::Object>();
472
574
  }
575
+ std::unique_ptr<Callback> cb(new Callback{pst, id});
576
+ v8::Local<v8::External> ext = v8::External::New(st.isolate, cb.get());
577
+ v8::Local<v8::Function> function;
578
+ if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function)) goto fail;
579
+ st.callbacks.push_back(cb.release());
473
580
  if (!obj->Set(st.context, key, function).FromMaybe(false)) goto fail;
474
581
  }
475
582
  cause = NO_ERROR;
@@ -504,14 +611,14 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
504
611
  // support foo.bar.baz paths
505
612
  v8::String::Utf8Value path(st.isolate, name);
506
613
  if (!*path) goto fail;
614
+ const char *p = *path;
615
+ const char *pe = p + path.length();
507
616
  v8::Local<v8::Object> obj = st.context->Global();
508
617
  v8::Local<v8::String> key;
509
- for (const char *p = *path;;) {
510
- size_t n = strcspn(p, ".");
511
- auto type = v8::NewStringType::kNormal;
512
- if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail;
513
- if (p[n] == '\0') break;
514
- p += n + 1;
618
+ for (;;) {
619
+ bool last;
620
+ if (!read_path_key(st, p, pe, &key, &last)) goto fail;
621
+ if (last) break;
515
622
  v8::Local<v8::Value> val;
516
623
  if (!obj->Get(st.context, key).ToLocal(&val)) goto fail;
517
624
  if (!val->ToObject(st.context).ToLocal(&obj)) goto fail;
@@ -659,9 +766,22 @@ extern "C" void v8_heap_snapshot(State *pst)
659
766
  auto snapshot = st.isolate->GetHeapProfiler()->TakeHeapSnapshot();
660
767
  OutputStream os;
661
768
  snapshot->Serialize(&os, v8::HeapSnapshot::kJSON);
769
+ const_cast<v8::HeapSnapshot*>(snapshot)->Delete();
662
770
  v8_reply(st.ruby_context, os.buf.data(), os.buf.size()); // not serialized because big
663
771
  }
664
772
 
773
+ extern "C" void v8_perform_microtask_checkpoint(State *pst)
774
+ {
775
+ // Leave any termination active so the enclosing v8_call/v8_eval frame
776
+ // surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
777
+ State& st = *pst;
778
+ v8::TryCatch try_catch(st.isolate);
779
+ try_catch.SetVerbose(st.verbose_exceptions);
780
+ v8::HandleScope handle_scope(st.isolate);
781
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
782
+ reply_retry(st, v8::Undefined(st.isolate));
783
+ }
784
+
665
785
  extern "C" void v8_pump_message_loop(State *pst)
666
786
  {
667
787
  State& st = *pst;
@@ -691,7 +811,7 @@ fail:
691
811
  int snapshot(bool is_warmup, bool verbose_exceptions,
692
812
  const v8::String::Utf8Value& code,
693
813
  v8::StartupData blob, v8::StartupData *result,
694
- char (*errbuf)[512])
814
+ std::vector<char> *errbuf)
695
815
  {
696
816
  // SnapshotCreator takes ownership of isolate
697
817
  v8::Isolate *isolate = v8::Isolate::Allocate();
@@ -715,7 +835,8 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
715
835
  auto type = v8::NewStringType::kNormal;
716
836
  if (!v8::String::NewFromUtf8(isolate, *code, type, code.length()).ToLocal(&source)) {
717
837
  v8::String::Utf8Value s(isolate, try_catch.Exception());
718
- if (*s) snprintf(*errbuf, sizeof(*errbuf), "%c%s", cause, *s);
838
+ if (!set_error_message(*errbuf, cause, s))
839
+ set_error_message(*errbuf, cause, "unexpected failure");
719
840
  goto fail;
720
841
  }
721
842
  v8::ScriptOrigin origin(filename);
@@ -732,8 +853,16 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
732
853
  v8::String::Utf8Value name(isolate, m->GetScriptResourceName());
733
854
  auto line = m->GetLineNumber(context).FromMaybe(0);
734
855
  auto column = m->GetStartColumn(context).FromMaybe(0);
735
- snprintf(*errbuf, sizeof(*errbuf), "%c%s\n%s:%d:%d",
736
- cause, *s, *name, line, column);
856
+ if (*s && *name) {
857
+ errbuf->clear();
858
+ errbuf->push_back(static_cast<char>(cause));
859
+ append_utf8(*errbuf, s);
860
+ append_literal(*errbuf, "\n");
861
+ append_utf8(*errbuf, name);
862
+ append_format(*errbuf, ":%d:%d", line, column);
863
+ } else {
864
+ set_error_message(*errbuf, cause, "unexpected failure");
865
+ }
737
866
  goto fail;
738
867
  }
739
868
  cause = INTERNAL_ERROR;
@@ -764,7 +893,7 @@ extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n)
764
893
  v8::Local<v8::Value> result;
765
894
  v8::StartupData blob{nullptr, 0};
766
895
  int cause = INTERNAL_ERROR;
767
- char errbuf[512] = {0};
896
+ std::vector<char> errbuf;
768
897
  {
769
898
  v8::Local<v8::Value> code_v;
770
899
  if (!des.ReadValue(st.context).ToLocal(&code_v)) goto fail;
@@ -793,10 +922,8 @@ fail:
793
922
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
794
923
  if (cause) result = v8::Undefined(st.isolate);
795
924
  v8::Local<v8::Value> err;
796
- if (*errbuf) {
797
- if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
798
- err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
799
- }
925
+ if (!errbuf.empty()) {
926
+ err = string_from_bytes(st.isolate, errbuf);
800
927
  } else {
801
928
  err = to_error(st, &try_catch, cause);
802
929
  }
@@ -818,7 +945,7 @@ extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n)
818
945
  v8::Local<v8::Value> result;
819
946
  v8::StartupData blob{nullptr, 0};
820
947
  int cause = INTERNAL_ERROR;
821
- char errbuf[512] = {0};
948
+ std::vector<char> errbuf;
822
949
  {
823
950
  v8::Local<v8::Value> request_v;
824
951
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -865,10 +992,8 @@ fail:
865
992
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
866
993
  if (cause) result = v8::Undefined(st.isolate);
867
994
  v8::Local<v8::Value> err;
868
- if (*errbuf) {
869
- if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
870
- err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
871
- }
995
+ if (!errbuf.empty()) {
996
+ err = string_from_bytes(st.isolate, errbuf);
872
997
  } else {
873
998
  err = to_error(st, &try_catch, cause);
874
999
  }
@@ -919,6 +1044,7 @@ State::~State()
919
1044
  {
920
1045
  v8::Locker locker(isolate);
921
1046
  v8::Isolate::Scope isolate_scope(isolate);
1047
+ persistent_safe_context_function.Reset();
922
1048
  persistent_safe_context.Reset();
923
1049
  persistent_context.Reset();
924
1050
  ruby_exception.Reset();
@@ -42,6 +42,7 @@ void v8_call(struct State *pst, const uint8_t *p, size_t n);
42
42
  void v8_eval(struct State *pst, const uint8_t *p, size_t n);
43
43
  void v8_heap_stats(struct State *pst);
44
44
  void v8_heap_snapshot(struct State *pst);
45
+ void v8_perform_microtask_checkpoint(struct State *pst);
45
46
  void v8_pump_message_loop(struct State *pst);
46
47
  void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
47
48
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
@@ -278,7 +278,7 @@ static void ser_int(Ser *s, int64_t v)
278
278
  if (v > INT64_MIN/1024)
279
279
  if (v <= INT64_MAX/1024)
280
280
  return ser_num(s, v);
281
- t = v < 0 ? -v : v;
281
+ t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
282
282
  sign = v < 0 ? -1 : 1;
283
283
  ser_bigint(s, &t, sizeof(t), sign);
284
284
  } else {
@@ -77,9 +77,13 @@ module MiniRacer
77
77
  raise "TruffleRuby #{RUBY_ENGINE_VERSION} does not have support for inner contexts, use a more recent version"
78
78
  end
79
79
 
80
+
81
+ if TruffleRuby.native?
82
+ raise "You need the TruffleRuby JVM Standalone for mini_racer because it is not possible to install the js component in the the Native standalone"
83
+ end
84
+
80
85
  unless Polyglot.languages.include? "js"
81
- raise "The language 'js' is not available, you likely need to `export TRUFFLERUBYOPT='--jvm --polyglot'`\n" \
82
- "You also need to install the 'js' component, see https://github.com/oracle/truffleruby/blob/master/doc/user/polyglot.md#installing-other-languages"
86
+ raise "The language 'js' is not available, you need to install the 'js' component.\nSee https://github.com/oracle/truffleruby/blob/master/doc/user/polyglot.md#installing-other-languages"
83
87
  end
84
88
 
85
89
  @context = Polyglot::InnerContext.new(on_cancelled: -> {
@@ -102,20 +106,37 @@ module MiniRacer
102
106
  else
103
107
  @snapshot = nil
104
108
  end
105
- @is_object_or_array_func, @is_map_func, @is_map_iterator_func, @is_time_func, @js_date_to_time_func, @is_symbol_func, @js_symbol_to_symbol_func, @js_new_date_func, @js_new_array_func, @js_new_uint8array_func = eval_in_context <<-CODE
106
- [
107
- (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
108
- (x) => { return x instanceof Map },
109
- (x) => { return x[Symbol.toStringTag] === 'Map Iterator' },
110
- (x) => { return x instanceof Date },
111
- (x) => { return x.getTime(x) },
112
- (x) => { return typeof x === 'symbol' },
113
- (x) => { var r = x.description; return r === undefined ? 'undefined' : r },
114
- (x) => { return new Date(x) },
115
- (x) => { return new Array(x) },
116
- (x) => { return new Uint8Array(x) },
117
- ]
118
- CODE
109
+
110
+ @is_object_or_array_func = eval_in_context "(x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) }"
111
+ @is_map_func = eval_in_context "(x) => { return x instanceof Map }"
112
+ @is_map_iterator_func = eval_in_context "(x) => { return x[Symbol.toStringTag] === 'Map Iterator' }"
113
+ @is_time_func = eval_in_context "(x) => { return x instanceof Date }"
114
+ @is_symbol_func = eval_in_context "(x) => { return typeof x === 'symbol' }"
115
+ @is_uint8_array_func = eval_in_context "(x) => { return x instanceof Uint8Array }"
116
+
117
+ @js_date_to_time_func = eval_in_context "(x) => { return x.getTime(x) }"
118
+ @js_symbol_to_symbol_func = eval_in_context "(x) => { var r = x.description; return r === undefined ? 'undefined' : r }"
119
+ @js_new_date_func = eval_in_context "(x) => { return new Date(x) }"
120
+ @js_new_array_func = eval_in_context "(x) => { return new Array(x) }"
121
+ # looks up a (dotted) function name as properties from globalThis,
122
+ # instead of evaluating the name as source code, so that names with
123
+ # embedded NUL bytes or other invalid syntax resolve correctly
124
+ @js_lookup_call_target_func = eval_in_context <<~JS
125
+ (name) => {
126
+ let target = globalThis;
127
+ for (const key of name.split(".")) {
128
+ if (target == null) {
129
+ throw new ReferenceError(name + " is not defined");
130
+ }
131
+ target = target[key];
132
+ }
133
+ if (target === undefined) {
134
+ throw new ReferenceError(name + " is not defined");
135
+ }
136
+ return target;
137
+ }
138
+ JS
139
+ @js_new_uint8array_func = eval_in_context "(x) => { return new Uint8Array(x) }"
119
140
  end
120
141
 
121
142
  def dispose_unsafe
@@ -171,7 +192,7 @@ module MiniRacer
171
192
  raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
172
193
  begin
173
194
  translate do
174
- function = eval_in_context(function_name)
195
+ function = @js_lookup_call_target_func.call(convert_ruby_to_js(encode(function_name)))
175
196
  function.call(*convert_ruby_to_js(arguments))
176
197
  end
177
198
  rescue Polyglot::ForeignException => e
@@ -222,8 +243,11 @@ module MiniRacer
222
243
 
223
244
  def convert_js_to_ruby(value)
224
245
  case value
225
- when true, false, Integer, Float
246
+ when true, false, Integer
226
247
  value
248
+ when Float
249
+ # match the C extension: integral doubles convert to Ruby Integers
250
+ value.finite? && value == value.truncate ? value.to_i : value
227
251
  else
228
252
  if value.nil?
229
253
  nil
@@ -232,6 +256,10 @@ module MiniRacer
232
256
  elsif value.respond_to?(:to_str)
233
257
  value.to_str.dup
234
258
  elsif value.respond_to?(:to_ary)
259
+ if uint8_array?(value)
260
+ return value.to_a.pack('C*')
261
+ end
262
+
235
263
  value.to_ary.map do |e|
236
264
  if e.respond_to?(:call)
237
265
  nil
@@ -281,15 +309,19 @@ module MiniRacer
281
309
  @is_time_func.call(value)
282
310
  end
283
311
 
312
+ def symbol?(value)
313
+ @is_symbol_func.call(value)
314
+ end
315
+
316
+ def uint8_array?(value)
317
+ @is_uint8_array_func.call(value)
318
+ end
319
+
284
320
  def js_date_to_time(value)
285
321
  millis = @js_date_to_time_func.call(value)
286
322
  Time.at(Rational(millis, 1000))
287
323
  end
288
324
 
289
- def symbol?(value)
290
- @is_symbol_func.call(value)
291
- end
292
-
293
325
  def js_symbol_to_symbol(value)
294
326
  @js_symbol_to_symbol_func.call(value).to_s.to_sym
295
327
  end
@@ -359,6 +391,10 @@ module MiniRacer
359
391
  class Platform
360
392
  def self.set_flag_as_str!(flag)
361
393
  raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
394
+ raise ArgumentError, "flag contains NUL byte" if flag.include?("\0")
395
+ # the C extension normalizes flags into a 256 byte "--flag" buffer
396
+ normalized = flag.start_with?("--") ? flag : "--#{flag}"
397
+ raise ArgumentError, "flag too long" if normalized.bytesize >= 256
362
398
  raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
363
399
  Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
364
400
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.21.0"
4
+ VERSION = "0.21.2"
5
5
  LIBV8_NODE_VERSION = "~> 24.12.0.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 0.21.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -136,9 +136,9 @@ licenses:
136
136
  - MIT
137
137
  metadata:
138
138
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
139
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.0/CHANGELOG
140
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.0
141
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.0
139
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.2/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.2
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.2
142
142
  rdoc_options: []
143
143
  require_paths:
144
144
  - lib
@@ -147,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
- version: '3.1'
150
+ version: '3.3'
151
151
  required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  requirements:
153
153
  - - ">="