Skip to content
Open
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Fixed

- **`load_module/3` now propagates top-level module evaluation errors** — runtime exceptions thrown while evaluating module code are returned as `{:error, %QuickBEAM.JSError{}}` instead of incorrectly succeeding with `:ok`.
- **Propagate runtime errors from pending job execution** — `load_module/3`, `eval/3`, `call/3`, and `load_bytecode/2` now detect and return errors thrown during QuickJS job draining instead of silently swallowing them.

## 0.8.1

Expand Down
103 changes: 103 additions & 0 deletions docs/wasm-js-api-roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# WebAssembly JS API roadmap

## Goal

Bring QuickBEAM's `WebAssembly` polyfill closer to the WebAssembly JavaScript Interface standard.

## Standards checked

- https://webassembly.github.io/spec/js-api/
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global

## Current status

### Implemented

- `WebAssembly.compile(bytes)`
- `WebAssembly.instantiate(bytes | module)`
- `WebAssembly.validate(bytes)`
- `new WebAssembly.Module(bytes)`
- `new WebAssembly.Instance(module)`
- `WebAssembly.Module.exports(module)`
- `WebAssembly.Module.imports(module)`
- numeric wasm calls for `i32`, `i64`, `f32`, `f64`
- `i64` results mapped to JS `BigInt`
- exported numeric globals
- exported memory exposure
- `WebAssembly.Module.customSections()`
- `WebAssembly.compileStreaming()`
- `WebAssembly.instantiateStreaming()`
- `importObject` validation for function/memory/global imports
- JS-owned function imports executed inline on the owning QuickJS worker / ContextPool thread
- snapshot-style memory/global imports for instantiation
- exported imported memory reuses the original `WebAssembly.Memory` wrapper

### Not yet standard-complete

- runtime-backed `Memory.buffer` semantics
- runtime-backed tables
- full global import/export parity
- table imports
- live shared imported memory/global semantics
- compile options (`builtins`, `importedStringConstants`)
- `Tag`, `Exception`, `JSTag`
- exact object caching / identity semantics from the spec
- exact error semantics for every edge case

## Implementation phases

### Phase 1 — Instantiation and linking

1. harden function imports around shared budget / instruction limits
2. implement memory imports
3. implement table imports
4. implement global imports
5. validate `importObject` shape and types
6. return `LinkError` and `TypeError` in the right places

### Phase 2 — Memory

1. make exported memory runtime-backed
2. make imported memory visible to wasm
3. improve `buffer` semantics
4. improve `grow()` semantics

### Phase 3 — Table

1. make exported tables runtime-backed
2. make imported tables runtime-backed
3. implement shared JS/wasm mutation visibility

### Phase 4 — Global

1. imported globals
2. exported globals with shared state
3. mutability checks
4. `i64` globals as `BigInt`

### Phase 5 — Namespace completeness

1. `compileStreaming()`
2. `instantiateStreaming()`
3. `Module.customSections()`

### Phase 6 — JS API 2.0 / newer features

1. compile options
2. `WebAssembly.JSTag`
3. `WebAssembly.Tag`
4. `WebAssembly.Exception`

## Recommended order

1. imports
2. real memory semantics
3. real tables
4. real globals
5. streaming and custom sections
6. newer API additions
3 changes: 2 additions & 1 deletion lib/quickbeam/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ defmodule QuickBEAM.Application do
id: :quickbeam_pg,
start: {:pg, :start_link, [QuickBEAM.BroadcastChannel]}
},
QuickBEAM.LockManager
QuickBEAM.LockManager,
QuickBEAM.WasmAPI
]

QuickBEAM.Storage.init()
Expand Down
23 changes: 15 additions & 8 deletions lib/quickbeam/beam_to_js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,25 @@ fn convert_recursive(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm

// Tuple
var tuple_arity: c_int = 0;
// SAFETY: immediately filled by enif_get_tuple
var tuple_elems: [*c]const e.ErlNifTerm = undefined;
if (e.enif_get_tuple(env, term, &tuple_arity, &tuple_elems) != 0) {
var tuple_elems: ?[*]const e.ErlNifTerm = null;
if (e.enif_get_tuple(env, term, &tuple_arity, @ptrCast(&tuple_elems)) != 0) {
const elems = tuple_elems orelse return js.js_null();
// {:bytes, binary} → Uint8Array
if (tuple_arity == 2) {
var tag_buf: [16]u8 = undefined;
const tag_len = e.enif_get_atom(env, tuple_elems[0], &tag_buf, tag_buf.len, e.ERL_NIF_LATIN1);
const tag_len = e.enif_get_atom(env, elems[0], &tag_buf, tag_buf.len, e.ERL_NIF_LATIN1);
if (tag_len > 0 and std.mem.eql(u8, tag_buf[0..@intCast(tag_len - 1)], "bytes")) {
// SAFETY: immediately filled by enif_inspect_binary
var bbin: e.ErlNifBinary = undefined;
if (e.enif_inspect_binary(env, tuple_elems[1], &bbin) != 0) {
if (e.enif_inspect_binary(env, elems[1], &bbin) != 0) {
return make_uint8array(ctx, bbin.data, bbin.size);
}
}
}
// Generic tuple → Array
const arr = qjs.JS_NewArray(ctx);
for (0..@intCast(tuple_arity)) |idx| {
const elem = convert_recursive(ctx, env, tuple_elems[idx], depth + 1);
const elem = convert_recursive(ctx, env, elems[idx], depth + 1);
_ = qjs.JS_SetPropertyUint32(ctx, arr, @intCast(idx), elem);
}
return arr;
Expand Down Expand Up @@ -159,10 +159,17 @@ fn convert_list(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, dep
return arr;
}

fn map_iterator_first() e.ErlNifMapIteratorEntry {
return switch (@typeInfo(e.ErlNifMapIteratorEntry)) {
.@"enum" => @as(e.ErlNifMapIteratorEntry, @enumFromInt(1)),
else => std.mem.zeroes(e.ErlNifMapIteratorEntry),
};
}

fn convert_map(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, depth: u32) qjs.JSValue {
if (beam_proxy.class_id != 0) {
var map_size: usize = 0;
if (e.enif_get_map_size(env, term, &map_size) != 0 and map_size > 4) {
if (e.enif_get_map_size(env, term, &map_size) != 0 and map_size > 0) {
return beam_proxy.create(ctx, env, term);
}
}
Expand All @@ -171,7 +178,7 @@ fn convert_map(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, dept

// SAFETY: immediately filled by enif_map_iterator_create
var iter: e.ErlNifMapIterator = undefined;
if (e.enif_map_iterator_create(env, term, &iter, e.ERL_NIF_MAP_ITERATOR_FIRST) == 0) {
if (e.enif_map_iterator_create(env, term, &iter, map_iterator_first()) == 0) {
return obj;
}
defer e.enif_map_iterator_destroy(env, &iter);
Expand Down
1 change: 1 addition & 0 deletions lib/quickbeam/context_worker.zig
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ fn handle_create_context(
.pending_calls = std.AutoHashMap(u64, worker.PendingCall).init(gpa),
.timers = std.AutoHashMap(u64, worker.TimerEntry).init(gpa),
.start_time = std.time.nanoTimestamp(),
.max_reductions = p.max_reductions,
};

entry.state.install_globals();
Expand Down
10 changes: 8 additions & 2 deletions lib/quickbeam/js_to_beam.zig
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ fn convert_recursive(ctx: *qjs.JSContext, val: qjs.JSValue, state: *ConvertState
const ptr = qjs.JS_ToCString(ctx, val);
if (ptr != null) {
defer qjs.JS_FreeCString(ctx, ptr);
return beam.make(std.mem.span(ptr), state.opts).v;
const value = std.mem.span(ptr);
if (std.fmt.parseInt(i64, value, 10)) |parsed| {
return e.enif_make_int64(state.opts.env, @bitCast(parsed));
} else |_| {
return beam.make(value, state.opts).v;
}
}
return beam.make_into_atom("nil", state.opts).v;
}
Expand Down Expand Up @@ -156,7 +161,8 @@ fn convert_number(ctx: *qjs.JSContext, val: qjs.JSValue, opts: Env) e.ErlNifTerm
return beam.make_into_atom("-Infinity", opts).v;
}
if (d == @trunc(d) and d >= -9007199254740991 and d <= 9007199254740991) {
return beam.make(@as(i64, @intFromFloat(d)), opts).v;
const value: i64 = @intFromFloat(d);
return e.enif_make_int64(opts.env, @bitCast(value));
}
return beam.make(d, opts).v;
}
Expand Down
121 changes: 117 additions & 4 deletions lib/quickbeam/native.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,95 @@ defmodule QuickBEAM.Native do
{:priv, String.replace_prefix(path, "priv/", ""), @lexbor_cflags}
end)

@wamr_cflags [
"-std=c11",
"-D_GNU_SOURCE",
"-DWASM_ENABLE_INTERP=1",
"-DWASM_ENABLE_AOT=0",
"-DWASM_ENABLE_FAST_INTERP=0",
"-DWASM_ENABLE_LIBC_BUILTIN=0",
"-DWASM_ENABLE_LIBC_WASI=0",
"-DWASM_ENABLE_MULTI_MODULE=0",
"-DWASM_ENABLE_BULK_MEMORY=1",
"-DWASM_ENABLE_REF_TYPES=1",
"-DWASM_ENABLE_SIMD=0",
"-DWASM_ENABLE_TAIL_CALL=1",
"-DWASM_ENABLE_MEMORY64=0",
"-DWASM_ENABLE_GC=0",
"-DWASM_ENABLE_THREAD_MGR=0",
"-DWASM_ENABLE_SHARED_MEMORY=0",
"-DWASM_ENABLE_EXCE_HANDLING=0",
"-DWASM_ENABLE_MINI_LOADER=0",
"-DWASM_ENABLE_WAMR_COMPILER=0",
"-DWASM_ENABLE_JIT=0",
"-DWASM_ENABLE_FAST_JIT=0",
"-DWASM_ENABLE_DEBUG_INTERP=0",
"-DWASM_ENABLE_INSTRUCTION_METERING=1",
"-DWASM_ENABLE_DUMP_CALL_STACK=0",
"-DWASM_ENABLE_PERF_PROFILING=0",
"-DWASM_ENABLE_LOAD_CUSTOM_SECTION=0",
"-DWASM_ENABLE_CUSTOM_NAME_SECTION=1",
"-DWASM_ENABLE_GLOBAL_HEAP_POOL=0",
"-DWASM_ENABLE_SPEC_TEST=0",
"-DWASM_ENABLE_LABELS_AS_VALUES=1",
"-DWASM_ENABLE_WASM_CACHE=0",
"-DWASM_ENABLE_STRINGREF=0",
"-DWASM_MEM_ALLOC_WITH_SYSTEM_ALLOCATOR=1",
"-DWASM_RUNTIME_API_EXTERN=",
"-DBH_MALLOC=wasm_runtime_malloc",
"-DBH_FREE=wasm_runtime_free",
"-I#{@c_src_dir}",
"-I#{@c_src_dir}/wamr/include",
"-I#{@c_src_dir}/wamr/interpreter",
"-I#{@c_src_dir}/wamr/common",
"-I#{@c_src_dir}/wamr/shared/utils",
"-I#{@c_src_dir}/wamr/shared/platform/include",
"-I#{@c_src_dir}/wamr/shared/mem-alloc",
"-I#{@c_src_dir}/wamr/shared/platform/#{if(:os.type() == {:unix, :darwin}, do: "darwin", else: "linux")}"
]

@wamr_src (Path.wildcard("priv/c_src/wamr/interpreter/wasm_loader.c") ++
Path.wildcard("priv/c_src/wamr/interpreter/wasm_interp_classic.c") ++
Path.wildcard("priv/c_src/wamr/interpreter/wasm_runtime.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_runtime_common.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_exec_env.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_memory.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_native.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_application.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_loader_common.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_blocking_op.c") ++
Path.wildcard("priv/c_src/wamr/common/wasm_c_api.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_assert.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_common.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_hashmap.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_leb128.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_list.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_log.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_queue.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_vector.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/bh_bitmap.c") ++
Path.wildcard("priv/c_src/wamr/shared/utils/runtime_timer.c") ++
Path.wildcard("priv/c_src/wamr/shared/mem-alloc/mem_alloc.c") ++
Path.wildcard("priv/c_src/wamr/shared/mem-alloc/ems/*.c") ++
Path.wildcard("priv/c_src/wamr/shared/platform/common/posix/posix_malloc.c") ++
Path.wildcard("priv/c_src/wamr/shared/platform/common/posix/posix_memmap.c") ++
Path.wildcard("priv/c_src/wamr/shared/platform/common/posix/posix_thread.c") ++
Path.wildcard("priv/c_src/wamr/shared/platform/common/posix/posix_time.c") ++
Path.wildcard("priv/c_src/wamr/shared/platform/common/posix/posix_blocking_op.c") ++
[
if(:os.type() == {:unix, :darwin},
do: "priv/c_src/wamr/shared/platform/darwin/platform_init.c",
else: "priv/c_src/wamr/shared/platform/linux/platform_init.c"
)
] ++
["priv/c_src/wamr/common/arch/invokeNative_general.c"] ++
["priv/c_src/wamr/shared/platform/common/memory/mremap.c"] ++
["priv/c_src/wamr_bridge.c"])
|> Enum.sort()
|> Enum.map(fn path ->
{:priv, String.replace_prefix(path, "priv/", ""), @wamr_cflags}
end)

@quickjs_cflags if System.get_env("QUICKBEAM_UBSAN") == "1",
do: [
"-std=c11",
Expand All @@ -28,6 +117,13 @@ defmodule QuickBEAM.Native do
],
else: ["-std=c11", "-D_GNU_SOURCE"]

if System.get_env("QUICKBEAM_BUILD") in ["1", "true"] and
is_nil(System.get_env("ZIG_LOCAL_CACHE_DIR")) do
zig_local_cache_dir = Path.expand(Path.join(Mix.Project.build_path(), "zig-cache"))
File.mkdir_p!(zig_local_cache_dir)
System.put_env("ZIG_LOCAL_CACHE_DIR", zig_local_cache_dir)
end

use ZiglerPrecompiled,
otp_app: :quickbeam,
base_url: "https://github.com/elixir-volt/quickbeam/releases/download/v#{@version}",
Expand All @@ -39,7 +135,13 @@ defmodule QuickBEAM.Native do
c: [
include_dirs: [
{:priv, "c_src"},
{:priv, "c_src/lexbor/ports/posix"}
{:priv, "c_src/lexbor/ports/posix"},
{:priv, "c_src/wamr/include"},
{:priv, "c_src/wamr/interpreter"},
{:priv, "c_src/wamr/common"},
{:priv, "c_src/wamr/shared/utils"},
{:priv, "c_src/wamr/shared/platform/include"},
{:priv, "c_src/wamr/shared/mem-alloc"}
],
src:
[
Expand All @@ -48,9 +150,9 @@ defmodule QuickBEAM.Native do
{:priv, "c_src/libunicode.c", @quickjs_cflags},
{:priv, "c_src/dtoa.c", @quickjs_cflags},
{:priv, "c_src/lexbor_bridge.c", @lexbor_cflags}
] ++ @lexbor_src
] ++ @lexbor_src ++ @wamr_src
],
resources: [:RuntimeResource, :PoolResource],
resources: [:RuntimeResource, :PoolResource, :WasmModuleResource, :WasmInstanceResource],
nifs: [
eval: 3,
compile: 2,
Expand Down Expand Up @@ -95,6 +197,17 @@ defmodule QuickBEAM.Native do
pool_dom_text: 3,
pool_dom_html: 2,
disasm_bytecode: 1,
load_addon: 3
load_addon: 3,
wasm_compile: 1,
wasm_start: 3,
wasm_start_with_imports: 5,
wasm_stop: 1,
wasm_call: 3,
wasm_memory_size: 1,
wasm_memory_grow: 2,
wasm_read_memory: 3,
wasm_write_memory: 3,
wasm_read_global: 2,
wasm_write_global: 3
]
end
20 changes: 20 additions & 0 deletions lib/quickbeam/quickbeam.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ const worker = @import("worker.zig");
const ct = @import("context_types.zig");
const context_worker = @import("context_worker.zig");
pub const napi = @import("napi.zig");
pub const wasm_nif = @import("wasm_nif.zig");
pub const WasmModuleResource = wasm_nif.WasmModuleResource;
pub const WasmInstanceResource = wasm_nif.WasmInstanceResource;
pub const wasm_compile = wasm_nif.wasm_compile;
pub const wasm_start = wasm_nif.wasm_start;
pub fn wasm_start_with_imports(mod_res: WasmModuleResource, runtime_res: RuntimeResource, imports: beam.term, stack_size: u32, heap_size: u32) beam.term {
return wasm_nif.wasm_start_with_imports_internal(mod_res, runtime_res.unpack(), imports, stack_size, heap_size);
}
pub const wasm_stop = wasm_nif.wasm_stop;
pub const wasm_call = wasm_nif.wasm_call;
pub const wasm_memory_size = wasm_nif.wasm_memory_size;
pub const wasm_memory_grow = wasm_nif.wasm_memory_grow;
pub const wasm_read_memory = wasm_nif.wasm_read_memory;
pub const wasm_write_memory = wasm_nif.wasm_write_memory;
pub const wasm_read_global = wasm_nif.wasm_read_global;
pub const wasm_write_global = wasm_nif.wasm_write_global;

export fn quickbeam_wasm_host_invoke_js(runtime_data: ?*anyopaque, callback_name_z: [*:0]const u8, signature_z: [*:0]const u8, raw_args: [*]u64, err_buf: [*]u8, err_buf_size: u32) bool {
return worker.quickbeam_wasm_host_invoke_js_impl(runtime_data, callback_name_z, signature_z, raw_args, err_buf, err_buf_size);
}

const std = types.std;
const beam = @import("beam");
Expand Down
Loading
Loading