Crust
September 16, 2026 · View on GitHub
A Unified C/C++/Rust Compiler Environment for Systems Programming
A new C++, from a self-contained system
C++26 adds features to a language whose problem was never a shortage of features. Crust goes the other direction: a C++ subset with opinions, compiled by a self-contained toolchain that owns the whole stack — the compiler, the C it emits, the boot path, the threading model — with no clang, no gcc frontend, no rustc, and for the bare-metal targets, no operating system underneath. The position paper is CPP_DIRECTION.md; the short version is that every credible safety story in this space — JSF, MISRA, seL4, SPARK — is a subtraction story, and the first mover that makes the smaller language load-bearing gets to define it.
Owning the full stack is not a purity exercise. It is what lets the line between language, runtime and OS blur where blurring wins:
Threading is the clearest case. std::thread and thread_local are
OS thinking: the language cannot see thread structure, so every context
switch saves the whole architectural register file, and every
thread-local is a memory segment reached through an indirection register.
Crust's bare-metal model declares the threads to the compiler:
assert io_thread in threads.left( core=0 )
assert compute_thread in threads.right( core=0 )
From two declarations, the compiler computes each thread's transitive
register footprint over the whole-program call graph, splits the register
file into disjoint left/right banks, re-runs allocation under each
budget, and generates the context switcher — which saves only what
the running side actually uses. A thread's persistent state lives in its
registers across switches: what thread_local approximates with a TLS
segment and a tp-relative load, this model gets for the price of
nothing, because the other thread provably cannot touch the bank.
Measured, not projected (BAREMETAL_THREADS.md): a
printf-shaped IO thread against a compute thread on bare-metal AArch64 —
IO's seven-function call graph needs six callee-saved homes, compute
needs zero, so switching away from compute saves nothing at all;
50,522 preemptive switches with the generated switcher in the IRQ vector
and zero corruptions, verified by per-iteration invariants; the
partitioned ISR is 60 instructions against 75 for save-all. None of this
is expressible in a std::thread world, because pthreads sits below the
language and the language cannot see through it.
The same ownership runs the rest of the way down: the toolchain builds
and boots its own OS (CRUSTOS.md, and the Full-stackless
Computing paper below), C++ and Rust meet in one translation unit with
one destructor lowering, error handling is the checked except model —
destructors on the error path with no unwinder, unhandled errors as
compile errors (CPPRUST.md) — and one class digest lets a
C++ class subclass an rpython one and the reverse
(CPPRPY.md). What the subset refuses, it refuses with the
reason and the replacement in the diagnostic; the refusals are tested as
pinned behaviour, because for this compiler a refusal is the
deliverable.
Main Crust Documentation:
- CRUST.md C+Rust
- CPPRUST.md C+C++Rust
- CSRUST.md C# subset → C++ subset → Rust/C (
cs2cpp+cpprust) - UNITY_PACK.md packed
engine.c/data.cfrom a Unity-subset scene - UNITY_PACK_GPU.md SoA upload, GLSL std140/std430, culling order
- SHIVYCX.md SHIVYC-X (C Compiler)
- CPP2RUST.md C++ to Rust translator
- TRANSPILER.md the Python→C transpiler (
py2c.py) - REGEX.md one regex engine, shared by C, C++, RPython and minipy
- BUILDTOOLS.md lowering
tools/to native binaries - WASM.md the WebAssembly back end (
--target wasm) - GLES2.md OpenGL ES 2.0 — native surfaceless and wasm/WebGL
- examples/wireproto/README.md dual FE/BE compile + binary protocol
Hardware:
- MACOS.md macOS on Apple Silicon (
--os macos) - WINDOWS.md 64-bit Windows (
--os windows), with no MinGW or MSVC needed - FREEBSD.md FreeBSD/amd64 (
--os freebsd) - BAREMETAL_ARM64.md
- RASPI.md Raspberry Pi
- JETSON_NANO.md Nvidia Jetson Nano
- BAREMETAL_THREADS.md
Crust Papers:
- https://dx.doi.org/10.2139/ssrn.7396160 "Memory Safety Where it is Needed: Proof-guided Runtime Checking in a Toolchain Small Enough to Read"
- https://dx.doi.org/10.2139/ssrn.7382398 "A Successor Discipline, Not a Successor Language: Safety by Subtraction in a Self-Contained C++ Toolchain"
- https://dx.doi.org/10.2139/ssrn.7315678 "Interoperation Without an Interface: C++ and Rust in One Translation Unit, and One Toolchain"
- https://dx.doi.org/10.2139/ssrn.7226482 "Full-stackless Computing: A Multi-language Compiler That Builds and Boots its Own Operating System"
Modern systems development forces engineers to choose between two paradigms:
- The ubiquitous simplicity and legacy ecosystem of C.
- The type safety, spatial memory guarantees, and modern ergonomics of Rust.
Today, systems that use both languages must compile them through isolated pipelines (clang/gcc and rustc) and stitch them together using dynamic (.so) or static (.a) libraries via a foreign function interface (FFI).
This FFI boundary comes at a steep price:
- Forced C ABI lowerings that strip high-level type metadata.
- Opaque calling conventions that inhibit aggressive interprocedural optimizations (IPO) and register allocation across language boundaries.
- Heavy, fragile Link-Time Optimization (LTO) setups that slow compile times dramatically while still missing frontend-level alias and lifetime optimizations.
Crust solves this by uniting C/C++ and Rust into a single compiler frontend. By natively accepting ISO C syntax alongside a growing subset of Rust syntax, Crust enables seamless, zero-overhead cohabitation of both paradigms inside a single compilation unit.
C++ and Rust in one file
This is an introduction to how the two languages meet in this project. It
assumes no knowledge of the rest of the documentation; CRUST.md and
CPPRUST.md are the reference material behind it.
The one idea
Most attempts to make C++ and Rust talk to each other put something between them: a foreign function interface, a generated shim, a marshalling layer, a description of one language's types written in the other's. Something has to translate, and that something costs a call boundary the optimiser cannot see through.
This project takes the other route. Both languages are lowered to plain C source, and then one C compiler reads the result:
foo.rs ──▶ shivyc/crust.py ──┐
├──▶ plain C ──▶ ShivyCX ──▶ machine code
foo.cpp ──▶ tools/cpprust.py ──┘
By the time anything is compiled there is no C++ and no Rust left, so there is no boundary to cross. A Rust function calling a C++ method is a C function calling a C function: same translation unit, same intermediate representation, same register allocator, and inlining across the two sides is just inlining.
The cost of this is honesty about scope. Neither front end implements its whole language, and both refuse what they cannot lower rather than guessing. That refusal is the design, not a gap in it -- a subset you can trust is worth more than a superset that is quietly wrong somewhere.
Where the two sides meet
They meet at the symbol. Both lowerings were chosen to produce the same shape of C, so the same data has the same name from either side.
A Rust impl method:
impl Counter {
fn bump(&mut self, by: i32) { self.n += by; }
}
void Counter_bump(Counter *self, int by) { self->n += by; }
A C++ class method:
class Counter {
public:
void bump(int by) { n = n + by; }
};
void Counter_bump(Counter *this, int by) { this->n = this->n + by; }
&mut self and this are the same pointer. Type_method is the same name.
That is not a coincidence and it is not for looks: it is what lets a class
written in one language be used from the other without anything in between.
Destruction: the same function from both sides
The clearest case is object lifetime, because both languages have opinions about it and they turn out to agree on the symbol.
A Rust type with a destructor:
struct Res { id: i32 }
impl Drop for Res {
fn drop(&mut self) { printf("released %d\n", self.id); }
}
lowers to Res_drop(Res *self). A C++ ~Res() lowers to Res_drop(Res *)
as well. So a C++ class can hold a Rust type by value, and its destructor
calls the Rust one directly:
class Holder {
public:
Vec_int nums; /* a Crust Vec<i32> */
Res r; /* a Crust type with impl Drop */
};
No destructor is written there. Both members own something, so the class gets
an implicit one that releases each of them -- and Res_drop in it is the
function Crust emitted from the impl Drop above.
Where the two languages genuinely disagree, each side follows its own rule rather than one being bent to the other. Members are destroyed in reverse declaration order on the C++ side, because that is C++; Rust's field glue frees in declaration order, because that is Rust. The symbol is shared; the order is not.
What each side is for
They are not redundant. Each brings something the other does not.
Rust brings move semantics and the ownership discipline around them. Passing an owning value by value is a move: the source is zeroed, the callee takes it, and reading the source afterwards is rejected rather than silently yielding an empty value.
C++ brings a full object lifecycle: constructors chosen by arity, copy
construction and operator=, member and base construction ordering,
inheritance and virtual dispatch. Where a Rust impl Drop gives a type a
destructor, a C++ class gives it a whole life.
So the natural division is that C++ owns the structure -- class hierarchies, RAII wrappers, anything with a rich lifecycle -- and Rust owns the work, with containers and algorithms that move rather than copy. A program can use whichever is the better tool for each piece and pay nothing for mixing them.
The boundary is checked, not assumed
Sharing a representation means bugs can cross languages, and the passes look for exactly that. Two examples, both of which were real:
Passing an owned value by value from C++ to a Rust function is refused:
return consume(t.samples); /* a Rust fn consume(v: Vec<i32>) */
Rust drops a by-value owning parameter when the callee returns, so consume
frees the buffer -- and the C++ destructor frees it again. One buffer, two
frees. The diagnostic names the fix, and the fix costs nothing: pass
&t.samples, which is what a Rust &Vec<i32> parameter lowers to anyway.
Deriving a copy on a type that owns something is refused on the Rust side, for the same reason C++ refuses to copy a class with a destructor and no copy constructor. It is the Rule of Three, arrived at from two directions.
A whole file
#include "tally.cpp"
int printf(const char *, ...);
/* Rust: a type with a destructor, and a function that moves a Vec. */
struct Res { id: i32 }
impl Drop for Res {
fn drop(&mut self) { printf(" released %d\n", self.id); }
}
fn total(v: &Vec<i32>) -> i32 {
let mut acc: i32 = 0;
for i in 0..v.len() { acc += v.get(i); }
acc
}
/* C: the driver. */
int main(void) {
printf("sum = %d\n", collect());
return 0;
}
with tally.cpp alongside it:
class Tally {
public:
Vec_int samples; /* by value: complete here, no forward declaration */
Res mark;
void add(int v) { Vec_int_push(&samples, v); }
};
int collect(void) {
Tally t;
t.mark.id = 7;
t.add(3);
t.add(4);
return total(&t.samples); /* C++ calling Rust, by pointer */
}
It prints:
released 7
sum = 7
One translation unit. Tally is destroyed at the closing brace of
collect, releasing both members -- which is where released 7 comes from,
and it is the Rust impl Drop that printed it, called from a C++ destructor
nobody wrote. total is a Rust function reading a C++ member. Nothing is
marshalled anywhere.
Where to go next
CRUST.md-- the Rust subset: what it supports, what it refuses, and why. Ownership,Drop, moves, and the bundled core containers.CPPRUST.md-- the C++ subset: classes, templates, inheritance and virtual dispatch, the suppliedstring/vector/map/smart pointers, and the C++11 spellings.examples/crust/-- working programs, each run bytools/crust_examples.pyagainst its expected output.ownmember.cis the owning-member shape,raii.cthe borrowing one,cpp11.cthe C++11 spellings, anddispatch.cvirtual dispatch with Rust reducing the results.
Crust Architectural Philosophy
Function-Level Syntax Isolation To prevent syntactic ambiguity and parsing complexity, Crust enforces function-level syntax boundaries. A source file contains functions written entirely in standard C syntax alongside functions written in Rust syntax.
Benchmarks
- (June 29, 2026) https://doi.org/10.5281/zenodo.21048364
References
- ShivC — the original compiler ShivyCX was rewritten from.
- https://github.com/OpenSourceJesus/C-Compiler — the extended C compiler by Gilead Cosman this work is based on.
- C11 Specification — http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
- x86-64 ABI — https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf
- Iterated Register Coalescing (George and Appel) — https://www.cs.purdue.edu/homes/hosking/502/george.pdf
- (B. Hartshorn, viXra 2025, 2026).
- Foundational Problems with Compilers and Operating Systems https://ai.vixra.org/abs/2507.0081
- Rethinking Meta-Interpreters for High-Performance Execution https://ai.vixra.org/abs/2606.0084
- Closing the Compiler Loop: Toward a Self-Hosting, Dependency-Free Python JIT https://zenodo.org/records/21178004
- Beyond WebAssembly: Rethinking the Web Browser with Post-JavaScript, VM-Free Page Execution https://ai.vixra.org/pdf/2607.0013v1.pdf