Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- 0.21.0 - 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

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 18 additions & 4 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
13 changes: 13 additions & 0 deletions ext/mini_racer_extension/serde.c
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
11 changes: 11 additions & 0 deletions lib/mini_racer.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion lib/mini_racer/truffleruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/mini_racer/version.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

module MiniRacer
VERSION = "0.20.0"
VERSION = "0.21.0"
LIBV8_NODE_VERSION = "~> 24.12.0.1"
end
13 changes: 13 additions & 0 deletions test/mini_racer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading