WebAssembly Backend
This page is about the development of the WebAssembly backend in GCC.
Support status
Since the backend is still early days, there are features that are currently missing:
- Data sections (assembler lacks support for properties on sections)
- Debug info (assembler lacks support for code labels)
WebAssembly reference types
WebAssembly tables
- exceptions (blocked on structurization)
- Labels-as-values (blocked on structurization)
- structurization (currently all control flow is compiled to a br_table within a loop)
setjump/longjump (blocked on exceptions)
Building
Since the backend currently only includes the compiler, we would need a couple of external dependencies to get ourselves a working toolchain:
gcc itself1 (https://forge.sourceware.org/gcc/gcc-TEST/pulls/160)
wasi-libc (https://github.com/WebAssembly/wasi-libc)
wasm-ld (https://github.com/llvm/llvm-project/tree/main/lld)
To build wasi-libc use the following configuration:
cmake \ -S <path to source>\ -B <path to build>\ -DCMAKE_INSTALL_PREFIX=<path to install>\ -DCMAKE_C_COMPILER=clang\ -DCMAKE_AR=llvm-ar\ -DTARGET_TRIPLE=wasm32-wasi\ -DMALLOC=dlmalloc\ -DSETJMP=OFF\ -DBUILD_TESTS=OFF\ -DBUILD_SHARED=OFF
After obtaining and building the dependencies, use this command to configure your build:
RANLIB_FOR_TARGET=$(which ranlib)\ AR_FOR_BUILD=$(which ar)\ AR_FOR_TARGET=$(which ar)\ ../configure\ --with-sysroot=<path to wasi-libc install>\ --with-as=<path to wat2wasm binary from WABT>\ --with-ld=<path to wasm-ld>\ --with-native-system-header-dir=/include/wasm32-wasi\ --libdir=/lib/wasm32-wasi\ --target=wasm32\ --enable-languages=c\ --disable-multilib\ --disable-gcov\ --disable-threads\ --disable-bootstrap\ --disable-decimal-float\ --disable-fixed-point\
After configuration, use make all-target-libgcc to build.
Testing should work normally, provided you set up DejaGnu with a WASI-capable WebAssembly runtime.
Implementation annoyances
This section describes various idiosyncrasies of WebAssembly that make porting difficult and workarounds that are present in the port to deal with them.
Structured control flow
GCC expects a target to have control flow instructions that transfer control to labels or addresses in memory; WebAssembly does not provide those. WebAssembly function is a tree of nested control flow constructs — loops, conditionals, blocks, exception handlers.
The current workaround to this issue is to emit a function as a loop that contains a switch statement, and within that statement emit RTL basic blocks, and emit labels as switch arms. Jumps are therefore implemented as a write to the control flow variable and then a loop repetition.
This is suboptimal, but it's the only option available until there is way to convert RTL into a control flow construct tree representation that reflects WebAssembly.
https://github.com/WebAssembly/design/issues/796 is the proposal of adding goto and labels to WebAssembly.
i32/i64 and lack of register allocation
WebAssembly has two disjoint infinite sets of integer registers: i32 and i64. Operations only happen either between all i32 registers or all i64 registers.
Under normal circumstances, this isn't a problem: just define a register class for i32 and i64, assign appropriate constraints to operations, and let the register allocator handle the rest.
Unfortunately, register allocator is only designed to allocate a fixed set of registers, and cannot cope with an arbitrary amount of allocatable registers that is required at least by calling convention (all scalars in wasm are passed via registers, no matter the amount). For that reason, WebAssemblyBackend, much like nvptx, has to disable IRA, LRA, as well as all other passes that rely on a fixed register set.
The current workaround to this issue is very ugly: we basically need to perform the work normally done by register classes and constraints by register modes and predicates.
More specifically, replace every occurrence of register_operand by subregister_for_{s,d}i_operand, to specify which actual registers we expect for the operation. subregister_for_{s,d}i_operand does the same thing as register_operand, except that if the operand is a subreg expression, it looks through to the underlying register, and checks if it's DI (i64) or lower (i32).
During expansion, registers that don't match the predicate are converted to the appropriate mode via conversion instructions.
This is of course very crude: since only mode determines which register set the value gets allocated to, there is no option to store a value of small width in a large register if it's usually accessed via "crossing subregs". Likewise it's hard to coalesce accesses to a DImode register via subregs.
Proper register allocation would also be useful here to compress the set of declared registers for a function. Currently we use more registers than necessary, which bloats the binary in two ways: it generates needless register declarations, and also increases the average length of a register access instructions (since the index of the register to access has variable length encoding).
Unprototyped function calls
C89 supports forward-declaring functions without specifying their types of parameters, like so:
int foo();
int main() {
return foo(1, 2);
}This is supposed compile and work, assuming that there is a definition of foo the signature of which matches.
This is a problem for WebAssembly, though: it requires all calls to have a pre-defined signature. Any direct calls need to reference a prototyped function declaration (either imported or defined), and any indirect ones must specify a function type at the call site.
We cannot infer a protoyped declaration from a callsite. Consider the following example:
extern int tag, foo();
int main() {
return tag ? foo() : foo(1);
}Any of the following translation units will yield a well-defined program when linked against the prior one:
int tag = 0;
int foo(int) { return 0; }or
int tag = 1;
int foo(void) { return 0; }The problem can be solved, however, if we pretend that any unprototyped function call is actually a dynamic one; then we can supply the signature we call with at call-site, and the interpereter will check if they match during execution of the program.