We use strings all the time in JavaScript. When we want to read/write strings with WebAssembly, we have a problem.
WebAssembly function parameter and return value types only support 32/64-bit integers/floats. WebAssembly has no concept of a string. Worse, WebAssembly doesn't even have the concept of an array.
What WebAssembly does have is a linear memory model. One can think of WebAssembly's memory as one large Uint8Array
that can only grow in size.
Back in JavaScript land, we can already read/ write strings to an Uint8Array
with TextEncoder
/ TextDecoder
.
Neat, if we encode/decode strings from WebAssembly's memory, we can pass the index and length via functions.
Sending a string from WebAssembly to JavaScript
Zig makes this easy as it already represents strings as an array of UTF-8 bytes, and when Zig is compiled to WebAssembly, Zig's memory is the same as WebAssembly's memory. Furthermore, Zig keeps track of string as slices. Slices are simply a pointer and length to a thing. A pointer to something in Zig's memory is the same as an index to WebAssembly's memory!
Zig defines a string and sends it to JavaScript:
Aside:
[*]const u8
means an slice of unknown length of unmodifiableu8
.[*]const u8
is compiled down to a pointer. Pointers in WebAssembly are just indices in memory which areu32
. Therefore, in this context[*]const u8
is the same asu32
.[*:0]u8
will be explained later.
Aside: Where is the question string allocated? Why don't we need to use an allocator? For compile time strings, Zig will statically allocate the string somewhere in memory for us. This is why
question.ptr
andquestion.len
are well-defined.
JavaScript receives the pointer and length, then decodes it out of memory:
What about encodeString
?
When we want to encode a string to WebAssembly memory, we run into another problem. Where in memory should we encode the string? We can't just write anywhere into memory, as Zig might already have data there.
What we need to do is ask Zig for space. We need to tell Zig to allocate a segment of memory for us.
Sending a string from JavaScript to Zig
For step 2, we need a function that allocates into WebAssembly memory. To do this, in Zig, we wrap alloc
and export it:
Aside:
std.heap.page_allocator
maps directly to WebAssembly memory.
Aside: Why is the function is named
allocUint8
? While it can allocate space for a string, it can be generally used to allocateu8
.
JavaScript encodes a string to memory and sends a pointer to Zig:
Why are we null-terminating the string? Two reasons.
First, strings in Zig are both null-terminated, and length tracked. This allows Zig to easily interop with APIs that expect null-terminated strings and, simultaneously, don't require O(N)
to figure out the length of a string.
Second, WebAssembly multi-value return is not implemented yet. This means we can't return [pointer, buffer.length]
in JavaScript. We can only return a single value, and a null-terminated string has an implicit length.
In Zig, we take the pointer and convert it back to a normal string:
Aside: We can now understand
[*:0]u8
to mean a null-terminated slice of unknown length ofu8
.
Since memory was allocated by JavaScript side, we need to deallocate it on the Zig side.
std.mem.span
scans the memory starting at the pointer until it finds a null byte and returns a normal string.
For a complete example see https://github.com/Pyrolistical/zig-wasm-canvas [demo]
Do you want to learn the latest web tech while building esports? You're in luck, Battlefy is hiring.