From bf9de819961552c326adb9edafb9a38a54671a94 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 16 Apr 2026 17:41:30 +1000 Subject: [PATCH 1/2] 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. --- CHANGELOG | 3 +++ README.md | 19 ++++++++++++++++ .../mini_racer_extension.c | 22 +++++++++++++++---- ext/mini_racer_extension/serde.c | 13 +++++++++++ lib/mini_racer.rb | 11 ++++++++++ lib/mini_racer/truffleruby.rb | 5 ++++- lib/mini_racer/version.rb | 2 +- test/mini_racer_test.rb | 13 +++++++++++ 8 files changed, 82 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 13e576e..b503fd2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +- 0.20.1 - 16-04-2026 + - Add MiniRacer::Binary for returning Uint8Array to JavaScript from attached Ruby callbacks + - 0.20.0 - 24-02-2026 - Add Snapshot.load to restore snapshots from binary data, enabling disk persistence diff --git a/README.md b/README.md index 4e8e3b7..5db0981 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,25 @@ puts context.eval("array_and_hash()") # => {"a" => 1, "b" => [1, {"a" => 1}]} ``` +### Return binary data from Ruby to JavaScript + +Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`: + +```ruby +require "digest" + +context = MiniRacer::Context.new +context.attach("sha256_raw", ->(data) { + MiniRacer::Binary.new(Digest::SHA256.digest(data)) +}) + +# Inside JavaScript the return value is a Uint8Array +context.eval("sha256_raw('hello') instanceof Uint8Array") # => true +context.eval("sha256_raw('hello').length") # => 32 +``` + +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. + ### GIL free JavaScript execution The Ruby Global interpreter lock is released when scripts are executing: diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c index 7cd1fa1..e888b1b 100644 --- a/ext/mini_racer_extension/mini_racer_extension.c +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -193,6 +193,7 @@ static VALUE terminated_error; static VALUE context_class; static VALUE snapshot_class; static VALUE date_time_class; +static VALUE binary_class; static VALUE js_function_class; static pthread_mutex_t flags_mtx = PTHREAD_MUTEX_INITIALIZER; @@ -688,10 +689,17 @@ static int serialize1(Ser *s, VALUE refs, VALUE v) // entirely new objects if (rb_respond_to(v, rb_intern("to_time"))) { v = rb_funcall(v, rb_intern("to_time"), 0); - } - if (rb_obj_is_kind_of(v, rb_cTime)) { - struct timeval tv = rb_time_timeval(v); - ser_date(s, tv.tv_sec*1e3 + tv.tv_usec/1e3); + if (rb_obj_is_kind_of(v, rb_cTime)) { + struct timeval tv = rb_time_timeval(v); + ser_date(s, tv.tv_sec*1e3 + tv.tv_usec/1e3); + } else { + snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v))); + return -1; + } + } else if (!NIL_P(binary_class) && rb_obj_is_kind_of(v, binary_class)) { + t = rb_ivar_get(v, rb_intern("@data")); + Check_Type(t, T_STRING); + ser_uint8array(s, RSTRING_PTR(t), RSTRING_LEN(t)); } else { snprintf(s->err, sizeof(s->err), "unsupported type %s", rb_class2name(CLASS_OF(v))); return -1; @@ -1111,6 +1119,11 @@ static VALUE context_alloc(VALUE klass) if (Qtrue == rb_funcall(rb_cObject, f, 1, a)) date_time_class = rb_const_get(rb_cObject, rb_intern("DateTime")); } + if (NIL_P(binary_class)) { + VALUE m = rb_const_get(rb_cObject, rb_intern("MiniRacer")); + if (Qtrue == rb_funcall(m, rb_intern("const_defined?"), 1, rb_str_new_cstr("Binary"))) + binary_class = rb_const_get(m, rb_intern("Binary")); + } c = ruby_xmalloc(sizeof(*c)); memset(c, 0, sizeof(*c)); c->exception = Qnil; @@ -1763,5 +1776,6 @@ void Init_mini_racer_extension(void) rb_define_singleton_method(c, "set_flags!", platform_set_flags, -1); date_time_class = Qnil; // lazy init + binary_class = Qnil; // lazy init js_function_class = rb_define_class_under(m, "JavaScriptFunction", rb_cObject); } diff --git a/ext/mini_racer_extension/serde.c b/ext/mini_racer_extension/serde.c index 7d5d51f..de35473 100644 --- a/ext/mini_racer_extension/serde.c +++ b/ext/mini_racer_extension/serde.c @@ -322,6 +322,19 @@ static void ser_string16(Ser *s, const void *p, size_t n) w(s, p, n); } +// Uint8Array: ArrayBuffer header + data + typed array view descriptor +static void ser_uint8array(Ser *s, const void *p, size_t n) +{ + w_byte(s, 'B'); // ArrayBuffer tag + w_varint(s, n); // byte length + w(s, p, n); // raw bytes + w_byte(s, 'V'); // typed array view tag + w_byte(s, 'B'); // Uint8Array type + w_varint(s, 0); // byteOffset + w_varint(s, n); // byteLength + w_varint(s, 0); // flags +} + static void ser_object_begin(Ser *s) { w_byte(s, 'o'); diff --git a/lib/mini_racer.rb b/lib/mini_racer.rb index be670e5..3eb90b3 100644 --- a/lib/mini_racer.rb +++ b/lib/mini_racer.rb @@ -1,6 +1,17 @@ require "mini_racer/version" require "pathname" +module MiniRacer + class Binary + attr_reader :data + + def initialize(data) + raise TypeError, "wrong argument type #{data.class} (expected String)" unless data.is_a?(String) + @data = data + end + end +end + if RUBY_ENGINE == "truffleruby" require "mini_racer/truffleruby" else diff --git a/lib/mini_racer/truffleruby.rb b/lib/mini_racer/truffleruby.rb index bae8ba0..802acfd 100644 --- a/lib/mini_racer/truffleruby.rb +++ b/lib/mini_racer/truffleruby.rb @@ -102,7 +102,7 @@ def init_unsafe(isolate, snapshot) else @snapshot = nil end - @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 + @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 [ (x) => { return (x instanceof Object || x instanceof Array) && !(x instanceof Date) && !(x instanceof Function) }, (x) => { return x instanceof Map }, @@ -113,6 +113,7 @@ def init_unsafe(isolate, snapshot) (x) => { var r = x.description; return r === undefined ? 'undefined' : r }, (x) => { return new Date(x) }, (x) => { return new Array(x) }, + (x) => { return new Uint8Array(x) }, ] CODE end @@ -329,6 +330,8 @@ def convert_ruby_to_js(value) js_new_date(value.to_f * 1000) when DateTime js_new_date(value.to_time.to_f * 1000) + when MiniRacer::Binary + @js_new_uint8array_func.call(value.data.bytes) else "Undefined Conversion" end diff --git a/lib/mini_racer/version.rb b/lib/mini_racer/version.rb index 6716a2d..c01e6b0 100644 --- a/lib/mini_racer/version.rb +++ b/lib/mini_racer/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module MiniRacer - VERSION = "0.20.0" + VERSION = "0.20.1" LIBV8_NODE_VERSION = "~> 24.12.0.1" end diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 211b052..53e563f 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -1224,6 +1224,19 @@ def test_large_integer end end + def test_binary_returns_uint8array + context = MiniRacer::Context.new + context.attach("add_one", ->(data) { + MiniRacer::Binary.new(data.bytes.map { _1 + 1 }.pack("C*")) + }) + + result = context.eval <<~JS + var output = add_one(new Uint8Array([0, 1, 2, 3])); + (output instanceof Uint8Array) && Array.from(output).join(",") === "1,2,3,4"; + JS + assert_equal true, result + end + def test_exception_message_encoding e = nil begin From 4346f72b4336be6a2104f784c204092115ed81b8 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 17 Apr 2026 09:32:14 +1000 Subject: [PATCH 2/2] version bump --- CHANGELOG | 2 +- lib/mini_racer/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b503fd2..9f213c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -- 0.20.1 - 16-04-2026 +- 0.21.0 - 16-04-2026 - Add MiniRacer::Binary for returning Uint8Array to JavaScript from attached Ruby callbacks - 0.20.0 - 24-02-2026 diff --git a/lib/mini_racer/version.rb b/lib/mini_racer/version.rb index c01e6b0..c6d6f1c 100644 --- a/lib/mini_racer/version.rb +++ b/lib/mini_racer/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module MiniRacer - VERSION = "0.20.1" + VERSION = "0.21.0" LIBV8_NODE_VERSION = "~> 24.12.0.1" end