Skip to content
Closed
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
23 changes: 23 additions & 0 deletions lib/quickbeam/text_encoding.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Comment on lines +296 to +300
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)];
Comment on lines +300 to +318
} else {
var byte_offset: usize = 0;
var byte_len: usize = 0;
Expand Down
31 changes: 31 additions & 0 deletions lib/quickbeam/wasm_js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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);
Comment on lines +499 to +503
}

fn wasm_read_global_impl(
ctx_opt: ?*qjs.JSContext,
_: qjs.JSValue,
Expand Down
21 changes: 21 additions & 0 deletions priv/c_src/wamr_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions priv/c_src/wamr_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
13 changes: 7 additions & 6 deletions priv/ts/webassembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Comment on lines +184 to +188
}

grow(delta: number): number {
Expand Down