From 1a2df798d5a66da60a58413ff8ea6d729a93f59f Mon Sep 17 00:00:00 2001 From: "E.FU" Date: Thu, 18 Jun 2026 13:32:17 +0800 Subject: [PATCH 1/2] fix: live-alias WebAssembly.Memory.buffer to WAMR linear memory --- lib/quickbeam/wasm_js.zig | 31 +++++++++++++++++++++++++++++++ priv/c_src/wamr_bridge.c | 21 +++++++++++++++++++++ priv/c_src/wamr_bridge.h | 6 ++++++ priv/ts/webassembly.ts | 13 +++++++------ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/lib/quickbeam/wasm_js.zig b/lib/quickbeam/wasm_js.zig index d09cc3c14..5386ef604 100644 --- a/lib/quickbeam/wasm_js.zig +++ b/lib/quickbeam/wasm_js.zig @@ -90,6 +90,7 @@ pub fn install(ctx: *qjs.JSContext, global: qjs.JSValue, max_reductions: i64) vo _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_memory_size", qjs.JS_NewCFunction(ctx, &wasm_memory_size_impl, "__qb_wasm_memory_size", 1)); _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_memory_grow", qjs.JS_NewCFunction(ctx, &wasm_memory_grow_impl, "__qb_wasm_memory_grow", 2)); _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_read_memory", qjs.JS_NewCFunction(ctx, &wasm_read_memory_impl, "__qb_wasm_read_memory", 3)); + _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_memory_buffer", qjs.JS_NewCFunction(ctx, &wasm_memory_buffer_impl, "__qb_wasm_memory_buffer", 1)); _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_read_global", qjs.JS_NewCFunction(ctx, &wasm_read_global_impl, "__qb_wasm_read_global", 2)); _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_write_global", qjs.JS_NewCFunction(ctx, &wasm_write_global_impl, "__qb_wasm_write_global", 3)); } @@ -472,6 +473,36 @@ fn wasm_read_memory_impl( return make_uint8array(ctx, bytes.ptr, bytes.len); } +// The ArrayBuffer returned by wasm_memory_buffer_impl aliases WAMR's linear +// memory directly; WAMR owns that storage, so the JS GC must not free it. +fn wasm_buffer_noop_free( + _: ?*qjs.JSRuntime, + _: ?*anyopaque, + _: ?*anyopaque, +) callconv(.c) void {} + +fn wasm_memory_buffer_impl( + ctx_opt: ?*qjs.JSContext, + _: qjs.JSValue, + argc: c_int, + argv: [*c]qjs.JSValue, +) callconv(.c) qjs.JSValue { + const ctx = ctx_opt orelse return js.js_exception(); + if (argc < 1) return throw_error(ctx, "instance id required"); + const state = get_context_state(ctx) orelse return throw_error(ctx, "missing wasm context state"); + var instance_id_i64: i64 = 0; + if (qjs.JS_ToInt64(ctx, &instance_id_i64, argv[0]) != 0 or instance_id_i64 < 0) return throw_error(ctx, "invalid instance handle"); + const entry = get_instance_entry(state, @intCast(instance_id_i64)) orelse return throw_error(ctx, "instance not found"); + + var size: u32 = 0; + const base = wamr.wamr_bridge_memory_data(entry.managed.inst, &size); + if (base == null or size == 0) return throw_error(ctx, "memory not available"); + + // Live alias: DataView/TypedArray writes on this buffer land directly in + // WAMR linear memory, and reads observe live guest state. No copy. + return qjs.JS_NewArrayBuffer(ctx, base, size, &wasm_buffer_noop_free, null, false); +} + fn wasm_read_global_impl( ctx_opt: ?*qjs.JSContext, _: qjs.JSValue, diff --git a/priv/c_src/wamr_bridge.c b/priv/c_src/wamr_bridge.c index d0e1cb682..64fb5b99b 100644 --- a/priv/c_src/wamr_bridge.c +++ b/priv/c_src/wamr_bridge.c @@ -475,6 +475,27 @@ wamr_bridge_write_memory(WamrInstance *inst, uint32_t offset, return true; } +uint8_t * +wamr_bridge_memory_data(WamrInstance *inst, uint32_t *out_size) +{ + if (out_size) + *out_size = 0; + if (!inst || !inst->inst) + return NULL; + + uint32_t mem_size = wamr_bridge_memory_size(inst); + if (mem_size == 0) + return NULL; + + void *native = wasm_runtime_addr_app_to_native(inst->inst, 0); + if (!native) + return NULL; + + if (out_size) + *out_size = mem_size; + return (uint8_t *)native; +} + static bool read_global_value(const wasm_global_inst_t *global, wasm_val_t *value, char *err_buf, uint32_t err_buf_size) diff --git a/priv/c_src/wamr_bridge.h b/priv/c_src/wamr_bridge.h index 401b9beba..3460640a8 100644 --- a/priv/c_src/wamr_bridge.h +++ b/priv/c_src/wamr_bridge.h @@ -98,6 +98,12 @@ bool wamr_bridge_read_memory(WamrInstance *inst, uint32_t offset, bool wamr_bridge_write_memory(WamrInstance *inst, uint32_t offset, const uint8_t *buf, uint32_t len); +/* Native base pointer of the instance's linear memory (for live aliasing). + * Returns NULL and sets *out_size to 0 when no memory is present. The + * returned pointer is owned by WAMR and is valid until the memory grows or + * the instance is destroyed; callers must not free it. */ +uint8_t *wamr_bridge_memory_data(WamrInstance *inst, uint32_t *out_size); + bool wamr_bridge_read_global(WamrInstance *inst, const char *name, wasm_val_t *value, char *err_buf, uint32_t err_buf_size); diff --git a/priv/ts/webassembly.ts b/priv/ts/webassembly.ts index 4c06d9b23..cc8680f23 100644 --- a/priv/ts/webassembly.ts +++ b/priv/ts/webassembly.ts @@ -74,6 +74,8 @@ declare function __qb_wasm_read_memory( length: number ): BufferSource +declare function __qb_wasm_memory_buffer(instanceHandle: number): ArrayBuffer + declare function __qb_wasm_read_global(instanceHandle: number, name: string): unknown declare function __qb_wasm_write_global( @@ -179,12 +181,11 @@ class WasmMemory { } const handle = this._handle - const size = qbWasmCall(() => __qb_wasm_memory_size(handle), 'memory size failed') as number - const bytes = qbWasmCall( - () => __qb_wasm_read_memory(handle, 0, size), - 'memory read failed' - ) as BufferSource - return wasmToUint8Array(bytes).slice().buffer + // Live alias of WAMR linear memory: host writes via DataView/TypedArray + // propagate into the guest, and reads observe live guest state. A grow + // may move the backing store, so callers must re-read `.buffer` afterward + // (matching browser detach-on-grow semantics). + return qbWasmCall(() => __qb_wasm_memory_buffer(handle), 'memory buffer failed') as ArrayBuffer } grow(delta: number): number { From 4658b668857887a40286ad67de210562fe64495a Mon Sep 17 00:00:00 2001 From: "E.FU" Date: Thu, 18 Jun 2026 13:32:17 +0800 Subject: [PATCH 2/2] fix: accept DataView in TextDecoder.decode --- lib/quickbeam/text_encoding.zig | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/quickbeam/text_encoding.zig b/lib/quickbeam/text_encoding.zig index b348fd6e5..46603350d 100644 --- a/lib/quickbeam/text_encoding.zig +++ b/lib/quickbeam/text_encoding.zig @@ -293,6 +293,29 @@ fn text_decoder_decode( const ptr = qjs.JS_GetArrayBuffer(ctx, &len, input) orelse return qjs.JS_NewString(ctx, ""); data = @as([*]const u8, @ptrCast(ptr))[0..len]; + } else if (qjs.JS_IsDataView(input)) { + // A DataView is a BufferSource but not a TypedArray, so + // JS_GetTypedArrayBuffer rejects it; read its view window directly. + // (Go's wasm_exec.js loadString passes a DataView here.) + const buffer_val = qjs.JS_GetPropertyStr(ctx, input, "buffer"); + defer qjs.JS_FreeValue(ctx, buffer_val); + const offset_val = qjs.JS_GetPropertyStr(ctx, input, "byteOffset"); + defer qjs.JS_FreeValue(ctx, offset_val); + const length_val = qjs.JS_GetPropertyStr(ctx, input, "byteLength"); + defer qjs.JS_FreeValue(ctx, length_val); + + var byte_offset_i64: i64 = 0; + var byte_len_i64: i64 = 0; + if (qjs.JS_ToInt64(ctx, &byte_offset_i64, offset_val) != 0 or byte_offset_i64 < 0 or + qjs.JS_ToInt64(ctx, &byte_len_i64, length_val) != 0 or byte_len_i64 < 0) + { + return qjs.JS_ThrowTypeError(ctx, "argument must be a BufferSource"); + } + + var ab_size: usize = 0; + const buf_ptr = qjs.JS_GetArrayBuffer(ctx, &ab_size, buffer_val) orelse + return qjs.JS_NewString(ctx, ""); + data = @as([*]const u8, @ptrCast(buf_ptr + @as(usize, @intCast(byte_offset_i64))))[0..@intCast(byte_len_i64)]; } else { var byte_offset: usize = 0; var byte_len: usize = 0;