compile to wasm
let's use this code as an example how to compile low level c to webassembly.
by low level, i mean programs that use no or very little libraries calls, which includes libc.
webassembly separates code and data. code is static. data lives in a single block of memory, called linear memory.
in wasm32 memory is indexed by an int32. what was a pointer in c is an index (int32) in wasm.
wasm can be written without using stack memory, only heap for application data.
that is what i usually do when writing webassembly directly or with a custom compiler.
the reason that can be done, is that functions can use an unlimited number of local variables and may include deep expression trees.
mapping these to registers or when to spill is not of wasm's concern, but propagated to the virtual machine implementation, often a jit compiler.
c however, often uses stack memory, e.g. to pass a reference of a local variable or to allocate a local array.
that's why c compilers, such as clang targetting wasm, use some space within the linear memory section for the c stack.
clang compiled programs use the following memory layout and define some internal variables that can be accessed (or exported):
| data | <--- stack | heap ---> |
0 __data_end __heap_base max(can be grown at runtime)
for details see surma's article.
this is often not necessary to know, but we use it to implement malloc
in a.c the main function allocates a local input buffer and continues with a read-eval-print-loop.
for wasm on a webpage read would block the ui leaving the page unresponsive. we need somthing else.
instead of entering with main, we allocate the input buffer in js and call ws(s) directly with the input string when the terminal receives a return key.
let's compile a.c from arthur unmodified using clang:
PATH="$PATH:/usr/lib/llvm-13/bin" # i need this for clang to find the linker:
clang-13 -Os --target=wasm32 -mbulk-memory \
-Wl,--export=w_,--export=e,--export=__heap_base,-allow-undefined \
--no-standard-libraries -Wl,--no-entry -oa.wasm a.c #a.wasm is 2065 bytes
we export the functions explicitly, that we need to have available within js and allow undefined function references.
what the compiler cannot resolve will be an import to the wasm module. it needs to be provided by js, otherwise the module cannot be instantiated.
let's check what clang came up with, converting the wasm binary to text format: wasm2wat is part of wabt.
there are binaries to download, or it may be available as a package for your system.
$wasm2wat a.wasm -o a.wat
$head -15 a.wat
(module
(type (;0;) (func (param i32) (result i32)))
(type (;1;) (func (param i32 i32) (result i32)))
(type (;2;) (func (param i32 i32 i32) (result i32)))
(type (;3;) (func (param i32)))
(type (;4;) (func (param i32 i32)))
(import "env" "strlen" (func (;0;) (type 0)))
(import "env" "write" (func (;1;) (type 2)))
(import "env" "sprintf" (func (;2;) (type 2)))
(import "env" "malloc" (func (;3;) (type 0)))
(import "env" "strchr" (func (;4;) (type 1)))
(func (;5;) (type 3) (param i32)
(local i32)
global.get 0
i32.const 16
$grep export a.wat
(export "memory" (memory 0))
(export "w_" (func 6))
(export "e" (func 20))
(export "__heap_base" (global 1))
we see that the module expects strlen write sprintf malloc strchr as imports and exports memory w_ e __heap_base.
from the function type index you can conclude which parameters each function needs and returns.
int32 can be a js number (int64 would need to be a BigInt, e.g. 0n).
malloc
a.c uses malloc but never frees. that means we can implement it using a simple bump allocator.
initially the heap starts at __heap_base upto max memory which we can query in the browser console (press F12):
K.memory.buffer.byteLength //131072 two block of wasm memory (a block is 64kb)
K.__heap_base //66784 (global variable exported from wasm)
to access memory, e.g. read from or write into we need a array view:
c=new Uint8Array(K.memory.buffer)
this way we can also slice the data part of vector, if we know where it starts and how long it is, to convert data between js and wasm.
for other types just use Float64Array or Int32Array using the same underlying buffer.
however if we grow the linear memory section
K.memory.grow(4) //grow by 4*64kb, it returns the old number of blocks
the underlying memory is copied (similar to realloc) and the array view must be recreated (otherwise it is detached).
js source