Platform

The brintOS toolchain

hwjs-cc is the compiler toolchain behind brintOS's in-browser Linux. It is a pinned LLVM build, an LLVM pass of our own — HwjsCoroutinize — and libc ports for wasm32 in both musl and glibc, supported equally. Together they compile ordinary C and C++ into wasm executables our kernel can fork, exec, preempt, and schedule like any other Linux binary.

What it is

To build software for the in-browser Linux machine you need four things: a C library that knows our kernel's syscall ABI, a compiler that targets WebAssembly with the right features, a linker that imports the kernel's shared memories and emits the container our binfmt_wasm loader recognizes, and — the hard part — a way to make a running wasm program suspendable, resumable, and forkable, because a real kernel has to be able to stop a task, switch to another, and duplicate one. hwjs-cc is all of that. Once the toolchain is built and wired up, wasmcc hello.c -o hello produces a wasm-native Linux executable, and whole autotools packages (bash, coreutils) build through it unmodified.

Be clear-eyed about what "built and wired up" means, though: this is a from-source toolchain, not a package-manager install. You build our pinned LLVM fork (stock or emsdk clang won't do — the pass must load into the exact compiler it was built against), build the HwjsCoroutinize pass and the drivers against it, and give the drivers a wasm32 libc — sysroot, coroutine startfiles, and the whole-program bitcode pool — through the documented environment interface. The honest end-to-end walkthrough is Install the toolchain in the developer guide.

It is not a custom WebAssembly runtime and it requires no non-standard wasm extensions. The browser's stock wasm engine — the same V8, SpiderMonkey, or JavaScriptCore your tab is already using — executes every binary we produce.

The hard part: compiler-native coroutines

WebAssembly has no instruction that suspends a running call stack, and that one gap is what makes "real Linux on wasm" hard. A kernel has to deschedule a task mid-execution, resume it later exactly where it left off, jump across stack frames for setjmp/longjmp, and copy a suspended task to implement fork(). Earlier attempts in this space reached for whole-program rewriters (Binaryen's Asyncify) or, later, the engine's one-shot stack-switching API (JSPI). We use neither. Both tax every build, compose badly with exception handling, and — in JSPI's case — can suspend a stack but cannot duplicate one, which is exactly what fork() needs.

Instead, our LLVM pass — HwjsCoroutinize — turns every function on a suspend-reachable call chain into a coroutine whose live state lives in ordinary linear memory as plain, copyable data. That single mechanism delivers everything at once:

  • Park and resume. A task that makes a blocking syscall has its continuation parked and the scheduler resumes another task — a real context switch, not cooperative politeness.
  • Deep setjmp/longjmp. Non-local jumps work at arbitrary depth and across frames, the way bash and friends expect.
  • Forkable continuations. Because a suspended stack is reified into the process's own linear memory, do_fork can copy it into the child and resume both halves independently — parent and child returning from the same point. That is real Unix fork(), on a platform whose native primitives refuse to duplicate a stack.
  • Bounded preemption. A per-task budget checked at coroutine suspend points lets the scheduler take a CPU back from a tight compute loop within a bounded number of iterations — so one runaway process can't freeze the machine.

The pass is gated by an extensive, non-vacuous test suite, and the runtime is "never-silent": a miscompile or a mismatched continuation is a loud, named trap at build, link, or run time — never a laundered wrong value.

What's in the toolchain

  • A pinned LLVM fork. Our fork of llvm-project (clang + wasm-ld, LLVM 22) that the HwjsCoroutinize pass plugin loads into: the upstream base plus the wasm32-hwjs patches the kernel and glibc builds need. Clang has no stable plugin ABI across releases, so we pin the exact compiler the pass was built against — there is no Emscripten and no WASI SDK in the picture. The toolchain is LLVM-only today: clang is the only supported compiler, because the coroutine transform is an LLVM pass. GCC's WebAssembly backend is still too limited to build the kernel or a libc this way — we're tracking its progress and expect to add a GCC path before long.
  • The HwjsCoroutinize pass. The out-of-tree LLVM pass — in the hwjs-cc repo — that does the coroutinization above, plus the native floating-point-cast and cross-module records that make dynamic loading work.
  • Two libc ports for wasm32 — musl and glibc, first-class and interchangeable. Each is a new arch/wasm32 tree inside an otherwise-stock upstream tree (musl 1.2, glibc 2.43). The arch-specific files implement our syscall ABI: every syscall wrapper emits a call to the kernel's exported wasm_syscall import, so C code that calls read() just gets bytes back, exactly as on any other Linux arch. Pick musl for small, static binaries or glibc for full GNU/Linux source compatibility — the kernel, the pass, and the drivers are identical either way. (A from-source glibc build under clang only became practical very recently: glibc 2.43, released January 2026, shipped experimental support for building with clang — the release we port from.)
  • wasmcc / wasmld. Thin wrappers around the pinned clang and wasm-ld that set the wasm32 target, enable the multi-memory, atomics, bulk-memory, exception-handling, and reference-types proposals, point -isystem at our musl headers, run the HwjsCoroutinize pass at link, import env.kernel_memory and env.user_memory as the multi-memory pair the kernel expects, and attach the small versioned binary header our kernel reads. Once the toolchain is set up they are drop-in replacements for cc/ld in any build system that respects CC. They ship in brintos/hwjs-cc alongside the pass: toolchain/build.sh builds the pass and wires up the drivers against the pinned LLVM fork, which you build once first (its build-brintos.sh carries the required dylib + plugin flags). The drivers then need to be pointed at a wasm32 libc — see Install the toolchain for the complete, honest recipe and docs/BUILDING.md in the repo for the authoritative environment interface.

How it compares

Two other C toolchains target WebAssembly for Linux-style applications, and hwjs-cc is deliberately neither.

Emscripten targets a JavaScript runtime. Its libc is implemented in JS, and "syscalls" go through a JS shim layer. Programs built with Emscripten run on the web but they don't run on Linux — they run on Emscripten.

wasi-libc targets the WebAssembly System Interface, a capability-based syscall API designed for serverless and embedded wasm runtimes. Programs built with wasi-libc run on WASI runtimes (Wasmtime, Wasmer, Node's WASI) but they don't run on Linux either — they run on WASI, which deliberately omits fork, exec, signals, and the real process model.

hwjs-cc targets our Linux kernel's syscall ABI. Programs built with it look like regular Linux executables to the kernel; they call real Linux syscalls; they get real EINTR, real SIGCHLD, real poll(2) semantics — because they actually are running on Linux. The difference is that Linux happens to be compiled to WebAssembly.

The output format

Binaries produced by hwjs-cc are standard wasm modules with one addition: a custom section carrying a small versioned binary header. The header tells the kernel the entry symbol, required stack and heap sizes, and a reserved capability bitmap. When the kernel runs execve, our binfmt_wasm loader reads this section and swaps the new image in — the same extensible mechanism Linux already uses to recognize and launch an ELF. Existing wasm tooling (wasm-objdump, wasm2wat, wasm-strip, wabt) operates on these binaries as ordinary wasm modules; the custom section is ignored by tools that don't understand it, per the wasm spec.

Static by default, dynamic when you need it

The minimal demo images static-link everything, which keeps each executable self-contained. But dynamic linking is real, and both libcs ride the same substrate: musl gains dlopen/dlsym over a cross-module coroutine ABI, and glibc runs its own real dynamic loader (ld.so) on top of it. A loaded .so registers its own continuation and relocation records into the live runtime, and setjmp/longjmp/fork compose across the module boundary.

Open source

The toolchain is open. musl is MIT-licensed and glibc is LGPL; our wasm32 arch ports are contributed back upstream where possible, and the wrapper drivers are MIT. The kernel-side binfmt_wasm loader is GPL because it's part of Linux. The binary-header format is documented as versioned ABI — any future toolchain that emits the same custom section will produce binaries our kernel runs.

Browse the source: the hwjs-cc compiler toolchain (the HwjsCoroutinize pass) and the pinned llvm-project it builds on.

If your team builds compilers, runtimes, or distros that target wasm-native Linux, get in touch. We're especially interested in upstream LLVM improvements that let us retire workarounds in our linker driver, patches that improve musl- and glibc-on-wasm32, and progress on GCC's WebAssembly backend.