cc / gcc / clang — a C compiler that runs inside WASIX
cc, gcc and clang all run clang 23 as a WASIX module, with the WASIX
sysroot and compiler-rt builtins alongside it. It compiles and links in a
single process — no subprocesses, no fork, no asyncify.
cc hello.c -o hello.wasm # compile and link
clang -c hello.c -o hello.o # compile only
cc a.c b.c -o prog.wasm # several inputs, one binary
--sysroot=/sysroot is supplied by the packaged driver configuration
(/bin/clang.cfg), so no flags are needed for the normal case.
-c works
Every other wasm-hosted clang fails on compile-only invocations:
$ clang -c hello.c -o hello.o
error: unknown integrated tool '-cc1'. Valid tools include '-cc1' and '-cc1as'.
That is clang's integrated cc1 path mangling the tool name it passes to itself —
clang/clang, syrusakbary/clang and the clang 16 this package replaces all do
it. clang 23 built from the WASI portability patch does not:
$ clang -c hello.c -o hello.o
$ od -An -tx1 -N4 hello.o
00 61 73 6d # \0asm — a real object file
No -fno-integrated-cc1 workaround is needed.
Why the linker runs in-process
A C compiler has to run a frontend and a linker. WASIX gives a module no usable way to spawn either:
fork()never returns without asyncify, and the runtime kills the process (Noexec, exit 45).posix_spawn()returnsENOEXECand, when given a real path, never returns at all.wasm-opt --asyncifyis the documented answer forfork, but it is not viable here: binaryen 133 aborts in its liveness fixpoint (liveness-traversal.h:244) on a module this size, and every link would pay for a copy of the compiler's address space.
So this build removes the subprocesses instead:
| step | how it runs |
|---|---|
| cc1 (frontend) | in-process, always — the driver's "more than one job ⇒ spawn cc1" rule is disabled |
wasm-ld (link) | lld::wasm::link() called directly by the driver through llvm::sys::ExecuteInProcessHook |
Both are small, targeted patches (see patches/). The result is faster than the
forking design would have been: a link costs no address-space copy, and
cc hello.c -o hello.wasm finishes in about 1.5 s of compiler time.
What is in the package
| path | what it is |
|---|---|
/bin/clang.wasm | clang 23.0.0git for wasm32-unknown-wasi, 68 MB, with lld's wasm driver linked in |
/bin/clang.cfg | driver configuration: --sysroot=/sysroot |
/sysroot | the WASIX sysroot: headers, libc, crt objects (33 MB) |
/lib/clang/23 | clang's resource directory: builtin headers and libclang_rt.builtins.a (8.5 MB) |
Three command names — cc, gcc, clang — all the same compiler.
Why gcc is clang
GCC has no wasm backend — upstream has never supported wasm32 as an output
target, which is why emscripten ships clang for the same job. gcc --version
prints clang's banner. It takes GCC's usual flags.
How it was built
Source: msorvig/llvm-project-wasi, branch wasi — the LLVM tree with the
WASI portability patch that makes LLVM itself buildable for wasm32-wasip1.
Upstream LLVM stops at #error GetMainExecutable is not implemented on this host.
cmake -G Ninja -S llvm -B build \
-DCMAKE_TOOLCHAIN_FILE=Toolchain-WASIX-LLVM.cmake \
-DLLVM_NATIVE_TOOL_DIR=<native>/bin \ # tblgen has to run on the host
-DLLVM_TARGETS_TO_BUILD=WebAssembly \
-DLLVM_ENABLE_PROJECTS="clang;lld" \
-DLLVM_ENABLE_THREADS=OFF \
-DLLVM_DEFAULT_TARGET_TRIPLE=wasm32-wasi \
-DCMAKE_BUILD_TYPE=MinSizeRel -DLLVM_ENABLE_ASSERTIONS=ON
ninja bin/clang
The toolchain file targets wasm32-wasi against the wasix sysroot (not
wasi-libc — that has no proc_exec at all), with -flto,
-D_WASI_EMULATED_MMAN, and the flags the WASIX runtime requires:
-matomics -mbulk-memory -Wl,--shared-memory -Wl,--import-memory -Wl,--max-memory=4294967296 -Wl,-z,stack-size=8388608,--stack-first.
Without the shared-memory flags the module builds and then refuses to
instantiate (incompatible import type ... shared: false vs true).
The final link is a full-LTO wasm-ld run: ~10 minutes, ~12 GB peak RSS.
Everything before it is parallel across all cores.
Verified
$ cc --version → clang version 23.0.0git, Target: wasm32-unknown-wasi
$ clang -c hello.c -o hello.o → 685-byte wasm object, no -fno-integrated-cc1
$ cc hello.c -o hello.wasm → 64 067 bytes; runs: "hello from clang 23"
$ cc a.o b.o -o two.wasm → 64 138 bytes; runs
$ cc -c a.c b.c → separate objects, then linked
Caveats
- No
llvm-ar/llvm-ranliband no standalonewasm-ldbinary: spawning a linker is what this build exists to avoid, andclangalready contains one. -fno-integrated-cc1does not work: it asks the driver to spawn itself, which WASIX cannot do.