DEV Community

Avinash Kori
Avinash Kori

Posted on

How I Run a Real C Compiler in the Browser with WebAssembly

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

  1. 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.
  2. 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.
  3. Compile to wasm32-wasi in memory. The worker runs clang -std=c17 -O2 -Wall main.c -lm against an in-memory file system and gets a main.wasm binary back.
  4. 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.
  5. 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 tight for (;;) 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);
Enter fullscreen mode Exit fullscreen mode

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) == 4 and sizeof(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 strcpy overflow 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.h doesn't exist, and gets() 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)