Skip to content

Commit 03bbead

Browse files
committed
Free the ABI string buffers on the JS bridge
Every Ruby String crossing to JS is copied into a fresh `xmalloc` buffer by `rstring_to_abi_string`, and `rb_js_abi_host_string_free` is never called on it. The canonical ABI leaves the caller owning a lowered string param, and the generated bindings only pass ptr/len, so those bytes are never reclaimed. wasm linear memory never shrinks, so the loss is permanent. Four sites lower a string without freeing: `_rb_js_eval_js`, `_rb_js_obj_aref`, `_rb_js_obj_aset` and `_rb_js_string_to_js`. Because `_rb_js_obj_call` resolves its method through `_rb_js_obj_aref`, EVERY `JS::Object#call` leaks the length of its method name. Two more leak in the opposite direction. `js-value-to-string` and `js-value-typeof` have the host allocate through `cabi_realloc` and hand ownership to the guest, but `_rb_js_obj_to_s` and `_rb_js_obj_typeof` copy the bytes out with `rb_utf8_str_new` and abandon the buffer. Their siblings `_rb_js_obj_to_i` and `_rb_js_obj_to_f` already call `rb_js_abi_host_raw_integer_free` on the same kind of returned payload, so this brings the two string readers in line with them. In `_rb_js_eval_js`, `_rb_js_obj_aref` and `_rb_js_obj_aset` the free has to precede `raise_js_error_if_failure`, which longjmps past anything after it. `types.h` gains a `rb_js_abi_host_string_free` alias so the component-model build resolves it to `ext_string_free`; the legacy bindings already declare it. Both implementations are the same guarded `free`. Measured on a 60fps canvas application making roughly 1000 crossings per frame: about 1 MB/s of unreclaimable linear memory before, and flat after. Such an application dies at 2 GB rather than 4, because the generated glue reads guest pointers as signed i32, so the first symptom is a confusing "RangeError: Start offset -2147..." out of `new Uint8Array`.
1 parent 3f6e185 commit 03bbead

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

packages/gems/js/ext/js/js-core.c

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ static VALUE _rb_js_eval_js(VALUE _, VALUE code_str) {
101101
rstring_to_abi_string(code_str, &abi_str);
102102
rb_js_abi_host_js_abi_result_t ret;
103103
rb_js_abi_host_eval_js(&abi_str, &ret);
104+
rb_js_abi_host_string_free(&abi_str);
104105
raise_js_error_if_failure(&ret);
105106
return jsvalue_s_new(ret.val.success);
106107
}
@@ -197,6 +198,7 @@ static VALUE _rb_js_obj_aref(VALUE obj, VALUE key) {
197198
rstring_to_abi_string(key, &key_abi_str);
198199
rb_js_abi_host_js_abi_result_t ret;
199200
rb_js_abi_host_reflect_get(p->abi, &key_abi_str, &ret);
201+
rb_js_abi_host_string_free(&key_abi_str);
200202
raise_js_error_if_failure(&ret);
201203
return jsvalue_s_new(ret.val.success);
202204
}
@@ -224,6 +226,7 @@ static VALUE _rb_js_obj_aset(VALUE obj, VALUE key, VALUE val) {
224226
rstring_to_abi_string(key, &key_abi_str);
225227
rb_js_abi_host_js_abi_result_t ret;
226228
rb_js_abi_host_reflect_set(p->abi, &key_abi_str, v->abi, &ret);
229+
rb_js_abi_host_string_free(&key_abi_str);
227230
raise_js_error_if_failure(&ret);
228231
rb_js_abi_host_js_abi_value_free(&ret.val.success);
229232
RB_GC_GUARD(rv);
@@ -346,7 +349,9 @@ static VALUE _rb_js_obj_typeof(VALUE obj) {
346349
struct jsvalue *p = check_jsvalue(obj);
347350
rb_js_abi_host_string_t ret0;
348351
rb_js_abi_host_js_value_typeof(p->abi, &ret0);
349-
return rb_str_new((const char *)ret0.ptr, ret0.len);
352+
VALUE typeof_str = rb_str_new((const char *)ret0.ptr, ret0.len);
353+
rb_js_abi_host_string_free(&ret0);
354+
return typeof_str;
350355
}
351356

352357
/*
@@ -366,7 +371,9 @@ static VALUE _rb_js_obj_to_s(VALUE obj) {
366371
struct jsvalue *p = check_jsvalue(obj);
367372
rb_js_abi_host_string_t ret0;
368373
rb_js_abi_host_js_value_to_string(p->abi, &ret0);
369-
return rb_utf8_str_new((const char *)ret0.ptr, ret0.len);
374+
VALUE to_s_str = rb_utf8_str_new((const char *)ret0.ptr, ret0.len);
375+
rb_js_abi_host_string_free(&ret0);
376+
return to_s_str;
370377
}
371378

372379
/*
@@ -494,7 +501,10 @@ static VALUE _rb_js_float_to_js(VALUE obj) {
494501
static VALUE _rb_js_string_to_js(VALUE obj) {
495502
rb_js_abi_host_string_t abi_str;
496503
rstring_to_abi_string(obj, &abi_str);
497-
return jsvalue_s_new(rb_js_abi_host_string_to_js_string(&abi_str));
504+
rb_js_abi_host_own_js_abi_value_t js_str =
505+
rb_js_abi_host_string_to_js_string(&abi_str);
506+
rb_js_abi_host_string_free(&abi_str);
507+
return jsvalue_s_new(js_str);
498508
}
499509

500510
/*

packages/gems/js/ext/js/types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ typedef ext_list_string_t rb_abi_guest_list_string_t;
4444
ruby_js_js_runtime_export_js_value_to_host(borrow_js_value(value))
4545
# define rb_js_abi_host_raw_integer_free(ptr) \
4646
ruby_js_js_runtime_raw_integer_free(ptr)
47+
# define rb_js_abi_host_string_free(ptr) ext_string_free(ptr)
4748
# define rb_js_abi_host_rb_object_to_js_rb_value(val) \
4849
ruby_js_js_runtime_rb_object_to_js_rb_value(val)
4950
# define rb_js_abi_host_int_to_js_number(val) \

packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,4 +441,29 @@ def test_apply
441441
floor = JS.global[:Math][:floor]
442442
assert_equal 3, floor.apply(3.14).to_i
443443
end
444+
445+
# Every string crossing the boundary now frees its ABI buffer, in both
446+
# directions. Freeing one too early would hand back another allocation's
447+
# bytes, so each round trip below carries a value distinct from its
448+
# neighbours - garbage or a stale repeat fails the assertion rather than
449+
# passing by luck. GC.stress shakes out an early free that only shows when
450+
# the allocator reuses the block promptly.
451+
def test_string_round_trips_survive_their_abi_buffers
452+
object = JS.eval("return {};")
453+
64.times do |i|
454+
key = "key_#{i}_#{"x" * i}"
455+
value = "value_#{i}_#{"y" * i}"
456+
object[key] = value
457+
assert_equal value, object[key].to_s
458+
assert_equal "string", object[key].typeof
459+
end
460+
GC.stress = true
461+
16.times do |i|
462+
key = "stress_#{i}"
463+
object[key] = JS.eval("return #{key.dump};")
464+
assert_equal key, object[key].to_s
465+
end
466+
ensure
467+
GC.stress = false
468+
end
444469
end

0 commit comments

Comments
 (0)