Technology
crustc: entirety of `rustc`, translated to C
Key Points
This is a functional Rust compiler you can build with GCC & make . # We need to provide a path to LLVM(`libLLVM.so.22.1-rust-1.98.0-nightly`) # I *could* include pre-built LLVM in the project, but I'd rather not embed random binaries in the project. make -j20 LLVM_LIB_DIR=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib
This is a functional Rust compiler you can build with GCC
& make
.
# We need to provide a path to LLVM(`libLLVM.so.22.1-rust-1.98.0-nightly`)
# I *could* include pre-built LLVM in the project, but I'd rather not embed random binaries in the project.
make -j20 LLVM_LIB_DIR=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib
It is just C code [1], which, when compiled, gives you a functional Rust compiler.
# It works - (library path to point to libLLVM.so.22.1-rust-1.98.0-nightly - rustc uses llvm)
LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib:./rustc_driver ./rustc/rustc --version
rustc 1.98.0-nightly (c712ea946 2026-06-16)
That Rust compiler can compile code - build core
, alloc
, std
- you name it!
This is a demo/teaser for my new Rust to C compiler toolchain.
The full cilly
toolchain compiles your own Rust to C for arbitrary targets.
This repo just shows the compiler compiling itself, as I believe this is the flashiest showcase I could do.
For the past 3 years, I have been working on compiling Rust to C. I made a few public attempts, like rustc_codegen_clr
, and a lot of private ones.
This is, by my count, the 14th attempt: cilly
. It is a Rust library for generating C code and a Rust compiler backend (read: plugin) that allows you to compile Rust to C.
The main innovation behind cilly
is that it adapts to C compilers.
It can generate "witness" programs, which check what a given compiler and platform support:
/* This compiles if and only if our C compiler supports _Thread_local. */
_Thread_local int KEYWORD_TLS_SUPPORTED;
This means Cilly will generate C code, which will make your specific, weird "Shminky C compiler for Blorbo OS" happy.
/* This will pass in some C compilers. */
assert(sizeof(float) == sizeof(double));
All type layouts, sizes, alignments, character encodings (ASCII), and integer formats (two's complement) are queried for.
With fallbacks, where possible.
I try my very best not to assume anything outside of ANSI C[2] - including workarounds for things in "modern" C standards, like strict aliasing.
Sadly, this means the output of cilly
is compiler-specific (i.e., you can't take the cilly C generated for Arm64 and run it on riscv32, but you can generate cilly C specifically for riscv32).
This build of rustc
(the generated C) is "targeting" ARM64 Linux because that is the ISA of my workstation.
The primary goal of this is support for old/obscure hardware with no LLVM/GCC support.
There are still some systems out there that don't support Rust
but support C
.
Whenever some project moves from Rust to C, or a Rust alternative of a C project is made, support for those targets is validly raised as a downside of Rust [3].
The goal of this project is to remove that problem.
cilly
wraps rustc
and a C compiler and translates the Rust code to C on the fly.
From the user perspective, this is as simple as defining what C compiler to use for a given target.
"triple": [
"sdcc_z180-unknown-none"
],
"tool_def": {
"kind": "local",
"compile": {
"base": {
"executable": "/usr/bin/sdcc",
"base_args": [
"-mz180",
"--std-c89",
"-c"
],
"input_arg_template": [
"{input}"
],
// JSON cut off for brevity.
}
}
}
cilly
is network transparent, and can talk to C compilers over TCP (may be extended to weird things like UART if need be).
This is a solution to the bootstrap paradox / platforms without C cross compilers.
You build a small C server on your Blorbo OS, run rustc
on some normal platform like Linux, and let cilly
talk over the wire.
I have successfully used this to compile small Rust programs for x86 Plan9 VMs, while running rustc
on Arm64 linux.
term% echo `{cat /dev/sysname} osversion `{cat /dev/osversion} cputype $cputype
gnot osversion 2000 cputype 386
term% /tmp/hello_plan9
Hello, world!
term% nm /tmp/hello_plan9 | grep rust_begin_unwind
1020 T _RNvCshfEkAwg4zv6_7___rustc17rust_begin_unwind
cilly
can optionally embed markers within its object files, and save its IR to a cache directory.
It can then read those markers, split functions / globals by their definition location, and generate
a directory with makefiles - to allow you to build Rust with a C compiler and make.
cilly
generated code is mostly ABI compatible with normal rustc
compiled code. I say mostly, because on some platforms(... like arm64)
rustc
choose an ABI not representable from C[4].
This rust compiler was built on:
uname -a
Linux spark-2773 6.17.0-1021-nvidia #21-Ubuntu SMP PREEMPT_DYNAMIC Wed May 27 19:14:05 UTC 2026 aarch64 aarch64 aarch64 GNU/Linux
This is the C compiler I used:
readelf -p .comment ./rustc/rustc
String dump of section '.comment':
[ 1] GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
[ 2e] Linker: Ubuntu LLD 18.1.3
In order to build the demo, you will need to provide it with the right LLVM libs. The easies way to do so is to just use the LLVM rustc
ships:
rustup install nightly-2026-06-16
With the right GCC(more modern GCC versions should work too, untested), right LLVM version, and GNU make installed, run:
# We need to provide a path to LLVM(`libLLVM.so.22.1-rust-1.98.0-nightly`)
# I *could* include pre-built LLVM in the project, but I'd rather not embed random binaries in the project.
make -j20 LLVM_LIB_DIR=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib
# CFLAGS work(CAVEAT: some flags will slow compilation down)
make -j20 CFLAGS=-g
And... voilà!
I strongly recommend not enabling optimizations: both because the may break stuff(this is just a demo, and it's... ee... rough around the edges[5]) AND because optimizations take time at this scale.
Without opts, my machine builds the project in a few minutes:
make -j20 937.98s user 123.77s system 1352% cpu 1:18.48 total
With opts, expect to choke on some specific larger rust files. You will blitz through most code, and then get stuck on those behemoths.
Run:
LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib:./rustc_driver ./rustc/rustc --version
You should see the rustc version printed.
For building programs, you will need to build std.
LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib:./rustc_driver ./rustc/rustc main.rs
error[E0463]: can't find crate for `std`
|
= note: the `aarch64-unknown-linux-gnu` target may not be installed
= help: consider downloading the target with `rustup target add aarch64-unknown-linux-gnu`
= help: consider building the standard library from source with `cargo build -Zbuild-std`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0463`.
Read BUILDING_STD.md
for that.
For some weird path-canonicalization-reasons, crustc
can crash when run in the directory it was built in(repo root, crustc). Works fine in other places.
...
I am confused too.
Nah, not yet. It is not ready for public consumption. I will release it as soon as possible - but
- I got a job(which means I no longer write code on a laptop with it's G & C keys broken, yay!)
- I got uni(well I am on summer break, but thesis-s don't tend to write themselves).
- I put my left hand in a blender. The blender won. (Still have all my fingers, just some stitches). I will not elaborate further.
1 - well, C code + some C++ LLVM wrappers. Rust uses a bit of C++ to expose some more LLVM knobs. They are shipped pre-compiled because they are LLVM-version-specific and a pain to build standalone.
2 - rarely, I have to make "reasonable assumptions" which would be goal stoppers. E.g. (void*)(uintptr_t)(ptr)
roundtrips. I document those AND add asserts for them (CHAR_BIT = 8) if possible.
3 - the weight of "Rust does not support Plan 9" as an argument against Rust depends on personal preferences, and evaluating that argument is left as an exercise to the reader.
4 - The issue is the struct return pointer(sret
). On most platforms, it is passed in the same reg as the first arg, so we can always force to return-by-sret(out pointer) by just having the first arg be the out pointer. Not on Arm64. There, sret pointer is passed in a different register, meaning the native C compiler has to chose to do a return-by-sret for us. Which it does not do for small structs(<16 bytes afaik). Annoying.
5 - I am chasing some optimization related bugs - part of the reason full cilly toolchain is not out.