Skip to content

Commit 40d83a3

Browse files
authored
FEATURE: add Ruby-to-JS Uint8Array support (#406)
* FEATURE: add Ruby-to-JS Uint8Array support Introduce MiniRacer::Binary for attached Ruby callbacks that need to return raw bytes to JavaScript. The C extension now serializes this wrapper as a Uint8Array, and TruffleRuby is updated to mirror the same conversion path. Also add documentation, tests, and bump the version to 0.20.1. * version bump
1 parent 89e6eec commit 40d83a3

8 files changed

Lines changed: 82 additions & 6 deletions

File tree

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
- 0.21.0 - 16-04-2026
2+
- Add MiniRacer::Binary for returning Uint8Array to JavaScript from attached Ruby callbacks
3+
14
- 0.20.0 - 24-02-2026
25
- Add Snapshot.load to restore snapshots from binary data, enabling disk persistence
36

README.md

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

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+
6382
### GIL free JavaScript execution
6483

6584
The Ruby Global interpreter lock is released when scripts are executing:

ext/mini_racer_extension/mini_racer_extension.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ static VALUE terminated_error;
193193
static VALUE context_class;
194194
static VALUE snapshot_class;
195195
static VALUE date_time_class;
196+
static VALUE binary_class;
196197
static VALUE js_function_class;
197198

198199
static pthread_mutex_t flags_mtx = PTHREAD_MUTEX_INITIALIZER;
@@ -688,10 +689,17 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
688689
// entirely new objects
689690
if (rb_respond_to(v, rb_intern("to_time"))) {
690691
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);
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);
695+
} else {
696+
snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v)));
697+
return -1;
698+
}
699+
} else if (!NIL_P(binary_class) && rb_obj_is_kind_of(v, binary_class)) {
700+
t = rb_ivar_get(v, rb_intern("@data"));
701+
Check_Type(t, T_STRING);
702+
ser_uint8array(s, RSTRING_PTR(t), RSTRING_LEN(t));
695703
} else {
696704
snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v)));
697705
return -1;
@@ -1111,6 +1119,11 @@ static VALUE context_alloc(VALUE klass)
11111119
if (Qtrue == rb_funcall(rb_cObject, f, 1, a))
11121120
date_time_class = rb_const_get(rb_cObject, rb_intern("DateTime"));
11131121
}
1122+
if (NIL_P(binary_class)) {
1123+
VALUE m = rb_const_get(rb_cObject, rb_intern("MiniRacer"));
1124+
if (Qtrue == rb_funcall(m, rb_intern("const_defined?"), 1, rb_str_new_cstr("Binary")))
1125+
binary_class = rb_const_get(m, rb_intern("Binary"));
1126+
}
11141127
c = ruby_xmalloc(sizeof(*c));
11151128
memset(c, 0, sizeof(*c));
11161129
c->exception = Qnil;
@@ -1763,5 +1776,6 @@ void Init_mini_racer_extension(void)
17631776
rb_define_singleton_method(c, "set_flags!", platform_set_flags, -1);
17641777

17651778
date_time_class = Qnil; // lazy init
1779+
binary_class = Qnil; // lazy init
17661780
js_function_class = rb_define_class_under(m, "JavaScriptFunction", rb_cObject);
17671781
}

ext/mini_racer_extension/serde.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,19 @@ static void ser_string16(Ser *s, const void *p, size_t n)
322322
w(s, p, n);
323323
}
324324

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+
325338
static void ser_object_begin(Ser *s)
326339
{
327340
w_byte(s, 'o');

lib/mini_racer.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
require "mini_racer/version"
22
require "pathname"
33

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+
415
if RUBY_ENGINE == "truffleruby"
516
require "mini_racer/truffleruby"
617
else

lib/mini_racer/truffleruby.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def init_unsafe(isolate, snapshot)
102102
else
103103
@snapshot = nil
104104
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
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
106106
[
107107
(x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) },
108108
(x) => { return x instanceof Map },
@@ -113,6 +113,7 @@ def init_unsafe(isolate, snapshot)
113113
(x) => { var r = x.description; return r === undefined ? 'undefined' : r },
114114
(x) => { return new Date(x) },
115115
(x) => { return new Array(x) },
116+
(x) => { return new Uint8Array(x) },
116117
]
117118
CODE
118119
end
@@ -329,6 +330,8 @@ def convert_ruby_to_js(value)
329330
js_new_date(value.to_f * 1000)
330331
when DateTime
331332
js_new_date(value.to_time.to_f * 1000)
333+
when MiniRacer::Binary
334+
@js_new_uint8array_func.call(value.data.bytes)
332335
else
333336
"Undefined Conversion"
334337
end

lib/mini_racer/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

33
module MiniRacer
4-
VERSION = "0.20.0"
4+
VERSION = "0.21.0"
55
LIBV8_NODE_VERSION = "~> 24.12.0.1"
66
end

test/mini_racer_test.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,19 @@ def test_large_integer
12241224
end
12251225
end
12261226

1227+
def test_binary_returns_uint8array
1228+
context = MiniRacer::Context.new
1229+
context.attach("add_one", ->(data) {
1230+
MiniRacer::Binary.new(data.bytes.map { _1 + 1 }.pack("C*"))
1231+
})
1232+
1233+
result = context.eval <<~JS
1234+
var output = add_one(new Uint8Array([0, 1, 2, 3]));
1235+
(output instanceof Uint8Array) && Array.from(output).join(",") === "1,2,3,4";
1236+
JS
1237+
assert_equal true, result
1238+
end
1239+
12271240
def test_exception_message_encoding
12281241
e = nil
12291242
begin

0 commit comments

Comments
 (0)