Skip to main content

lib/quickbeam/wasm_js.zig

const types = @import("types.zig");
const js = @import("js_helpers.zig");
const wasm_host_imports = @import("wasm_host_imports.zig");
const wasm_common = @import("wasm_common.zig");

const std = types.std;
const qjs = types.qjs;
const gpa = types.gpa;

const wamr = @import("wamr.zig").wamr;

const InstanceEntry = struct {
    mod: *wamr.WamrModule,
    managed: *wasm_common.ManagedInstance,
};

const ContextState = struct {
    next_instance_id: u64 = 1,
    max_reductions: i64 = 0,
    instances: std.AutoHashMapUnmanaged(u64, InstanceEntry) = .{},

    fn deinit(self: *ContextState) void {
        var it = self.instances.iterator();
        while (it.next()) |entry| {
            entry.value_ptr.managed.destroy();
            wamr.wamr_bridge_free_module(entry.value_ptr.mod);
        }
        self.instances.deinit(gpa);
    }
};

const ParsedHostImport = wasm_host_imports.ImportSpec;

var states_mutex: std.Thread.Mutex = .{};
var states: std.AutoHashMapUnmanaged(usize, *ContextState) = .{};

fn throw_error(ctx: *qjs.JSContext, msg: []const u8) qjs.JSValue {
    const err = qjs.JS_NewError(ctx);
    if (!js.js_is_exception(err)) {
        _ = qjs.JS_SetPropertyStr(ctx, err, "message", qjs.JS_NewStringLen(ctx, msg.ptr, msg.len));
    }
    return qjs.JS_Throw(ctx, err);
}

fn context_key(ctx: *qjs.JSContext) usize {
    return @intFromPtr(ctx);
}

fn ensure_context_state(ctx: *qjs.JSContext, max_reductions: i64) ?*ContextState {
    states_mutex.lock();
    defer states_mutex.unlock();

    const key = context_key(ctx);
    if (states.get(key)) |state| {
        state.max_reductions = max_reductions;
        return state;
    }

    const state = gpa.create(ContextState) catch return null;
    state.* = .{ .max_reductions = max_reductions };
    states.put(gpa, key, state) catch {
        gpa.destroy(state);
        return null;
    };
    return state;
}

fn get_context_state(ctx: *qjs.JSContext) ?*ContextState {
    states_mutex.lock();
    defer states_mutex.unlock();
    return states.get(context_key(ctx));
}

pub fn destroy_context(ctx: *qjs.JSContext) void {
    states_mutex.lock();
    const removed = states.fetchRemove(context_key(ctx));
    states_mutex.unlock();

    if (removed) |entry| {
        entry.value.deinit();
        gpa.destroy(entry.value);
    }
}

pub fn install(ctx: *qjs.JSContext, global: qjs.JSValue, max_reductions: i64) void {
    _ = ensure_context_state(ctx, max_reductions) orelse return;

    _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_start", qjs.JS_NewCFunction(ctx, &wasm_start_impl, "__qb_wasm_start", 3));
    _ = qjs.JS_SetPropertyStr(ctx, global, "__qb_wasm_call", qjs.JS_NewCFunction(ctx, &wasm_call_impl, "__qb_wasm_call", 3));
    _ = 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_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));
}

fn copy_js_string(ctx: *qjs.JSContext, value: qjs.JSValue) ?[]u8 {
    const ptr = qjs.JS_ToCString(ctx, value) orelse return null;
    defer qjs.JS_FreeCString(ctx, ptr);
    return gpa.dupe(u8, std.mem.span(ptr)) catch null;
}

fn get_property_string(ctx: *qjs.JSContext, obj: qjs.JSValue, name: [*:0]const u8) ?[]u8 {
    const prop = qjs.JS_GetPropertyStr(ctx, obj, name);
    defer qjs.JS_FreeValue(ctx, prop);
    return copy_js_string(ctx, prop);
}

fn get_array_length(ctx: *qjs.JSContext, value: qjs.JSValue) usize {
    const len_val = qjs.JS_GetPropertyStr(ctx, value, "length");
    defer qjs.JS_FreeValue(ctx, len_val);
    var len: i64 = 0;
    _ = qjs.JS_ToInt64(ctx, &len, len_val);
    if (len < 0) return 0;
    return @intCast(len);
}

fn get_bytes_view(ctx: *qjs.JSContext, value: qjs.JSValue) ![]const u8 {
    var buf_size: usize = 0;
    const direct = qjs.JS_GetArrayBuffer(ctx, &buf_size, value);
    if (direct != null) return direct[0..buf_size];

    var byte_offset: usize = 0;
    var byte_len: usize = 0;
    var bytes_per_element: usize = 0;
    const ab = qjs.JS_GetTypedArrayBuffer(ctx, value, &byte_offset, &byte_len, &bytes_per_element);
    if (js.js_is_exception(ab)) {
        const exc = qjs.JS_GetException(ctx);
        qjs.JS_FreeValue(ctx, exc);
        return error.BadArg;
    }
    defer qjs.JS_FreeValue(ctx, ab);

    const ptr = qjs.JS_GetArrayBuffer(ctx, &buf_size, ab);
    if (ptr == null) return error.BadArg;
    return ptr[byte_offset .. byte_offset + byte_len];
}

fn make_uint8array(ctx: *qjs.JSContext, data: [*]u8, size: usize) qjs.JSValue {
    const ab = qjs.JS_NewArrayBufferCopy(ctx, data, size);
    if (js.js_is_exception(ab)) return js.js_exception();
    defer qjs.JS_FreeValue(ctx, ab);

    const global = qjs.JS_GetGlobalObject(ctx);
    defer qjs.JS_FreeValue(ctx, global);
    const ctor = qjs.JS_GetPropertyStr(ctx, global, "Uint8Array");
    defer qjs.JS_FreeValue(ctx, ctor);

    var args = [_]qjs.JSValue{ab};
    return qjs.JS_CallConstructor(ctx, ctor, 1, &args);
}

fn parse_host_imports(ctx: *qjs.JSContext, imports_val: qjs.JSValue) ![]ParsedHostImport {
    if (qjs.JS_IsUndefined(imports_val) or qjs.JS_IsNull(imports_val)) {
        return gpa.alloc(ParsedHostImport, 0);
    }

    const len = get_array_length(ctx, imports_val);
    const imports = try gpa.alloc(ParsedHostImport, len);
    var parsed_count: usize = 0;
    errdefer {
        for (imports[0..parsed_count]) |item| {
            gpa.free(item.module_name);
            gpa.free(item.symbol);
            gpa.free(item.signature);
            gpa.free(item.callback_name);
        }
        gpa.free(imports);
    }

    for (imports, 0..) |*import, index| {
        const item = qjs.JS_GetPropertyUint32(ctx, imports_val, @intCast(index));
        defer qjs.JS_FreeValue(ctx, item);

        import.module_name = get_property_string(ctx, item, "module_name") orelse return error.BadArg;
        import.symbol = get_property_string(ctx, item, "symbol") orelse return error.BadArg;
        import.signature = get_property_string(ctx, item, "signature") orelse return error.BadArg;
        import.callback_name = get_property_string(ctx, item, "callback_name") orelse return error.BadArg;
        parsed_count += 1;
    }

    return imports;
}

fn free_host_imports(imports: []ParsedHostImport) void {
    for (imports) |import| {
        gpa.free(import.module_name);
        gpa.free(import.symbol);
        gpa.free(import.signature);
        gpa.free(import.callback_name);
    }
    gpa.free(imports);
}

fn parse_memory_initializers(ctx: *qjs.JSContext, values: qjs.JSValue) ![][]u8 {
    if (qjs.JS_IsUndefined(values) or qjs.JS_IsNull(values)) {
        return gpa.alloc([]u8, 0);
    }

    const len = get_array_length(ctx, values);
    const initializers = try gpa.alloc([]u8, len);
    var parsed_count: usize = 0;
    errdefer {
        for (initializers[0..parsed_count]) |bytes| gpa.free(bytes);
        gpa.free(initializers);
    }

    for (initializers, 0..) |*entry, index| {
        const item = qjs.JS_GetPropertyUint32(ctx, values, @intCast(index));
        defer qjs.JS_FreeValue(ctx, item);
        const bytes = try get_bytes_view(ctx, item);
        entry.* = try gpa.dupe(u8, bytes);
        parsed_count += 1;
    }

    return initializers;
}

fn free_memory_initializers(initializers: [][]u8) void {
    for (initializers) |bytes| gpa.free(bytes);
    gpa.free(initializers);
}

fn get_instance_entry(state: *ContextState, instance_id: u64) ?*InstanceEntry {
    return state.instances.getPtr(instance_id);
}

fn instruction_limit(ctx: *qjs.JSContext, state: *ContextState) c_int {
    if (state.max_reductions <= 0) return -1;

    const used = qjs.JS_GetContextReductionCount(ctx);
    const remaining = state.max_reductions - used;
    if (remaining <= 0) return 1;

    const fuel = @as(i128, remaining) * 100;
    return if (fuel > std.math.maxInt(c_int)) std.math.maxInt(c_int) else @intCast(fuel);
}

fn js_value_to_wasm(ctx: *qjs.JSContext, value: qjs.JSValue, kind: wamr.wasm_valkind_t) !wamr.wasm_val_t {
    var result = std.mem.zeroes(wamr.wasm_val_t);
    result.kind = kind;

    switch (kind) {
        wamr.WASM_I32 => {
            var v: i32 = 0;
            if (qjs.JS_IsBigInt(value)) {
                var big: i64 = 0;
                if (qjs.JS_ToBigInt64(ctx, &big, value) != 0) return error.BadArg;
                if (big < std.math.minInt(i32) or big > std.math.maxInt(i32)) return error.BadArg;
                v = @intCast(big);
            } else if (qjs.JS_ToInt32(ctx, &v, value) != 0) return error.BadArg;
            result.of.i32 = v;
        },
        wamr.WASM_I64 => {
            var v: i64 = 0;
            if (qjs.JS_IsBigInt(value)) {
                if (qjs.JS_ToBigInt64(ctx, &v, value) != 0) return error.BadArg;
            } else if (qjs.JS_ToInt64(ctx, &v, value) != 0) return error.BadArg;
            result.of.i64 = v;
        },
        wamr.WASM_F32 => {
            var v: f64 = 0;
            if (qjs.JS_ToFloat64(ctx, &v, value) != 0) return error.BadArg;
            result.of.f32 = @floatCast(v);
        },
        wamr.WASM_F64 => {
            var v: f64 = 0;
            if (qjs.JS_ToFloat64(ctx, &v, value) != 0) return error.BadArg;
            result.of.f64 = v;
        },
        else => return error.UnsupportedType,
    }

    return result;
}

fn wasm_to_js_value(ctx: *qjs.JSContext, value: wamr.wasm_val_t) qjs.JSValue {
    return switch (value.kind) {
        wamr.WASM_I32 => qjs.JS_NewInt32(ctx, value.of.i32),
        wamr.WASM_I64 => qjs.JS_NewBigInt64(ctx, value.of.i64),
        wamr.WASM_F32 => qjs.JS_NewFloat64(ctx, @as(f64, value.of.f32)),
        wamr.WASM_F64 => qjs.JS_NewFloat64(ctx, value.of.f64),
        else => js.js_undefined(),
    };
}

fn wasm_start_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, "WebAssembly start requires bytes");
    if (!wasm_common.ensure_init()) return throw_error(ctx, "WAMR initialization failed");

    const state = get_context_state(ctx) orelse return throw_error(ctx, "missing wasm context state");
    const bytes = get_bytes_view(ctx, argv[0]) catch return throw_error(ctx, "invalid wasm bytes");
    const host_imports = parse_host_imports(ctx, if (argc > 1) argv[1] else js.js_undefined()) catch return throw_error(ctx, "invalid host imports");
    defer free_host_imports(host_imports);
    const memory_initializers = parse_memory_initializers(ctx, if (argc > 2) argv[2] else js.js_undefined()) catch return throw_error(ctx, "invalid memory initializers");
    defer free_memory_initializers(memory_initializers);

    var err_buf: [256]u8 = undefined;
    const mod = wamr.wamr_bridge_compile(bytes.ptr, @intCast(bytes.len), &err_buf, err_buf.len);
    if (mod == null) return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    errdefer wamr.wamr_bridge_free_module(mod);

    var import_specs: []wasm_host_imports.ImportSpec = if (host_imports.len > 0)
        gpa.alloc(wasm_host_imports.ImportSpec, host_imports.len) catch return throw_error(ctx, "out of memory")
    else
        @constCast(&[_]wasm_host_imports.ImportSpec{});
    defer if (host_imports.len > 0) gpa.free(import_specs);

    for (host_imports, 0..) |import, index| {
        import_specs[index] = .{
            .module_name = import.module_name,
            .symbol = import.symbol,
            .signature = import.signature,
            .callback_name = import.callback_name,
        };
    }

    var prepared_imports = if (import_specs.len > 0)
        wasm_host_imports.prepare(import_specs, ctx, .js) catch return throw_error(ctx, "out of memory")
    else
        wasm_host_imports.PreparedImports.empty();
    errdefer prepared_imports.deinit();

    const managed = wasm_common.start_managed_instance(mod orelse return throw_error(ctx, "null module"), 65_536, 65_536, if (prepared_imports.registrations.len > 0) &prepared_imports else null, &err_buf) orelse return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    const mod_nn = mod orelse return throw_error(ctx, "null module");
    errdefer managed.destroy();

    if (memory_initializers.len > 1) return throw_error(ctx, "multiple memory imports are not supported yet");
    if (memory_initializers.len == 1) {
        if (!wamr.wamr_bridge_write_memory(managed.inst, 0, memory_initializers[0].ptr, @intCast(memory_initializers[0].len))) {
            return throw_error(ctx, "failed to initialize imported memory");
        }
    }

    const instance_id = state.next_instance_id;
    state.next_instance_id += 1;
    state.instances.put(gpa, instance_id, .{ .mod = mod_nn, .managed = managed }) catch return throw_error(ctx, "out of memory");

    return qjs.JS_NewInt64(ctx, @intCast(instance_id));
}

fn wasm_call_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 < 2) return throw_error(ctx, "WebAssembly call requires instance id and function name");

    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");

    const func_name_ptr = qjs.JS_ToCString(ctx, argv[1]) orelse return throw_error(ctx, "invalid function name");
    defer qjs.JS_FreeCString(ctx, func_name_ptr);

    var err_buf: [256]u8 = undefined;
    var param_count: u32 = 0;
    var result_count: u32 = 0;
    if (!wamr.wamr_bridge_function_signature(entry.managed.inst, func_name_ptr, &param_count, null, &result_count, null, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    const arg_array = if (argc > 2) argv[2] else js.js_undefined();
    const arg_len = if (qjs.JS_IsUndefined(arg_array) or qjs.JS_IsNull(arg_array)) 0 else get_array_length(ctx, arg_array);
    if (arg_len != param_count) return throw_error(ctx, "arity mismatch");

    const param_types = std.heap.c_allocator.alloc(wamr.wasm_valkind_t, param_count) catch return throw_error(ctx, "out of memory");
    defer std.heap.c_allocator.free(param_types);
    const result_types = std.heap.c_allocator.alloc(wamr.wasm_valkind_t, result_count) catch return throw_error(ctx, "out of memory");
    defer std.heap.c_allocator.free(result_types);

    if (!wamr.wamr_bridge_function_signature(entry.managed.inst, func_name_ptr, &param_count, if (param_count > 0) param_types.ptr else null, &result_count, if (result_count > 0) result_types.ptr else null, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    const params = std.heap.c_allocator.alloc(wamr.wasm_val_t, param_count) catch return throw_error(ctx, "out of memory");
    defer std.heap.c_allocator.free(params);
    for (0..param_count) |index| {
        const arg = qjs.JS_GetPropertyUint32(ctx, arg_array, @intCast(index));
        defer qjs.JS_FreeValue(ctx, arg);
        params[index] = js_value_to_wasm(ctx, arg, param_types[index]) catch return throw_error(ctx, "invalid argument type");
    }

    const results = std.heap.c_allocator.alloc(wamr.wasm_val_t, result_count) catch return throw_error(ctx, "out of memory");
    defer std.heap.c_allocator.free(results);

    const limit = instruction_limit(ctx, state);
    wamr.wamr_bridge_set_instruction_limit(entry.managed.inst, limit);

    if (!wamr.wamr_bridge_call_typed(entry.managed.inst, func_name_ptr, if (param_count > 0) params.ptr else null, param_count, if (result_count > 0) results.ptr else null, result_count, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    if (result_count == 0) return js.js_undefined();
    if (result_count == 1) return wasm_to_js_value(ctx, results[0]);

    const array = qjs.JS_NewArray(ctx);
    for (0..result_count) |index| {
        _ = qjs.JS_SetPropertyUint32(ctx, array, @intCast(index), wasm_to_js_value(ctx, results[index]));
    }
    return array;
}

fn wasm_memory_size_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");
    return qjs.JS_NewInt64(ctx, wamr.wamr_bridge_memory_size(entry.managed.inst));
}

fn wasm_memory_grow_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 < 2) return throw_error(ctx, "instance id and delta required");
    const state = get_context_state(ctx) orelse return throw_error(ctx, "missing wasm context state");
    var instance_id_i64: i64 = 0;
    var delta: i32 = 0;
    if (qjs.JS_ToInt64(ctx, &instance_id_i64, argv[0]) != 0 or instance_id_i64 < 0) return throw_error(ctx, "invalid instance handle");
    if (qjs.JS_ToInt32(ctx, &delta, argv[1]) != 0 or delta < 0) return throw_error(ctx, "invalid memory grow delta");
    const entry = get_instance_entry(state, @intCast(instance_id_i64)) orelse return throw_error(ctx, "instance not found");
    const grown = wamr.wamr_bridge_memory_grow(entry.managed.inst, @intCast(delta));
    if (grown < 0) return throw_error(ctx, "memory grow failed");
    return qjs.JS_NewInt64(ctx, grown);
}

fn wasm_read_memory_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 < 3) return throw_error(ctx, "instance id, offset, and length required");
    const state = get_context_state(ctx) orelse return throw_error(ctx, "missing wasm context state");
    var instance_id_i64: i64 = 0;
    var offset: i64 = 0;
    var length: 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");
    if (qjs.JS_ToInt64(ctx, &offset, argv[1]) != 0 or offset < 0) return throw_error(ctx, "invalid memory offset");
    if (qjs.JS_ToInt64(ctx, &length, argv[2]) != 0 or length < 0) return throw_error(ctx, "invalid memory length");
    const entry = get_instance_entry(state, @intCast(instance_id_i64)) orelse return throw_error(ctx, "instance not found");

    const bytes = gpa.alloc(u8, @intCast(length)) catch return throw_error(ctx, "out of memory");
    defer gpa.free(bytes);

    if (!wamr.wamr_bridge_read_memory(entry.managed.inst, @intCast(offset), bytes.ptr, @intCast(length))) {
        return throw_error(ctx, "out of bounds");
    }

    return make_uint8array(ctx, bytes.ptr, bytes.len);
}

fn wasm_read_global_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 < 2) return throw_error(ctx, "instance id and global name 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");

    const name_ptr = qjs.JS_ToCString(ctx, argv[1]) orelse return throw_error(ctx, "invalid global name");
    defer qjs.JS_FreeCString(ctx, name_ptr);

    var err_buf: [256]u8 = undefined;
    var value = std.mem.zeroes(wamr.wasm_val_t);
    if (!wamr.wamr_bridge_read_global(entry.managed.inst, name_ptr, &value, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    return wasm_to_js_value(ctx, value);
}

fn wasm_write_global_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 < 3) return throw_error(ctx, "instance id, global name, and value 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");

    const name_ptr = qjs.JS_ToCString(ctx, argv[1]) orelse return throw_error(ctx, "invalid global name");
    defer qjs.JS_FreeCString(ctx, name_ptr);

    var err_buf: [256]u8 = undefined;
    var current = std.mem.zeroes(wamr.wasm_val_t);
    if (!wamr.wamr_bridge_read_global(entry.managed.inst, name_ptr, &current, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    const value = js_value_to_wasm(ctx, argv[2], current.kind) catch return throw_error(ctx, "invalid global value");
    if (!wamr.wamr_bridge_write_global(entry.managed.inst, name_ptr, &value, &err_buf, err_buf.len)) {
        return throw_error(ctx, std.mem.sliceTo(&err_buf, 0));
    }

    return argv[2];
}