I recently discovered Gynvael’s Summer GameDev Challenge, which has
a fairly tight limit on the total size for game submissions: 64KiB, all-in.
Submissions are advised to use JavaScript or WebAssembly, but as far as I was
aware Emscripten produces a fair amount of boilerplate in JavaScript to make the
wasm experience painless. I found out that clang
natively supports compiling
to webassembly without any help from Emscripten, so I decided to see if I could
get it working. The result is a trivial helloworld example using WebAssembly
without Emscripten.
To start with, we’ll look at the C code:
// write is imported from js.
void write(const char* message, int length);
// We have no standard library access, so we need to provide our own strlen
// and puts.
unsigned long strlen(const char* c_string) {
const char* i = c_string;
while (*i) i++;
return i - c_string;
}
void puts(const char* message) {
write(message, strlen(message));
}
// We need to export symbols for them to be accessible in JavaScript. This
// attribute is how we tell clang to export the symbol from the WebAssembly
// module.
#define export __attribute__((visibility("default")))
export int main() {
puts("Hello, World!\n");
}
Next, we need to turn this into a WebAssembly module. Here is the Makefile:
# clang is natively a cross compiler, yay! You might need to install wasm-ld,
# though. On ArchLinux, that just required `pacman -S lld`.
CC = clang --target=wasm32
# The standard library probably won't work out of the box, so we'll disable it.
CFLAGS = -nostdlib
# --no-entry Prevents issues due to there being no _start. We'll
# just call main directly.
# --export-dynamic This works alongside the export macro in the code to
# ensure that our exported functions are exposed in the
# WebAssembly.
# --import-memory This makes the module expect a "memory" import which
# has the initial linear memory for the module.
# --allow-undefined-file This is a file containing the names of every function
# which the compiler should allow to be undefined in the
# compiled WebAssembly module. These will have to be
# defined in JavaScript instead. Oddly, this seems to
# interact weirdly with -flto: enabling -flto makes
# things compile with undefined symbols even if they are
# not listed in this file.
LDFLAGS = -Wl,--no-entry -Wl,--export-dynamic -Wl,--import-memory \
-Wl,--allow-undefined-file=jsimport.txt
hello.wasm: hello.c
${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@
With this, we can build a binary blob hello.wasm
. We can make this much
smaller by adding the various optimization flags but I omitted those for
brevity. The final step is to load this module in JavaScript. I was expecting
this to be much harder than it actually is, and was pleasantly surprised with
what it actually took:
<!doctype html>
<meta charset=utf-8>
<script>
// WebAssembly memory is allocated in 64KiB chunks, so 2 means 128KiB.
const memory = new WebAssembly.Memory({initial: 2});
// This is the write() function we are using in the C code. Pointers get passed
// to javascript as offsets into the memory ArrayBuffer.
function write(message, length) {
const value = new Uint8Array(memory.buffer, message, length);
console.log(new TextDecoder().decode(value));
}
// The second argument to instantiateStreaming is how we provide the
// dependencies (imports) of the WebAssembly module. The compiler flags we used
// mean that we have to provide the initial memory, and we are using the
// undefined function write, so we need to provide its definition. If you forget
// to provide something which the module needs, it will fail to instantiate. You
// can enumerate all the dependencies for a WebAssembly module with:
//
// const module = WebAssembly.compileStreaming(fetch('hello.wasm'));
// for (const {kind, module, name} of WebAssembly.Module.imports(module)) {
// console.log("%s %s::%s", kind, module, name);
// }
//
// Here, kind is "function", "table", "memory", or "global". We will need to set
// importObject[module][name] to something to satisfy the requirement. In our
// case, we have env::memory and env::write which we need to provide.
//
// After instantiating the module, the promise yields an object through which we
// can access the exported symbols of the module via o.instance.exports.foo. We
// will just invoke the main() function.
WebAssembly.instantiateStreaming(fetch('hello.wasm'), {env: {memory, write}})
.then(o => o.instance.exports.main());
</script>
Altogether, with various compiler optimizations enabled and all the HTML minified, the hello world program is only 517 bytes.