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_forkcan copy it into the child and resume both halves independently — parent and child returning from the same point. That is real Unixfork(), 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 theHwjsCoroutinizepass 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:clangis 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
HwjsCoroutinizepass. 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/wasm32tree 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 exportedwasm_syscallimport, so C code that callsread()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 pinnedclangandwasm-ldthat set the wasm32 target, enable the multi-memory, atomics, bulk-memory, exception-handling, and reference-types proposals, point-isystemat our musl headers, run theHwjsCoroutinizepass at link, importenv.kernel_memoryandenv.user_memoryas 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 forcc/ldin any build system that respectsCC. They ship inbrintos/hwjs-ccalongside the pass:toolchain/build.shbuilds the pass and wires up the drivers against the pinned LLVM fork, which you build once first (itsbuild-brintos.shcarries 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 anddocs/BUILDING.mdin 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.