mini_racer 0.20.0 → 0.21.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: 5aa16bf587a6e12df562d7a946ce3a965a8444debd9769b376075cafd10a3b5c
4
- data.tar.gz: 5aaf6a08ae90f05bc554bae0a06e93efce5318d00aacdfe959f36513533caac4
3
+ metadata.gz: 6c838b1bc35ed1adb0156f0e01d699afc180c174718f197a6afc1356fe9e4815
4
+ data.tar.gz: 7a4b00c4bcd4572bf7fdcce3df6455ded628706975fa8540946faee17e53617d
5
5
  SHA512:
6
- metadata.gz: 2b2641ceb0fd1770efd32cfb9e37aee92ca857ad6acbe2129157a58be9ed6bbef4a355c5af133f5749f229d578e10a452ef77a62eadbe436993f005146d242cd
7
- data.tar.gz: 3264e95d4951aa52ba080a94b847c5080705e1de0e3dd5fefae0786342ccece850b00db4f151220e65068e9e5d62fb0eebae0fda65e79647196903fae70d8012
6
+ metadata.gz: d826ca742dbe6580f92faebf11c827795536c55596c279fa070dd38836d03b7dcecc0b43fe254b6d95590bea1a24d8ec168f9d5008b668ba488a5aff7b6e3aea
7
+ data.tar.gz: 985effeb709d8dfc1481c679dfdc86a3930a0d84de6d3b6d9d154d6801a6766af76ffecd468e22d8b20b68952e02b756ab0ec11b1bc3c93736b078d046719798
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ - 0.21.1 - 25-05-2026
2
+ - Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
3
+ - Stop and join the reusable `:single_threaded` runner when contexts are disposed
4
+ - Document `:single_threaded` fork-safety requirements for pre-fork contexts
5
+
6
+ - 0.21.0 - 16-04-2026
7
+ - Add MiniRacer::Binary for returning Uint8Array to JavaScript from attached Ruby callbacks
8
+
1
9
  - 0.20.0 - 24-02-2026
2
10
  - Add Snapshot.load to restore snapshots from binary data, enabling disk persistence
3
11
 
data/README.md CHANGED
@@ -60,6 +60,25 @@ puts context.eval("array_and_hash()")
60
60
  # => {"a" => 1, "b" => [1, {"a" => 1}]}
61
61
  ```
62
62
 
63
+ ### Return binary data from Ruby to JavaScript
64
+
65
+ Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
66
+
67
+ ```ruby
68
+ require "digest"
69
+
70
+ context = MiniRacer::Context.new
71
+ context.attach("sha256_raw", ->(data) {
72
+ MiniRacer::Binary.new(Digest::SHA256.digest(data))
73
+ })
74
+
75
+ # Inside JavaScript the return value is a Uint8Array
76
+ context.eval("sha256_raw('hello') instanceof Uint8Array") # => true
77
+ context.eval("sha256_raw('hello').length") # => 32
78
+ ```
79
+
80
+ This is useful when you need to pass raw bytes (e.g., cryptographic digests, compressed data, binary file contents) from Ruby to JavaScript. The `MiniRacer::Binary` wrapper tells the bridge to serialize the data as a `Uint8Array` on the JavaScript side rather than a string.
81
+
63
82
  ### GIL free JavaScript execution
64
83
 
65
84
  The Ruby Global interpreter lock is released when scripts are executing:
@@ -120,6 +139,15 @@ Since 0.6.1 mini_racer does support V8 single threaded platform mode which shoul
120
139
  MiniRacer::Platform.set_flags!(:single_threaded)
121
140
  ```
122
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
+
123
151
  If you want to ensure your application does not leak memory after fork either:
124
152
 
125
153
  1. Ensure no `MiniRacer::Context` objects are created in the master process; or
@@ -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)
@@ -193,6 +197,7 @@ static VALUE terminated_error;
193
197
  static VALUE context_class;
194
198
  static VALUE snapshot_class;
195
199
  static VALUE date_time_class;
200
+ static VALUE binary_class;
196
201
  static VALUE js_function_class;
197
202
 
198
203
  static pthread_mutex_t flags_mtx = PTHREAD_MUTEX_INITIALIZER;
@@ -688,10 +693,17 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
688
693
  // entirely new objects
689
694
  if (rb_respond_to(v, rb_intern("to_time"))) {
690
695
  v = rb_funcall(v, rb_intern("to_time"), 0);
691
- }
692
- if (rb_obj_is_kind_of(v, rb_cTime)) {
693
- struct timeval tv = rb_time_timeval(v);
694
- ser_date(s, tv.tv_sec*1e3 + tv.tv_usec/1e3);
696
+ if (rb_obj_is_kind_of(v, rb_cTime)) {
697
+ struct timeval tv = rb_time_timeval(v);
698
+ ser_date(s, tv.tv_sec*1e3 + tv.tv_usec/1e3);
699
+ } else {
700
+ snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v)));
701
+ return -1;
702
+ }
703
+ } else if (!NIL_P(binary_class) && rb_obj_is_kind_of(v, binary_class)) {
704
+ t = rb_ivar_get(v, rb_intern("@data"));
705
+ Check_Type(t, T_STRING);
706
+ ser_uint8array(s, RSTRING_PTR(t), RSTRING_LEN(t));
695
707
  } else {
696
708
  snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v)));
697
709
  return -1;
@@ -860,18 +872,10 @@ void v8_dispatch(Context *c)
860
872
  // only called when inside v8_call, v8_eval, or v8_pump_message_loop
861
873
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
862
874
  {
863
- struct rendezvous_nogvl *args;
864
-
865
875
  buf_reset(&c->req);
866
- if (single_threaded) {
867
- assert(*c->res.buf == 'c'); // js -> ruby callback
868
- args = &(struct rendezvous_nogvl){c, &c->req, &c->res};
869
- rb_thread_call_with_gvl(rendezvous_callback, args);
870
- } else {
871
- pthread_cond_signal(&c->cv);
872
- while (!c->req.len)
873
- pthread_cond_wait(&c->cv, &c->mtx);
874
- }
876
+ pthread_cond_signal(&c->cv);
877
+ while (!c->req.len)
878
+ pthread_cond_wait(&c->cv, &c->mtx);
875
879
  buf_reset(&c->res);
876
880
  *p = c->req.buf;
877
881
  *n = c->req.len;
@@ -983,10 +987,45 @@ fail:
983
987
  goto out;
984
988
  }
985
989
 
990
+ static void *single_threaded_runner(void *arg)
991
+ {
992
+ Context *c;
993
+
994
+ c = arg;
995
+ pthread_mutex_lock(&c->mtx);
996
+ for (;;) {
997
+ while (!c->req.len && atomic_load(&c->quit) < 1)
998
+ pthread_cond_wait(&c->cv, &c->mtx);
999
+ if (atomic_load(&c->quit) >= 1)
1000
+ break;
1001
+ v8_single_threaded_enter(c->pst, c, dispatch);
1002
+ pthread_cond_signal(&c->cv);
1003
+ }
1004
+ pthread_mutex_unlock(&c->mtx);
1005
+ return NULL;
1006
+ }
1007
+
1008
+ static int single_threaded_runner_start(Context *c)
1009
+ {
1010
+ pid_t pid;
1011
+ int r;
1012
+
1013
+ pid = getpid();
1014
+ if (c->single_threaded_thr_started && c->single_threaded_pid == pid)
1015
+ return 0;
1016
+ c->single_threaded_thr_started = 0;
1017
+ c->single_threaded_pid = pid;
1018
+ r = pthread_create(&c->single_threaded_thr, NULL, single_threaded_runner, c);
1019
+ if (!r)
1020
+ c->single_threaded_thr_started = 1;
1021
+ return r;
1022
+ }
1023
+
986
1024
  static inline void *rendezvous_nogvl(void *arg)
987
1025
  {
988
1026
  struct rendezvous_nogvl *a;
989
1027
  Context *c;
1028
+ int r;
990
1029
 
991
1030
  a = arg;
992
1031
  c = a->context;
@@ -1002,7 +1041,16 @@ next:
1002
1041
  assert(c->res.len == 0);
1003
1042
  buf_move(a->req, &c->req); // v8 thread takes ownership of req
1004
1043
  if (single_threaded) {
1005
- v8_single_threaded_enter(c->pst, c, dispatch);
1044
+ r = single_threaded_runner_start(c);
1045
+ if (r) {
1046
+ buf_move(&c->req, a->req);
1047
+ pthread_mutex_unlock(&c->mtx);
1048
+ c->depth--;
1049
+ pthread_mutex_unlock(&c->rr_mtx);
1050
+ return (void *)(intptr_t)r;
1051
+ }
1052
+ pthread_cond_signal(&c->cv);
1053
+ do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1006
1054
  } else {
1007
1055
  pthread_cond_signal(&c->cv);
1008
1056
  do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
@@ -1011,6 +1059,7 @@ next:
1011
1059
  pthread_mutex_unlock(&c->mtx);
1012
1060
  if (*a->res->buf == 'c') { // js -> ruby callback?
1013
1061
  rb_thread_call_with_gvl(rendezvous_callback, a);
1062
+ buf_reset(a->res);
1014
1063
  goto next;
1015
1064
  }
1016
1065
  c->depth--;
@@ -1020,12 +1069,16 @@ next:
1020
1069
 
1021
1070
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1022
1071
  {
1072
+ void *r;
1073
+
1023
1074
  if (atomic_load(&c->quit)) {
1024
1075
  buf_reset(req);
1025
1076
  rb_raise(context_disposed_error, "disposed context");
1026
1077
  }
1027
- rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1028
- NULL, NULL, 0);
1078
+ r = rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1079
+ NULL, NULL, 0);
1080
+ if (r)
1081
+ rb_raise(runtime_error, "pthread_create: %s", strerror((int)(intptr_t)r));
1029
1082
  }
1030
1083
 
1031
1084
  // send request to & receive reply from v8 thread; takes ownership of |req|
@@ -1111,6 +1164,11 @@ static VALUE context_alloc(VALUE klass)
1111
1164
  if (Qtrue == rb_funcall(rb_cObject, f, 1, a))
1112
1165
  date_time_class = rb_const_get(rb_cObject, rb_intern("DateTime"));
1113
1166
  }
1167
+ if (NIL_P(binary_class)) {
1168
+ VALUE m = rb_const_get(rb_cObject, rb_intern("MiniRacer"));
1169
+ if (Qtrue == rb_funcall(m, rb_intern("const_defined?"), 1, rb_str_new_cstr("Binary")))
1170
+ binary_class = rb_const_get(m, rb_intern("Binary"));
1171
+ }
1114
1172
  c = ruby_xmalloc(sizeof(*c));
1115
1173
  memset(c, 0, sizeof(*c));
1116
1174
  c->exception = Qnil;
@@ -1177,7 +1235,16 @@ static void *context_free_thread_do(void *arg)
1177
1235
  Context *c;
1178
1236
 
1179
1237
  c = arg;
1180
- v8_single_threaded_dispose(c->pst);
1238
+ if (single_threaded && c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1239
+ pthread_mutex_lock(&c->mtx);
1240
+ atomic_store(&c->quit, 2);
1241
+ pthread_cond_signal(&c->cv);
1242
+ pthread_mutex_unlock(&c->mtx);
1243
+ pthread_join(c->single_threaded_thr, NULL);
1244
+ }
1245
+ if (c->pst)
1246
+ v8_single_threaded_dispose(c->pst);
1247
+ pthread_mutex_lock(&c->mtx);
1181
1248
  context_destroy(c);
1182
1249
  return NULL;
1183
1250
  }
@@ -1271,8 +1338,18 @@ static void *context_dispose_do(void *arg)
1271
1338
 
1272
1339
  c = arg;
1273
1340
  if (single_threaded) {
1341
+ pthread_mutex_lock(&c->mtx);
1342
+ while (c->req.len || c->res.len)
1343
+ pthread_cond_wait(&c->cv, &c->mtx);
1274
1344
  atomic_store(&c->quit, 1); // disposed
1275
- // intentionally a no-op for now
1345
+ if (c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1346
+ pthread_cond_signal(&c->cv);
1347
+ pthread_mutex_unlock(&c->mtx);
1348
+ pthread_join(c->single_threaded_thr, NULL);
1349
+ pthread_mutex_lock(&c->mtx);
1350
+ c->single_threaded_thr_started = 0;
1351
+ }
1352
+ pthread_mutex_unlock(&c->mtx);
1276
1353
  } else {
1277
1354
  pthread_mutex_lock(&c->mtx);
1278
1355
  while (c->req.len || c->res.len)
@@ -1763,5 +1840,6 @@ void Init_mini_racer_extension(void)
1763
1840
  rb_define_singleton_method(c, "set_flags!", platform_set_flags, -1);
1764
1841
 
1765
1842
  date_time_class = Qnil; // lazy init
1843
+ binary_class = Qnil; // lazy init
1766
1844
  js_function_class = rb_define_class_under(m, "JavaScriptFunction", rb_cObject);
1767
1845
  }
@@ -322,6 +322,19 @@ static void ser_string16(Ser *s, const void *p, size_t n)
322
322
  w(s, p, n);
323
323
  }
324
324
 
325
+ // Uint8Array: ArrayBuffer header + data + typed array view descriptor
326
+ static void ser_uint8array(Ser *s, const void *p, size_t n)
327
+ {
328
+ w_byte(s, 'B'); // ArrayBuffer tag
329
+ w_varint(s, n); // byte length
330
+ w(s, p, n); // raw bytes
331
+ w_byte(s, 'V'); // typed array view tag
332
+ w_byte(s, 'B'); // Uint8Array type
333
+ w_varint(s, 0); // byteOffset
334
+ w_varint(s, n); // byteLength
335
+ w_varint(s, 0); // flags
336
+ }
337
+
325
338
  static void ser_object_begin(Ser *s)
326
339
  {
327
340
  w_byte(s, 'o');
@@ -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,19 +106,19 @@ 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 = 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
- ]
117
- 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
+ @js_new_uint8array_func = eval_in_context "(x) => { return new Uint8Array(x) }"
118
122
  end
119
123
 
120
124
  def dispose_unsafe
@@ -231,6 +235,10 @@ module MiniRacer
231
235
  elsif value.respond_to?(:to_str)
232
236
  value.to_str.dup
233
237
  elsif value.respond_to?(:to_ary)
238
+ if uint8_array?(value)
239
+ return value.to_a.pack('C*')
240
+ end
241
+
234
242
  value.to_ary.map do |e|
235
243
  if e.respond_to?(:call)
236
244
  nil
@@ -280,15 +288,19 @@ module MiniRacer
280
288
  @is_time_func.call(value)
281
289
  end
282
290
 
291
+ def symbol?(value)
292
+ @is_symbol_func.call(value)
293
+ end
294
+
295
+ def uint8_array?(value)
296
+ @is_uint8_array_func.call(value)
297
+ end
298
+
283
299
  def js_date_to_time(value)
284
300
  millis = @js_date_to_time_func.call(value)
285
301
  Time.at(Rational(millis, 1000))
286
302
  end
287
303
 
288
- def symbol?(value)
289
- @is_symbol_func.call(value)
290
- end
291
-
292
304
  def js_symbol_to_symbol(value)
293
305
  @js_symbol_to_symbol_func.call(value).to_s.to_sym
294
306
  end
@@ -329,6 +341,8 @@ module MiniRacer
329
341
  js_new_date(value.to_f * 1000)
330
342
  when DateTime
331
343
  js_new_date(value.to_time.to_f * 1000)
344
+ when MiniRacer::Binary
345
+ @js_new_uint8array_func.call(value.data.bytes)
332
346
  else
333
347
  "Undefined Conversion"
334
348
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.20.0"
4
+ VERSION = "0.21.1"
5
5
  LIBV8_NODE_VERSION = "~> 24.12.0.1"
6
6
  end
data/lib/mini_racer.rb CHANGED
@@ -1,6 +1,17 @@
1
1
  require "mini_racer/version"
2
2
  require "pathname"
3
3
 
4
+ module MiniRacer
5
+ class Binary
6
+ attr_reader :data
7
+
8
+ def initialize(data)
9
+ raise TypeError, "wrong argument type #{data.class} (expected String)" unless data.is_a?(String)
10
+ @data = data
11
+ end
12
+ end
13
+ end
14
+
4
15
  if RUBY_ENGINE == "truffleruby"
5
16
  require "mini_racer/truffleruby"
6
17
  else
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-02-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -113,8 +112,8 @@ email:
113
112
  - sam.saffron@gmail.com
114
113
  executables: []
115
114
  extensions:
116
- - ext/mini_racer_loader/extconf.rb
117
115
  - ext/mini_racer_extension/extconf.rb
116
+ - ext/mini_racer_loader/extconf.rb
118
117
  extra_rdoc_files: []
119
118
  files:
120
119
  - CHANGELOG
@@ -137,10 +136,9 @@ licenses:
137
136
  - MIT
138
137
  metadata:
139
138
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
140
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.20.0/CHANGELOG
141
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.20.0
142
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.20.0
143
- post_install_message:
139
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.1/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.1
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.1
144
142
  rdoc_options: []
145
143
  require_paths:
146
144
  - lib
@@ -156,8 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
154
  - !ruby/object:Gem::Version
157
155
  version: '0'
158
156
  requirements: []
159
- rubygems_version: 3.5.22
160
- signing_key:
157
+ rubygems_version: 4.0.6
161
158
  specification_version: 4
162
159
  summary: Minimal embedded v8 for Ruby
163
160
  test_files: []