How to Build and Run a Simple WASI‑Enabled WebAssembly “cat” Program
This article explains why WebAssembly needs a standardized system interface, introduces the WebAssembly System Interface (WASI) for portable and secure execution, and provides a step‑by‑step tutorial for compiling a C "cat" program with wasi‑sdk and running it via Node.js.
Since WebAssembly was first released in 2017, it has gradually been adopted beyond browsers, allowing C and C++ code to be reused across environments with near‑native performance while remaining portable and secure. However, WebAssembly alone is just a binary instruction set and requires a virtual operating system interface to access system resources.
Projects like Emscripten expose a simulated POSIX layer to WebAssembly modules via JavaScript glue code, enabling the use of standard C libraries. When running WebAssembly outside the browser, these glue layers must be re‑implemented, leading to fragmented and non‑standard interfaces.
To provide a stable, portable, and secure foundation, the WebAssembly System Interface (WASI) standardizes system calls, emphasizing portability and capability‑based security, allowing hosts to expose only the resources a program should access.
Getting Started with WASI
We begin with a simple C program that mimics the cat command:
#include <stdio.h>
int main(int argc, char **argv) {
FILE* file = fopen(argv[0], "r");
char c = fgetc(file);
while (c != EOF) {
int wrote = fputc(c, stdout);
c = fgetc(file);
}
return 0;
}Compile the source to a WebAssembly binary using the wasi-sdk toolchain:
$ /path/to/wasi-sdk/bin/clang -target wasm32-wasi --sysroot /path/to/wasi-sdk/share/wasi-sysroot main.c -o main.wasmWe use wasi‑sdk v8.0; the SDK and Node.js support are still evolving.
The -target wasm32-wasi flag selects the WebAssembly target, --sysroot points to the SDK’s system libraries, and -o specifies the output file.
Running
Execute the compiled module with Node.js’s WASI implementation, configuring a sandboxed directory:
'use strict';
const { readFile } = require('fs').promises;
const { resolve } = require('path');
const { WASI } = require('wasi');
(async () => {
const wasi = new WASI({
args: [resolve('/sandbox', process.argv[2])],
preopens: { '/sandbox': __dirname }
});
const importObject = { wasi_unstable: wasi.wasiImport };
const wasm = await WebAssembly.compile(await readFile('./main.wasm'));
const instance = await WebAssembly.instantiate(wasm, importObject);
wasi.start(instance);
})();The program is given access only to the virtual /sandbox directory, which maps to the current folder. The WASI runtime calls the module’s exported _start function to begin execution.
Because WASI is still unstable, the Node.js flags --experimental-wasi-unstable-preview0 and --experimental-wasm-bigint are required for full functionality, especially when dealing with 64‑bit integers.
Running the example produces the expected output:
$ echo 'hello world!' > ./foo.txt
$ node --experimental-wasi-unstable-preview0 --experimental-wasm-bigint wasm.js ./foo.txt
hello world!This demonstrates a working WebAssembly “cat” program using WASI.
References
CraneStation – wasi‑sdk: https://github.com/CraneStation/wasi-sdk
Lin Clark – Standardizing WASI: https://hacks.mozilla.org/2019/03/standardizing-wasi-a-webassembly-system-interface/
Node.js Foundation – WASI API: https://nodejs.org/api/wasi.html
Node Underground
No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
