Skip to content
Merged
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
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
29 changes: 15 additions & 14 deletions lib/quickbeam/beam_call.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ const worker = @import("worker.zig");
const js = @import("js_helpers.zig");
const beam_to_js = @import("beam_to_js.zig");
const js_to_beam = @import("js_to_beam.zig");
const beam_helpers = @import("beam_helpers.zig");
const get_local_pid = beam_helpers.get_local_pid;
const inspect_binary = beam_helpers.inspect_binary;
const std = types.std;
const beam = types.beam;
const e = types.e;
Expand Down Expand Up @@ -38,8 +41,7 @@ fn beam_call_impl(
return qjs.JS_ThrowTypeError(ctx, "Beam.call: first argument must be a string");
const name = std.mem.span(name_ptr);

// SAFETY: resolve_funcs immediately filled by JS_NewPromiseCapability
var resolve_funcs: [2]qjs.JSValue = undefined;
var resolve_funcs = std.mem.zeroes([2]qjs.JSValue);
const promise = qjs.JS_NewPromiseCapability(ctx, &resolve_funcs);
if (js.js_is_exception(promise)) {
qjs.JS_FreeCString(ctx, name_ptr);
Expand Down Expand Up @@ -133,13 +135,13 @@ fn beam_call_sync_impl(
return beam_to_js.convert(ctx.?, result_env, slot.result_term.?);
} else {
// Extract error reason string from the term
// SAFETY: immediately filled by enif_inspect_binary
var bin: e.ErlNifBinary = undefined;
if (e.enif_inspect_binary(result_env, slot.result_term.?, &bin) != 0 and bin.size > 0) {
const msg = gpa.dupeZ(u8, bin.data[0..bin.size]) catch
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
defer gpa.free(msg);
return qjs.JS_ThrowInternalError(ctx, msg.ptr);
if (inspect_binary(result_env, slot.result_term.?)) |bin| {
if (bin.size > 0) {
const msg = gpa.dupeZ(u8, bin.data[0..bin.size]) catch
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
defer gpa.free(msg);
return qjs.JS_ThrowInternalError(ctx, msg.ptr);
}
}
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
}
Expand All @@ -161,14 +163,13 @@ fn beam_send_impl(
const pid_term = js_to_beam.convert(ctx.?, argv[0], send_env);
const msg_term = js_to_beam.convert(ctx.?, argv[1], send_env);

// SAFETY: pid immediately filled by enif_get_local_pid
var pid: beam.pid = undefined;
if (e.enif_get_local_pid(send_env, pid_term, &pid) == 0) {
const pid = get_local_pid(send_env, pid_term) orelse {
beam.free_env(send_env);
return qjs.JS_ThrowTypeError(ctx, "Beam.send: first argument must be a PID");
}
};

_ = e.enif_send(null, &pid, send_env, msg_term);
var send_pid = pid;
_ = e.enif_send(null, &send_pid, send_env, msg_term);
beam.free_env(send_env);
return js.js_true();
}
Expand Down
112 changes: 112 additions & 0 deletions lib/quickbeam/beam_helpers.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const types = @import("types.zig");
const beam = @import("beam");

const std = types.std;
const e = types.e;

pub const ListCell = struct {
head: e.ErlNifTerm,
tail: e.ErlNifTerm,
};

pub const MapPair = struct {
key: e.ErlNifTerm,
value: e.ErlNifTerm,
};

pub const NewBinary = struct {
term: e.ErlNifTerm,
data: [*c]u8,
};

pub fn caller_pid(env: *e.ErlNifEnv) beam.pid {
var pid = std.mem.zeroes(beam.pid);
_ = e.enif_self(env, &pid);
return pid;
}

pub fn existing_atom(env: *e.ErlNifEnv, name: []const u8) ?e.ErlNifTerm {
var atom = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_make_existing_atom_len(env, name.ptr, name.len, &atom, e.ERL_NIF_LATIN1) == 0) return null;
return atom;
}

pub fn map_value(env: *e.ErlNifEnv, map: e.ErlNifTerm, key: e.ErlNifTerm) ?e.ErlNifTerm {
var value = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_get_map_value(env, map, key, &value) == 0) return null;
return value;
}

pub fn map_uint(env: *e.ErlNifEnv, map: e.ErlNifTerm, key: []const u8) ?u64 {
const key_term = existing_atom(env, key) orelse return null;
const value = map_value(env, map, key_term) orelse return null;
return get_uint64(env, value);
}

pub fn get_uint64(env: *e.ErlNifEnv, term: e.ErlNifTerm) ?u64 {
var value: u64 = 0;
if (e.enif_get_uint64(env, term, &value) == 0) return null;
return value;
}

pub fn inspect_binary(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?e.ErlNifBinary {
var bin = std.mem.zeroes(e.ErlNifBinary);
if (e.enif_inspect_binary(env, term, &bin) == 0) return null;
return bin;
}

pub fn alloc_binary(size: usize) ?e.ErlNifBinary {
var bin = std.mem.zeroes(e.ErlNifBinary);
if (e.enif_alloc_binary(size, &bin) == 0) return null;
return bin;
}

pub fn make_new_binary(env: ?*e.ErlNifEnv, size: usize) ?NewBinary {
var term = std.mem.zeroes(e.ErlNifTerm);
const data = e.enif_make_new_binary(env, size, &term) orelse return null;
return .{ .term = term, .data = data };
}

pub fn term_to_binary(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?e.ErlNifBinary {
var bin = std.mem.zeroes(e.ErlNifBinary);
if (e.enif_term_to_binary(env, term, &bin) == 0) return null;
return bin;
}

pub fn binary_to_term(env: ?*e.ErlNifEnv, data: [*c]u8, size: usize) ?e.ErlNifTerm {
var term = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_binary_to_term(env, data, size, &term, 0) == 0) return null;
return term;
}

pub fn get_list_cell(env: ?*e.ErlNifEnv, list: e.ErlNifTerm) ?ListCell {
var head = std.mem.zeroes(e.ErlNifTerm);
var tail = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_get_list_cell(env, list, &head, &tail) == 0) return null;
return .{ .head = head, .tail = tail };
}

pub fn map_iterator_create(env: ?*e.ErlNifEnv, map: e.ErlNifTerm, entry: e.ErlNifMapIteratorEntry) ?e.ErlNifMapIterator {
var iter = std.mem.zeroes(e.ErlNifMapIterator);
if (e.enif_map_iterator_create(env, map, &iter, entry) == 0) return null;
return iter;
}

pub fn map_iterator_get_pair(env: ?*e.ErlNifEnv, iter: *e.ErlNifMapIterator) ?MapPair {
var key = std.mem.zeroes(e.ErlNifTerm);
var value = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_map_iterator_get_pair(env, iter, &key, &value) == 0) return null;
return .{ .key = key, .value = value };
}

pub fn get_local_pid(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?beam.pid {
var pid = std.mem.zeroes(beam.pid);
if (e.enif_get_local_pid(env, term, &pid) == 0) return null;
return pid;
}

pub fn make_map_from_arrays(env: ?*e.ErlNifEnv, keys: ?[*]e.ErlNifTerm, values: ?[*]e.ErlNifTerm, count: usize) ?e.ErlNifTerm {
var term = std.mem.zeroes(e.ErlNifTerm);
if (e.enif_make_map_from_arrays(env, keys, values, count, &term) == 0) return null;
return term;
}
45 changes: 19 additions & 26 deletions lib/quickbeam/beam_proxy.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
const types = @import("types.zig");
const js = @import("js_helpers.zig");
const beam_to_js = @import("beam_to_js.zig");
const beam_helpers = @import("beam_helpers.zig");
const inspect_binary = beam_helpers.inspect_binary;
const existing_atom = beam_helpers.existing_atom;
const make_new_binary = beam_helpers.make_new_binary;
const map_iterator_create = beam_helpers.map_iterator_create;
const map_iterator_get_pair = beam_helpers.map_iterator_get_pair;
const std = types.std;
const beam = types.beam;
const e = types.e;
const qjs = types.qjs;
Expand All @@ -18,10 +25,9 @@ fn lookupKey(data: *BeamProxyData, key: []const u8, result: *e.ErlNifTerm) bool
// Use a scratch env so we don't mutate the proxy's env heap.
const scratch = beam.alloc_env() orelse return false;
defer beam.free_env(scratch);
// SAFETY: enif_make_new_binary initializes bin_term before it is read.
var bin_term: e.ErlNifTerm = undefined;
const bin_ptr = e.enif_make_new_binary(scratch, key.len, &bin_term);
if (bin_ptr != null) {
if (make_new_binary(scratch, key.len)) |bin| {
const bin_ptr = bin.data;
const bin_term = bin.term;
if (key.len > 0) {
@memcpy(bin_ptr[0..key.len], key);
}
Expand All @@ -32,9 +38,7 @@ fn lookupKey(data: *BeamProxyData, key: []const u8, result: *e.ErlNifTerm) bool
}

// Try atom key
// SAFETY: enif_make_existing_atom_len initializes atom_key on success before use.
var atom_key: e.ErlNifTerm = undefined;
if (e.enif_make_existing_atom_len(data.env, key.ptr, key.len, &atom_key, e.ERL_NIF_LATIN1) != 0) {
if (existing_atom(data.env, key)) |atom_key| {
if (e.enif_get_map_value(data.env, data.term, atom_key, result) != 0) {
return true;
}
Expand Down Expand Up @@ -68,8 +72,7 @@ fn get_own_property(ctx: ?*qjs.JSContext, desc: ?*qjs.JSPropertyDescriptor, obj:
if (key_ptr == null) return 0;
defer qjs.JS_FreeCString(ctx, key_ptr);

// SAFETY: lookupKey writes result before any successful read.
var result: e.ErlNifTerm = undefined;
var result = std.mem.zeroes(e.ErlNifTerm);
if (!lookupKey(data, key_ptr[0..len], &result)) return 0;

if (desc) |d| {
Expand Down Expand Up @@ -102,36 +105,28 @@ fn get_own_property_names(ctx: ?*qjs.JSContext, ptab: [*c][*c]qjs.JSPropertyEnum
const raw = qjs.js_malloc(ctx, byte_size) orelse return -1;
const tab: [*]qjs.JSPropertyEnum = @ptrCast(@alignCast(raw));

// SAFETY: enif_map_iterator_create initializes iter on success before use.
var iter: e.ErlNifMapIterator = undefined;
if (e.enif_map_iterator_create(data.env, data.term, &iter, e.ERL_NIF_MAP_ITERATOR_FIRST) == 0) {
var iter = map_iterator_create(data.env, data.term, e.ERL_NIF_MAP_ITERATOR_FIRST) orelse {
qjs.js_free(ctx, raw);
return -1;
}
};
defer e.enif_map_iterator_destroy(data.env, &iter);

// SAFETY: enif_map_iterator_get_pair initializes key and val before use.
var key: e.ErlNifTerm = undefined;
// SAFETY: enif_map_iterator_get_pair initializes val before use.
var val: e.ErlNifTerm = undefined;
var idx: u32 = 0;

while (e.enif_map_iterator_get_pair(data.env, &iter, &key, &val) != 0) : ({
while (map_iterator_get_pair(data.env, &iter)) |pair| : ({
_ = e.enif_map_iterator_next(data.env, &iter);
}) {
var key_str: [256]u8 = undefined;
var key_len: usize = 0;

// SAFETY: enif_inspect_binary initializes bin on success before use.
var bin: e.ErlNifBinary = undefined;
if (e.enif_inspect_binary(data.env, key, &bin) != 0) {
if (inspect_binary(data.env, pair.key)) |bin| {
const copy_len = @min(bin.size, key_str.len);
if (copy_len > 0) {
@memcpy(key_str[0..copy_len], bin.data[0..copy_len]);
}
key_len = copy_len;
} else {
const alen = e.enif_get_atom(data.env, key, &key_str, key_str.len, e.ERL_NIF_LATIN1);
const alen = e.enif_get_atom(data.env, pair.key, &key_str, key_str.len, e.ERL_NIF_LATIN1);
if (alen > 0) {
key_len = @intCast(alen - 1);
}
Expand All @@ -154,8 +149,7 @@ fn get_own_property_names(ctx: ?*qjs.JSContext, ptab: [*c][*c]qjs.JSPropertyEnum
const okey = qjs.JS_AtomToCStringLen(ctx, &olen, otab[i].atom);
if (okey != null) {
defer qjs.JS_FreeCString(ctx, okey);
// SAFETY: lookupKey writes dummy before any successful read.
var dummy: e.ErlNifTerm = undefined;
var dummy = std.mem.zeroes(e.ErlNifTerm);
if (!lookupKey(data, okey[0..olen], &dummy)) {
if (idx < total) {
tab[idx] = .{
Expand Down Expand Up @@ -202,8 +196,7 @@ fn has_property(ctx: ?*qjs.JSContext, obj: qjs.JSValue, atom: qjs.JSAtom) callco
if (key_ptr == null) return 0;
defer qjs.JS_FreeCString(ctx, key_ptr);

// SAFETY: lookupKey writes result before any successful read.
var result: e.ErlNifTerm = undefined;
var result = std.mem.zeroes(e.ErlNifTerm);
return if (lookupKey(data, key_ptr[0..len], &result)) 1 else 0;
}

Expand Down
Loading
Loading