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:

Hardware:

Crust Papers:

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 supplied string/vector/map/smart pointers, and the C++11 spellings.
  • examples/crust/ -- working programs, each run by tools/crust_examples.py against its expected output. ownmember.c is the owning-member shape, raii.c the borrowing one, cpp11.c the C++11 spellings, and dispatch.c virtual 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

References

External Demos