CLI tool
tyfkda/xcc avatar
tyfkda/xcc

xcc, a self-hosting C compiler with its own libc and a WebAssembly target

Standalone C compiler/assembler/linker/libc for x86-64/aarch64/riscv64/wasm

555 stars40 forksCMIT

At a glance

What is it?
xcc is a standalone C toolchain that compiles, assembles and links, and ships its own C library, targeting x86-64, aarch64, riscv64 and WebAssembly. It is written in portable C with per-architecture backends, and it can compile itself, which is the property that makes it worth reading.
Who is it for?
xcc suits a developer who wants to read a working C compiler end to end, or who needs a small toolchain they can build and modify, and it suits a browser-based compiler experiment given the WebAssembly front end. It does not suit anyone who needs a production C compiler, since optimisation and the archiver are both listed as to-do items.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Five binaries, like a real toolchain

xcc is a C compiler running on Linux or MacOS, supporting the x86-64, aarch64, riscv64 and wasm architectures, with ELF64 as the binary format. The description calls it a standalone compiler, assembler, linker and libc.

The build produces five executables, and the split is the familiar one.

sh
$ make

The generated files are `xcc`, the compiler entry point, `cpp`, the preprocessor, `cc1`, the C compiler, `as`, the assembler, and `ld`, the linker.

That is the same division of labour gcc uses, and the reason is the same: a driver that orchestrates, a separate preprocessor, a separate compiler proper, and separate assembler and linker stages. The stage separation is what lets xcc emit assembly with `-S` and object files with `-c` without the driver having to know how to do either job itself.

The libc is the part that makes the toolchain standalone. The `libsrc/` directory in the repository holds the C library sources, and the Makefile builds it into `libc.a` and `crt0.a` alongside the compiler. So xcc can compile a program with its own runtime rather than requiring the system's libc, and the command line has switches for exactly that, with `-nodefaultlibs` to ignore the libc and `-nostdlib` to ignore the libc and crt0.

The requirements are modest: Linux or MacOS, a C compiler, either gcc or clang, and make. The host compiler is only needed to build the toolchain, because once you have it you can use it to compile itself.

The licence is MIT, the default branch is `main`, and there are no GitHub releases, so the toolchain is consumed from the repository rather than from a packaged release. The topics list is long and telling, naming aarch64, arm64, assembler, c-compiler, elf64, linker, mach-o, posix, preprocessor, riscv64, self-hosting, wasi and wasm, which is the full territory the project covers.

Using it, and the options that matter

The usage example is the shortest possible program and the shortest possible command.

sh
$ ./xcc -o hello examples/hello.c
$ ./hello
Hello, world!

The command line options are a short list, and for a compiler of this size it is a complete one.

`-o <filename>` sets the output filename, with a default of `a.out`. `-I <path>` adds an include path. `-D <label>(=value)` defines a macro. `-S` outputs assembly code, `-E` preprocesses only, and `-c` outputs an object file. Then the two libc switches, `-nodefaultlibs` to ignore the libc and `-nostdlib` to ignore the libc and crt0.

Two of those defaults are worth noticing. The output name is `a.out` rather than something derived from the source, which is the traditional Unix behaviour and a small sign of the project's lineage. And the presence of `-E` as a separate option means the preprocessor can be run on its own, which is what you want when you are debugging a macro expansion or a header problem rather than a compilation problem.

The repository ships examples that show the range. There is `examples/hello.c`, `examples/echo.c`, `examples/fib.c` and `examples/mandelbrot.c`, so the scope is from the trivial to a compute-heavy program that is a reasonable test of floating point support.

That last example connects to the Makefile, which has a commented-out `NO_FLONUM` switch. The floating point support in the compiler is optional and configurable, and the Makefile exposes a set of these language feature switches: `NO_FLONUM`, `NO_BITFIELD`, `NO_VLA` and `NO_WCHAR`, each of which defines a `__NO_...` macro when set. That is a compiler built by someone who wanted to know exactly which parts of C it implemented and to be able to turn parts of it off, which is a researcher's instinct rather than a vendor's.

The compiler flags themselves are strict in a way worth noting. The C flags are `-ansi -std=c11 -pedantic -MMD -Wall -Wextra -Werror -Wold-style-definition`, with a few warnings suppressed, and the optimisation is `-O2 -g3`. Warnings being errors in the build of the compiler itself is the discipline that makes a self-hosting toolchain work, because the second stage of the build has to compile the same sources with the compiler it just built.

How one compiler supports four architectures

The Makefile shows how the architecture support is organised, and the answer is that the frontend and backend are separated by directory.

The architecture is detected automatically, with `ARCHTYPE` defaulting to `x64` and switching to `aarch64` when `uname -m` reports `arm64` or `aarch64`. That variable then selects the directories, with `AS_ARCH_DIR:=$(AS_DIR)/arch/$(ARCHTYPE)` and `CC1_ARCH_DIR:=$(CC1_DIR)/arch/$(ARCHTYPE)`.

So the assembler and the compiler backend each have a per-architecture directory, and the frontend is shared, in `CC1_DIR/frontend`. The layout is `src/xcc` for the driver, `src/cc` for the compiler, `src/cpp` for the preprocessor, `src/as` for the assembler, `src/ld` for the linker, `src/util` for shared utilities and `src/_debug` for debugging support.

A frontend and backend split is the classic compiler architecture, and here it is doing the job of keeping the four targets from spreading through the code. The frontend deals with the language, the backend with the machine.

There is a second, more unusual dimension in the same Makefile: the platform. A `PLATFORM` variable, if set, adds a define naming the target platform in upper case, `XCC_TARGET_PLATFORM=XCC_PLATFORM_...`. And a `HOST_CC_PREFIX` variable adds a define for the host compiler prefix. Those two exist for the self-hosting case, where the compiler that is running has a different name than the one on your path.

The comment about the platform support in the topics list mentions mach-o and posix, which is the MacOS side of the picture. The binary format is ELF64 for the Unix targets, and the Makefile has a Darwin-specific branch: on Darwin the library list is empty, and everywhere else it is `$(LIB_DIR)/crt0.a $(LIB_DIR)/libc.a`. So the MacOS build does not link the bundled libc in the same way, and the reason is presumably that the platform provides its own.

The `include/` directory at the repository root holds the headers the compiler ships, and the `tests/` and `tool/` directories hold the test suite and the support tools, including the WASI runner used later.

Compiling the compiler with itself

The self-hosting configuration is the most interesting part of the Makefile, and it is set up with a single variable.

If `TARGET` is set, the Makefile switches into self-hosting mode. The parent dependencies become the host versions of the tools, `$(HOST)xcc $(HOST)cc1 $(HOST)cpp $(HOST)as $(HOST)ld` plus the libraries, the object directory moves to `obj/$(TARGET)`, the compiler variable becomes `./$(HOST)xcc` rather than the host's own cc, and a `SELF_HOSTING` define is added to the flags.

So the mechanism is straightforward: build with the host compiler to get a first-generation xcc, then use that xcc to compile the sources again, and you have a second-generation compiler built by the first one. Run that again and you have a third. The three generations should produce identical output, and that comparison is the standard test of a self-hosting compiler.

The `HOST_CC_PREFIX` variable exists to make this work when the host compiler has a version suffix or a different name, which is common on systems with several toolchains installed. It is defined into the C code as a preprocessor symbol so the source can invoke the right binary.

The reason to care about self-hosting, beyond the obvious appeal, is that it is the strongest available check on a compiler's correctness. A compiler that can compile itself has been exercised on a large, real, self-consistent C program, namely its own source. A compiler that cannot is taking that on trust. The fact that xcc is a self-hosting toolchain is listed in the topics and is visible in the build configuration, and it is the reason the project is worth studying even if you never intend to use it.

The `libsrc/` directory, holding the C library sources that are compiled into `libc.a` and `crt0.a`, is part of the same story. The libc has to be compilable by the compiler, and the compiler has to be compilable by itself, and the two are kept consistent by being in the same repository and the same build.

The WebAssembly path, and the browser front end

The WebAssembly support is a separate target with its own compiler, `wcc`, which includes the preprocessor and emits a `.wasm` file directly.

The requirements for that path are node.js, npm and `llvm-ar`, and the setup is an npm install.

sh
$ npm ci

Then a separate make target.

sh
$ make wcc

The generated file is `wcc`, a C compiler that includes the preprocessor and outputs .wasm directly, so there is no separate assembly and link step in the way the native build has.

The command line for the WebAssembly compiler has more options than the native one, and the extra ones are all about the WebAssembly conventions. The output name defaults to `a.wasm`. The `--entry-point=func_name` option sets the entry point, with a default of `_start`. The `-e func_name,...` option exports function names, comma separated. The `--stack-size=<size>` option sets the stack size, defaulting to 8192. And there is a `-Wl,...` option for linker options, with `--allow-undefined` to handle unresolved function symbols as imported at runtime, and `--export-all` to export all non-static functions.

Those two linker options exist because WebAssembly modules have no dynamic linking in the usual sense. A module either exports a symbol or imports it, and an undefined symbol is a link error unless the module is allowed to import it, in which case the host provides it at runtime. That is a different failure mode from ELF and it needs a different flag.

Running the result uses a tool in the repository.

sh
$ ./tool/runwasi hello.wasm
Hello, world!

The README notes that you can also use a WASI runtime such as Wasmtime or Wasmer instead.

The one documented limitation is about `goto`: it can jump to an ancestor and a later line only. That is a real restriction on a compiler and a sensible one to state plainly, since it means the compiler supports the forward and backward jumps within a block but not arbitrary jumps to arbitrary labels.

The online demo is a separate front end, and the package manifest shows how it is built. The package is called Wcc, version 0.1.0, described as a C-compiler for wasm, and the scripts are `npm run build-assets` which calls `make assets`, and `npm run default` which builds the assets and then runs vite on port 3000, with `npm run release` producing a production build with vite. The dependencies include `@wasmer/wasi` and `@wasmer/wasmfs`, so the demo runs the compiled module in the browser through a WASI shim, and the front end is TypeScript with Alpine.js and Vite, with ESLint and TypeScript tooling in the dev dependencies. Node 20 or newer is required.

What is missing, and where to take it

The TODO list in the README is two items, and they are the two things you would notice first if you tried to use this for real work.

The first is optimisation. The compiler is a code generator, not an optimiser, and while the build of the compiler itself uses `-O2 -g3` flags, that is the host compiler optimising the build of xcc, not xcc optimising its output. Code generated by xcc will be correct and unoptimised.

The second is the archiver. There is no `ar` in the generated file list, so static libraries are not produced, which means the `libc.a` and `crt0.a` the Makefile links are built by some other means, and a user cannot package their own objects into a library with the toolchain.

Those two gaps define the use case exactly. xcc is a compiler you read, and a compiler you compile with, not a compiler you ship. It is a self-hosting toolchain, a reference implementation, and a teaching project, and the README says as much by linking two references: a book on writing C compilers for people who want to understand the low layers, and rui314/9cc, a small C compiler.

The reference to 9cc is worth a note, because it puts xcc in a lineage. Small C compilers are a genre, they exist to be understood, and they differ in ambition. Some target one architecture and one operating system. Some add a WebAssembly target so the compiler can run in a browser, which is what xcc does with `wcc` and the Vite front end. Some add a self-hosting stage, which xcc also does. Putting those two features together, a compiler you can run in a browser tab that is also able to compile itself, is a fairly complete answer to what makes a compiler project interesting to read.

The realistic alternatives for using C in practice are the established toolchains, and the comparison is not close on performance or compatibility. What xcc offers instead is legibility. Every stage is a separate directory of small C files, the architecture split is by directory rather than by a runtime flag, and the whole thing builds with make on a laptop with no dependencies beyond a host compiler.

For a reader, that is the value proposition. It is a few thousand lines of C arranged so that you can hold the whole pipeline in your head, which is not something gcc, clang or tcc offers at any level of detail.

Editorial conclusion

xcc suits a developer who wants to read a working C compiler end to end, or who needs a small toolchain they can build and modify, and it suits a browser-based compiler experiment given the WebAssembly front end. It does not suit anyone who needs a production C compiler, since optimisation and the archiver are both listed as to-do items. Verify first that your host architecture is one of the four supported targets, that you have a host C compiler, make and the LLVM archiver for the WebAssembly build, and that the C features you rely on are not among the ones the Makefile can switch off.

Frequently asked questions

What is xcc?

It is a standalone C compiler, assembler, linker and C library for x86-64, aarch64, riscv64 and WebAssembly, running on Linux or MacOS and producing ELF64 binaries. It is written in portable C, is MIT licensed, and can compile itself.

How do I build xcc?

You need Linux or MacOS, a C compiler, gcc or clang, and make. Running make produces five executables: xcc as the compiler entry point, cpp as the preprocessor, cc1 as the C compiler, as as the assembler and ld as the linker.

How does xcc target several architectures?

The Makefile detects the architecture from uname -m and selects per-architecture directories for the assembler and the compiler backend, while the compiler frontend is shared between all of them. The primary language is C, the topics cover aarch64, riscv64, wasm and self-hosting, and the build also has switches to disable individual C language features.

What does the WebAssembly build produce?

A separate compiler, wcc, which includes the preprocessor and emits .wasm directly. Building it needs node.js, npm and llvm-ar, and you run make wcc after npm ci. The result can be run with the repository's own tool/runwasi script or with a WASI runtime such as Wasmtime or Wasmer.

What are the known limitations of xcc?

The TODO list names optimisation and an archiver, so the compiler does not optimise its output and there is no tool to package static libraries. On the WebAssembly side the README states that goto can jump to an ancestor and a later line only, and the binary format is ELF64 for the native targets.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. tyfkda/xcc on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tyfkda-xcc.svg)](https://hysenlabs.com/projects/tyfkda-xcc)

Was this useful?