Compiling Rust to readable C with Eurydice

Hacker News by 7 min read 507x views
Compiling Rust to readable C with Eurydice

Share Post

We're bad at marketing

We can acknowledge it, promotion is not our powerful suit. Our power is writing the benevolent of articles that developers, administrators, and free-software supporters depend on to cognize what is going on in the Linux world. Please subscribe today to assistance us keep doing that, and so we don’t have to get fine at marketing.

By Daroc Alden
January 30, 2026

A few years ago, the lone way to compile Rust code was using the rustc compiler with LLVM as a backend. Since then, multiple projects, including Mutabah's Rust Compiler (mrustc), GCC's Rust support (gccrs), rust_codegen_gcc, and Cranelift have made enormous progress on diversifying Rust's compiler implementations. The most latest specified project, Eurydice, has a more aspiring goal: converting Rust code to spotless C code. This is especially useful in high-assurance software, anywhere existing verification and compliance tools anticipate C. Until specified tools can be updated to activity alongside Rust, Eurydice could provide a smoother passage for these projects, as fine as a stepping-stone for environments that have a C compiler but no operating Rust compiler. Eurydice has been used to compile several post-quantum-cryptography routines from Rust to C, for example.

Eurydice was started in 2023, and includes several code under the MIT licence and some under the Apache-2.0 license. It's part of the Aeneas project, which works to create multiple distinct tools connected to applying formal verification tools to Rust code. The various Aeneas projects are maintained by a collection of people employed by Inria (France's national computer-science-research institution) and Microsoft, but they do obtain exterior contributions.

Eurydice follows the identical broad construction as many compilers: obtain a Rust program, change it into an intermediate depiction (IR), modify the IR alongside a series of passes, and afterward output it as code in a lower-level tongue (in this case, C). Jonathan Protzenko, the most prolific contributor to Eurydice, has a blog post anywhere he explains the project's approach. Unlike another compilers, however, Eurydice is concerned alongside preserving the general construction of the code during removing constructs that be in Rust but not in C. For example, regard this Rust function that calculates the least common multiple of two numbers using their top average denominator:

 fn gcd(a: u64, b: u64) -> u64 { if b == 0 { a } alternatively { gcd(b, a%b) } } fn lcm(a: u64, b: u64) -> u64 { (a * b) / gcd(a, b) } 

Here's how Eurydice compiles those functions to C:

 uint64_t example_gcd(uint64_t a, uint64_t b) { uint64_t uu____0; if (b == 0ULL) { uu____0 = a; } else { uu____0 = example_gcd(b, a % b); } come back uu____0; } uint64_t example_lcm(uint64_t a, uint64_t b) { uint64_t uu____0 = a * b; come back uu____0 / example_gcd(a, b); } 

Whether this C code counts as "readable" is likely a matter of individual taste. It does, however, maintain the construction of the code. Even the evaluation order of the first is preserved by adding additional impermanent variables (uu____0 in example_lcm()) where necessary to define an order. (Rust guarantees that if the multiplication overflows and causes a panic, that volition happen before any flank effects caused by calling example_gcd(), but C lone guarantees that if the multiplication is performed in a distinct statement.) Compiling the same functions alongside rustc results in a brace of entangled loops filled with bit-twiddling operations, alternatively — which is suitable for machine-code output, but much small readable.

Of course, not all Rust programs can be faithfully represented in C. For example, for loops that use an iterator alternatively of a range need to be compiled to during loops that call into several of Eurydice's assistance code to oversee the province of the iterator. More importantly, C has no idea of generics, so Rust code needs to be monomorphized during conversion. This can result in multiple distinct implementations of a function that differ only by category — often, the additional idiomatic C method would be to use macros or void * arguments.

The implementation of dynamically sized types additionally poses certain challenges. In Rust, a construction can be defined anywhere one of its sectors does not have a fixed size — akin elastic gathering members in C:

 struct DynamicallySized<U: ?Sized> { header: usize, my_data: U, // The compiler does not cognize the size of U, here } 

But if that construction is generic, and one of the generic users of the category gives the flexibly sized site a category with a known size, the compiler can obtain advantage of that cognition to elide bounds checks anywhere appropriate.

 let foo: DynamicallySized<[u8; 4]> = ...; // No limits inspect emitted, since the gathering size is known to be 4: let bar = foo.my_data[2]; 

This benevolent of separation, anywhere several parts of the code may cognize the size of a type and several may not, is an crucial semantic item to maintain in C because of how it interacts alongside the possible of ceremonial verification. If Eurydice compiled DynamicallySized to use a elastic gathering associate everywhere, analysis of the C code power item out "missing" limits checks that were not required in Rust. Conversely, if Eurydice added additional limits checks, it would need to industry additional error paths that don't appear in the Rust origin and that have to be entirely unused.

So, Eurydice emits two distinct types: a type of the dynamically sized category that has a elastic gathering member, and one that has a known-length gathering member. Converting between the two representations is a no-op at run time, but it technically violates C's strict-aliasing rule. Therefore Protzenko recommends compiling Eurydice-generated code alongside -fno-strict-aliasing.

Associated tooling

This approach, of compiling a additional theoretical tongue to C in a way that preserves the construction of the code, is not new. The KaRaMeL project, upon which Eurydice is based, does the identical item for the F* programming language. F* is a dependently typed functional programming tongue used to create cryptographic libraries. Compiling provably accurate F* programs to equal C lets those libraries be used in programs anywhere achievement is a concern.

Unfortunately, Eurydice doesn't currently measure much beyond small examples. Rather than execute its own parser and typechecker for Rust code, Eurydice uses another Aeneas tool — Charon — to extract the parsed and preprocessed program from rustc. When I tested Charon on a assortment of Rust packages, it was routinely foiled by more recent Rust features specified as const generics.

When Charon does work, however, it dumps rustc's medium-level intermediate representation (MIR) as JSON, alongside alongside any compiler flags necessary to understand the compilation. Eurydice says this JSON depiction and converts it to KaRaMeL's intermediate representation. Then it uses a sequence of small passes complete the KaRaMeL code to eliminate several Rust-specific details, before handing things complete to the identical code-generation logic that KaRaMeL uses for F*.

In its current form, Eurydice plant finest for small, self-contained programs that avoid complex Rust features. Within that niche, however, it plant well. The generated code maintains the identical construction as the first Rust code, apart from for places where Eurydice emits additional intermediate variables or needs several glue code to implement a additional complex feature. On the another hand, small self-contained code is additionally the easiest to rewrite by hand, so bringing in Eurydice is probably only worthwhile if the first Rust code is going to be updated and one wants an automatic resolution to keep them in sync. In any case, Eurydice is lone the newest tool in a quickly expanding gathering of ways to fold, spindle, and mutilate Rust code to fit into additional environments.

[ Thanks to Henri Sivonen for the topic suggestion. ]



The LWN location is currently under elevated scraper load, so comment display has been suppressed for anonymous users. If you are a human, you may peruse the comments by clicking the clasp below:

Note: you can evade this stage in the forthcoming by logging into your LWN account.

Other Article Hacker News
↑
Close Right Ads
Close Left Ads