Most online compilers send your code to a server, run it in a container and send the output back. I wanted one where the code never leaves the browser. Here is how the C compiler on Devtoolry works, and what surprised me along the way.
The pieces
- Clang and LLD compiled to WebAssembly. I use the YoWASP build of Clang 21. It is a normal npm package served from jsDelivr, about 23 MB compressed. The browser caches it after the first run.
- A module Web Worker. Compiling and running happen off the main thread, so the page stays responsive and an infinite loop can't freeze the tab.
-
Compile to wasm32-wasi in memory. The worker runs
clang -std=c17 -O2 -Wall main.c -lmagainst an in-memory file system and gets amain.wasmbinary back. - A minimal WASI host. The program needs stdin, stdout, stderr, clocks and randomness. It gets no file system and no network, which makes the sandbox simple.
-
A timeout that kills the worker. The page starts a timer when the program starts running (compilation and the first download don't count). If it fires, the page calls
worker.terminate()and shows "Timed out". Terminating a worker is the only reliable way to stop a tightfor (;;)loop.
The core of the run step is short:
const program = await WebAssembly.compile(binary);
const wasi = createWasi({ args: ['main'], stdin, write: (fd, text) => emit(fd === 2 ? 'stderr' : 'stdout', text) });
const instance = await WebAssembly.instantiate(program, wasi.imports);
postMessage({ type: 'status', status: 'ready' }); // the page starts the time limit here
const exitCode = wasi.start(instance);
What behaves differently from native C
Testing with a lot of real beginner programs turned up some differences worth knowing:
-
32-bit sizes. wasm32 means
sizeof(long) == 4andsizeof(void *) == 4. - NULL reads don't crash. Address 0 is valid linear memory, so reading through a NULL pointer returns a value (0 in my tests) instead of a segfault. Out-of-bounds access past the end of memory does trap.
-
No fortified libc. A
strcpyoverflow that aborts on Ubuntu GCC silently corrupts memory here. Clang still warns at compile time ("'strcpy' will always overflow"). - No interactive stdin. Input has to be typed into a box before running, because WASI's stdin is a byte buffer here, not a terminal.
-
Turbo C headers.
conio.hdoesn't exist, andgets()is rejected because C11 removed it. These are the two most common reasons beginner code fails.
Making C++ fast enough
C++ was the slow one: parsing <iostream>, <vector> and friends took 5–9 seconds per compile in WebAssembly. A precompiled header of the common standard headers, built once with the exact same Clang version and downloaded in the background, cuts a typical compile to 1–3 seconds. As a bonus, the header list doubles as GCC's <bits/stdc++.h>, which competitive programmers expect.
Was it worth it?
For learning and quick tests, yes. There's no account and no queue, it works on a phone, and privacy comes for free. The trade-offs are the first download and the lack of threads, files and sockets.
If you want to dig into the C side, I wrote up pointers, strings and GCC vs Clang with output from both compilers. Questions about the worker or the WASI shim are welcome in the comments.
Top comments (0)