Skip to content

Commit a8f3fb6

Browse files
authored
Merge pull request #3 from elixir-volt/wasm
WebAssembly support via WAMR interpreter
2 parents f86afb9 + b143d32 commit a8f3fb6

270 files changed

Lines changed: 127830 additions & 214 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/wasm-js-api-roadmap.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# WebAssembly JS API roadmap
2+
3+
## Goal
4+
5+
Bring QuickBEAM's `WebAssembly` polyfill closer to the WebAssembly JavaScript Interface standard.
6+
7+
## Standards checked
8+
9+
- https://webassembly.github.io/spec/js-api/
10+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface
11+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/instantiate_static
12+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Module
13+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Memory
14+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Table
15+
- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/JavaScript_interface/Global
16+
17+
## Current status
18+
19+
### Implemented
20+
21+
- `WebAssembly.compile(bytes)`
22+
- `WebAssembly.instantiate(bytes | module)`
23+
- `WebAssembly.validate(bytes)`
24+
- `new WebAssembly.Module(bytes)`
25+
- `new WebAssembly.Instance(module)`
26+
- `WebAssembly.Module.exports(module)`
27+
- `WebAssembly.Module.imports(module)`
28+
- numeric wasm calls for `i32`, `i64`, `f32`, `f64`
29+
- `i64` results mapped to JS `BigInt`
30+
- exported numeric globals
31+
- exported memory exposure
32+
- `WebAssembly.Module.customSections()`
33+
- `WebAssembly.compileStreaming()`
34+
- `WebAssembly.instantiateStreaming()`
35+
- `importObject` validation for function/memory/global imports
36+
- JS-owned function imports executed inline on the owning QuickJS worker / ContextPool thread
37+
- snapshot-style memory/global imports for instantiation
38+
- exported imported memory reuses the original `WebAssembly.Memory` wrapper
39+
40+
### Not yet standard-complete
41+
42+
- runtime-backed `Memory.buffer` semantics
43+
- runtime-backed tables
44+
- full global import/export parity
45+
- table imports
46+
- live shared imported memory/global semantics
47+
- compile options (`builtins`, `importedStringConstants`)
48+
- `Tag`, `Exception`, `JSTag`
49+
- exact object caching / identity semantics from the spec
50+
- exact error semantics for every edge case
51+
52+
## Implementation phases
53+
54+
### Phase 1 — Instantiation and linking
55+
56+
1. harden function imports around shared budget / instruction limits
57+
2. implement memory imports
58+
3. implement table imports
59+
4. implement global imports
60+
5. validate `importObject` shape and types
61+
6. return `LinkError` and `TypeError` in the right places
62+
63+
### Phase 2 — Memory
64+
65+
1. make exported memory runtime-backed
66+
2. make imported memory visible to wasm
67+
3. improve `buffer` semantics
68+
4. improve `grow()` semantics
69+
70+
### Phase 3 — Table
71+
72+
1. make exported tables runtime-backed
73+
2. make imported tables runtime-backed
74+
3. implement shared JS/wasm mutation visibility
75+
76+
### Phase 4 — Global
77+
78+
1. imported globals
79+
2. exported globals with shared state
80+
3. mutability checks
81+
4. `i64` globals as `BigInt`
82+
83+
### Phase 5 — Namespace completeness
84+
85+
1. `compileStreaming()`
86+
2. `instantiateStreaming()`
87+
3. `Module.customSections()`
88+
89+
### Phase 6 — JS API 2.0 / newer features
90+
91+
1. compile options
92+
2. `WebAssembly.JSTag`
93+
3. `WebAssembly.Tag`
94+
4. `WebAssembly.Exception`
95+
96+
## Recommended order
97+
98+
1. imports
99+
2. real memory semantics
100+
3. real tables
101+
4. real globals
102+
5. streaming and custom sections
103+
6. newer API additions

lib/quickbeam/application.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule QuickBEAM.Application do
1010
id: :quickbeam_pg,
1111
start: {:pg, :start_link, [QuickBEAM.BroadcastChannel]}
1212
},
13-
QuickBEAM.LockManager
13+
QuickBEAM.LockManager,
14+
QuickBEAM.WasmAPI
1415
]
1516

1617
QuickBEAM.Storage.init()

lib/quickbeam/beam_call.zig

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ const worker = @import("worker.zig");
33
const js = @import("js_helpers.zig");
44
const beam_to_js = @import("beam_to_js.zig");
55
const js_to_beam = @import("js_to_beam.zig");
6+
const beam_helpers = @import("beam_helpers.zig");
7+
const get_local_pid = beam_helpers.get_local_pid;
8+
const inspect_binary = beam_helpers.inspect_binary;
69
const std = types.std;
710
const beam = types.beam;
811
const e = types.e;
@@ -38,8 +41,7 @@ fn beam_call_impl(
3841
return qjs.JS_ThrowTypeError(ctx, "Beam.call: first argument must be a string");
3942
const name = std.mem.span(name_ptr);
4043

41-
// SAFETY: resolve_funcs immediately filled by JS_NewPromiseCapability
42-
var resolve_funcs: [2]qjs.JSValue = undefined;
44+
var resolve_funcs = std.mem.zeroes([2]qjs.JSValue);
4345
const promise = qjs.JS_NewPromiseCapability(ctx, &resolve_funcs);
4446
if (js.js_is_exception(promise)) {
4547
qjs.JS_FreeCString(ctx, name_ptr);
@@ -133,13 +135,13 @@ fn beam_call_sync_impl(
133135
return beam_to_js.convert(ctx.?, result_env, slot.result_term.?);
134136
} else {
135137
// Extract error reason string from the term
136-
// SAFETY: immediately filled by enif_inspect_binary
137-
var bin: e.ErlNifBinary = undefined;
138-
if (e.enif_inspect_binary(result_env, slot.result_term.?, &bin) != 0 and bin.size > 0) {
139-
const msg = gpa.dupeZ(u8, bin.data[0..bin.size]) catch
140-
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
141-
defer gpa.free(msg);
142-
return qjs.JS_ThrowInternalError(ctx, msg.ptr);
138+
if (inspect_binary(result_env, slot.result_term.?)) |bin| {
139+
if (bin.size > 0) {
140+
const msg = gpa.dupeZ(u8, bin.data[0..bin.size]) catch
141+
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
142+
defer gpa.free(msg);
143+
return qjs.JS_ThrowInternalError(ctx, msg.ptr);
144+
}
143145
}
144146
return qjs.JS_ThrowInternalError(ctx, "Beam.callSync failed");
145147
}
@@ -161,14 +163,13 @@ fn beam_send_impl(
161163
const pid_term = js_to_beam.convert(ctx.?, argv[0], send_env);
162164
const msg_term = js_to_beam.convert(ctx.?, argv[1], send_env);
163165

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

171-
_ = e.enif_send(null, &pid, send_env, msg_term);
171+
var send_pid = pid;
172+
_ = e.enif_send(null, &send_pid, send_env, msg_term);
172173
beam.free_env(send_env);
173174
return js.js_true();
174175
}

lib/quickbeam/beam_helpers.zig

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const types = @import("types.zig");
2+
const beam = @import("beam");
3+
4+
const std = types.std;
5+
const e = types.e;
6+
7+
pub const ListCell = struct {
8+
head: e.ErlNifTerm,
9+
tail: e.ErlNifTerm,
10+
};
11+
12+
pub const MapPair = struct {
13+
key: e.ErlNifTerm,
14+
value: e.ErlNifTerm,
15+
};
16+
17+
pub const NewBinary = struct {
18+
term: e.ErlNifTerm,
19+
data: [*c]u8,
20+
};
21+
22+
pub fn caller_pid(env: *e.ErlNifEnv) beam.pid {
23+
var pid = std.mem.zeroes(beam.pid);
24+
_ = e.enif_self(env, &pid);
25+
return pid;
26+
}
27+
28+
pub fn existing_atom(env: *e.ErlNifEnv, name: []const u8) ?e.ErlNifTerm {
29+
var atom = std.mem.zeroes(e.ErlNifTerm);
30+
if (e.enif_make_existing_atom_len(env, name.ptr, name.len, &atom, e.ERL_NIF_LATIN1) == 0) return null;
31+
return atom;
32+
}
33+
34+
pub fn map_value(env: *e.ErlNifEnv, map: e.ErlNifTerm, key: e.ErlNifTerm) ?e.ErlNifTerm {
35+
var value = std.mem.zeroes(e.ErlNifTerm);
36+
if (e.enif_get_map_value(env, map, key, &value) == 0) return null;
37+
return value;
38+
}
39+
40+
pub fn map_uint(env: *e.ErlNifEnv, map: e.ErlNifTerm, key: []const u8) ?u64 {
41+
const key_term = existing_atom(env, key) orelse return null;
42+
const value = map_value(env, map, key_term) orelse return null;
43+
return get_uint64(env, value);
44+
}
45+
46+
pub fn get_uint64(env: *e.ErlNifEnv, term: e.ErlNifTerm) ?u64 {
47+
var value: u64 = 0;
48+
if (e.enif_get_uint64(env, term, &value) == 0) return null;
49+
return value;
50+
}
51+
52+
pub fn inspect_binary(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?e.ErlNifBinary {
53+
var bin = std.mem.zeroes(e.ErlNifBinary);
54+
if (e.enif_inspect_binary(env, term, &bin) == 0) return null;
55+
return bin;
56+
}
57+
58+
pub fn alloc_binary(size: usize) ?e.ErlNifBinary {
59+
var bin = std.mem.zeroes(e.ErlNifBinary);
60+
if (e.enif_alloc_binary(size, &bin) == 0) return null;
61+
return bin;
62+
}
63+
64+
pub fn make_new_binary(env: ?*e.ErlNifEnv, size: usize) ?NewBinary {
65+
var term = std.mem.zeroes(e.ErlNifTerm);
66+
const data = e.enif_make_new_binary(env, size, &term) orelse return null;
67+
return .{ .term = term, .data = data };
68+
}
69+
70+
pub fn term_to_binary(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?e.ErlNifBinary {
71+
var bin = std.mem.zeroes(e.ErlNifBinary);
72+
if (e.enif_term_to_binary(env, term, &bin) == 0) return null;
73+
return bin;
74+
}
75+
76+
pub fn binary_to_term(env: ?*e.ErlNifEnv, data: [*c]u8, size: usize) ?e.ErlNifTerm {
77+
var term = std.mem.zeroes(e.ErlNifTerm);
78+
if (e.enif_binary_to_term(env, data, size, &term, 0) == 0) return null;
79+
return term;
80+
}
81+
82+
pub fn get_list_cell(env: ?*e.ErlNifEnv, list: e.ErlNifTerm) ?ListCell {
83+
var head = std.mem.zeroes(e.ErlNifTerm);
84+
var tail = std.mem.zeroes(e.ErlNifTerm);
85+
if (e.enif_get_list_cell(env, list, &head, &tail) == 0) return null;
86+
return .{ .head = head, .tail = tail };
87+
}
88+
89+
pub fn map_iterator_create(env: ?*e.ErlNifEnv, map: e.ErlNifTerm, entry: e.ErlNifMapIteratorEntry) ?e.ErlNifMapIterator {
90+
var iter = std.mem.zeroes(e.ErlNifMapIterator);
91+
if (e.enif_map_iterator_create(env, map, &iter, entry) == 0) return null;
92+
return iter;
93+
}
94+
95+
pub fn map_iterator_get_pair(env: ?*e.ErlNifEnv, iter: *e.ErlNifMapIterator) ?MapPair {
96+
var key = std.mem.zeroes(e.ErlNifTerm);
97+
var value = std.mem.zeroes(e.ErlNifTerm);
98+
if (e.enif_map_iterator_get_pair(env, iter, &key, &value) == 0) return null;
99+
return .{ .key = key, .value = value };
100+
}
101+
102+
pub fn get_local_pid(env: ?*e.ErlNifEnv, term: e.ErlNifTerm) ?beam.pid {
103+
var pid = std.mem.zeroes(beam.pid);
104+
if (e.enif_get_local_pid(env, term, &pid) == 0) return null;
105+
return pid;
106+
}
107+
108+
pub fn make_map_from_arrays(env: ?*e.ErlNifEnv, keys: ?[*]e.ErlNifTerm, values: ?[*]e.ErlNifTerm, count: usize) ?e.ErlNifTerm {
109+
var term = std.mem.zeroes(e.ErlNifTerm);
110+
if (e.enif_make_map_from_arrays(env, keys, values, count, &term) == 0) return null;
111+
return term;
112+
}

lib/quickbeam/beam_proxy.zig

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
const types = @import("types.zig");
22
const js = @import("js_helpers.zig");
33
const beam_to_js = @import("beam_to_js.zig");
4+
const beam_helpers = @import("beam_helpers.zig");
5+
const inspect_binary = beam_helpers.inspect_binary;
6+
const existing_atom = beam_helpers.existing_atom;
7+
const make_new_binary = beam_helpers.make_new_binary;
8+
const map_iterator_create = beam_helpers.map_iterator_create;
9+
const map_iterator_get_pair = beam_helpers.map_iterator_get_pair;
10+
const std = types.std;
411
const beam = types.beam;
512
const e = types.e;
613
const qjs = types.qjs;
@@ -18,10 +25,9 @@ fn lookupKey(data: *BeamProxyData, key: []const u8, result: *e.ErlNifTerm) bool
1825
// Use a scratch env so we don't mutate the proxy's env heap.
1926
const scratch = beam.alloc_env() orelse return false;
2027
defer beam.free_env(scratch);
21-
// SAFETY: enif_make_new_binary initializes bin_term before it is read.
22-
var bin_term: e.ErlNifTerm = undefined;
23-
const bin_ptr = e.enif_make_new_binary(scratch, key.len, &bin_term);
24-
if (bin_ptr != null) {
28+
if (make_new_binary(scratch, key.len)) |bin| {
29+
const bin_ptr = bin.data;
30+
const bin_term = bin.term;
2531
if (key.len > 0) {
2632
@memcpy(bin_ptr[0..key.len], key);
2733
}
@@ -32,9 +38,7 @@ fn lookupKey(data: *BeamProxyData, key: []const u8, result: *e.ErlNifTerm) bool
3238
}
3339

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

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

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

105-
// SAFETY: enif_map_iterator_create initializes iter on success before use.
106-
var iter: e.ErlNifMapIterator = undefined;
107-
if (e.enif_map_iterator_create(data.env, data.term, &iter, e.ERL_NIF_MAP_ITERATOR_FIRST) == 0) {
108+
var iter = map_iterator_create(data.env, data.term, e.ERL_NIF_MAP_ITERATOR_FIRST) orelse {
108109
qjs.js_free(ctx, raw);
109110
return -1;
110-
}
111+
};
111112
defer e.enif_map_iterator_destroy(data.env, &iter);
112113

113-
// SAFETY: enif_map_iterator_get_pair initializes key and val before use.
114-
var key: e.ErlNifTerm = undefined;
115-
// SAFETY: enif_map_iterator_get_pair initializes val before use.
116-
var val: e.ErlNifTerm = undefined;
117114
var idx: u32 = 0;
118115

119-
while (e.enif_map_iterator_get_pair(data.env, &iter, &key, &val) != 0) : ({
116+
while (map_iterator_get_pair(data.env, &iter)) |pair| : ({
120117
_ = e.enif_map_iterator_next(data.env, &iter);
121118
}) {
122119
var key_str: [256]u8 = undefined;
123120
var key_len: usize = 0;
124121

125-
// SAFETY: enif_inspect_binary initializes bin on success before use.
126-
var bin: e.ErlNifBinary = undefined;
127-
if (e.enif_inspect_binary(data.env, key, &bin) != 0) {
122+
if (inspect_binary(data.env, pair.key)) |bin| {
128123
const copy_len = @min(bin.size, key_str.len);
129124
if (copy_len > 0) {
130125
@memcpy(key_str[0..copy_len], bin.data[0..copy_len]);
131126
}
132127
key_len = copy_len;
133128
} else {
134-
const alen = e.enif_get_atom(data.env, key, &key_str, key_str.len, e.ERL_NIF_LATIN1);
129+
const alen = e.enif_get_atom(data.env, pair.key, &key_str, key_str.len, e.ERL_NIF_LATIN1);
135130
if (alen > 0) {
136131
key_len = @intCast(alen - 1);
137132
}
@@ -154,8 +149,7 @@ fn get_own_property_names(ctx: ?*qjs.JSContext, ptab: [*c][*c]qjs.JSPropertyEnum
154149
const okey = qjs.JS_AtomToCStringLen(ctx, &olen, otab[i].atom);
155150
if (okey != null) {
156151
defer qjs.JS_FreeCString(ctx, okey);
157-
// SAFETY: lookupKey writes dummy before any successful read.
158-
var dummy: e.ErlNifTerm = undefined;
152+
var dummy = std.mem.zeroes(e.ErlNifTerm);
159153
if (!lookupKey(data, okey[0..olen], &dummy)) {
160154
if (idx < total) {
161155
tab[idx] = .{
@@ -202,8 +196,7 @@ fn has_property(ctx: ?*qjs.JSContext, obj: qjs.JSValue, atom: qjs.JSAtom) callco
202196
if (key_ptr == null) return 0;
203197
defer qjs.JS_FreeCString(ctx, key_ptr);
204198

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

0 commit comments

Comments
 (0)