Hayroll wraps the c2rust tool for converting
code written in C to the Rust programming language. The hayroll command is a
drop-in replacement for C2Rust. "Hayroll" stands for "HARVEST Annotator
for Yielding Regions Of Lexical Logic".
The c2rust program runs the C preprocessor before translating from C to Rust.
This means that macros are expanded (which destroys abstractions that the
programmer put in the code) and that conditionally-compiled code is lost.
For example, consider translating this C code (note especially the code comments):
float sinhf(float x) {
#ifdef __LIBMCS_FPU_DAZ // conditional compilation
x *= __volatile_onef; // conditional compilation
#endif // conditional compilation
float t, w, h;
int32_t ix, jx;
GET_FLOAT_WORD(jx, x); // statement macro
ix = jx & 0x7fffffff;
if (!FLT_UWORD_IS_FINITE(ix)) { // expression macro
return x + x;
}
h = 0.5f;
...The output of c2rust is (note again the code comments):
pub unsafe extern "C" fn sinhf(mut x: libc::c_float) -> libc::c_float {
// conditionally compiled code is lost
let mut t: libc::c_float = 0.;
let mut w: libc::c_float = 0.;
let mut h: libc::c_float = 0.;
let mut ix: int32_t = 0;
let mut jx: int32_t = 0;
loop { // statement macro is expanded
let mut gf_u = ieee_float_shape_type { value: 0. };
gf_u.value = x;
jx = gf_u.word as int32_t;
if !(0 as libc::c_int == 1 as libc::c_int) {
break;
}
} // ... end of statement macro expansion
ix = jx & 0x7fffffff as libc::c_int;
if !((ix as libc::c_long) < 0x7f800000 as libc::c_long) { // expr macro expanded
return x + x;
}
h = 0.5f32;
...By contrast, the output of Hayroll is (the code comments highlight improvements):
pub unsafe extern "C" fn sinhf(mut x: libc::c_float) -> libc::c_float {
#[cfg(feature = "defLIBMCS_FPU_DAZ")] // conditional compilation is retained
(x *= __volatile_onef); // conditional compilation is retained
let mut t: libc::c_float = 0.;
let mut w: libc::c_float = 0.;
let mut h: libc::c_float = 0.;
let mut ix: int32_t = 0;
let mut jx: int32_t = 0;
GET_FLOAT_WORD(&mut jx, &mut x); // statement macro becomes a function
ix = jx & 0x7fffffff as libc::c_int;
if FLT_UWORD_IS_FINITE(&mut ix as *mut int32_t) == 0 { // expr macro is function
return x + x;
}
h = 0.5f32;
...Ensure that only one version of LLVM and Clang are installed on your computer, and that they are the same version and not version 20. (This is necessary for C2Rust.)
Install Rust, if it is not already installed:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shNow, install Hayroll:
git clone https://github.com/UW-HARVEST/Hayroll
cd Hayroll
./prerequisites.bash
./build.bash
# Optionally, run tests (takes less than one minute):
cd ./build && ctestThe prerequisites.bash script has been tested on Ubuntu.
For installation on other operating systems, follow the instructions in
prerequisites.md, and please contribute back your
instructions or a pull request to make prerequisites.bash work on more
operating systems. Thanks!
To build a project, a C build system typically makes multiple calls to the
compiler with a long list of arguments. A compile_commands.json records these
commands and arguments for the convenience of downstream analysis.
An easy way to generate a compile_commands.json file is to run
make clean && bear -- makeYou should manually delete entires in compile_commands.json
that point to any source files that you do not want to translate.
The ./hayroll executable offers a turn-key solution from C source
files to Rust files, with macros (partially) preserving their
structure.
<path_to_Hayroll>/hayroll <path_to_compile_commands.json> <output_directory>./hayroll overwrites the output directory. Files are grouped per original C
file. We use .{split}.xxx below to denote artifacts for each split (DefineSet).
xxx.c: A copy of the original C source file.xxx.premise_tree.raw.txt: Raw premise tree from symbolic execution.xxx.premise_tree.txt: Refined premise tree (human-friendly form).xxx.defset.txt: The list of DefineSets (splits). The index here maps to.{split}.xxxfiles below (split indices start at 0).
Per split (for each .{split}):
-
xxx.{split}.cu.c: The compilation unit (all needed#includes inlined). -
xxx.{split}.cpp2c: Maki's macro analysis on the compilation unit. -
xxx.{split}.cpp2c.ranges.json: Complemented conditional range summary using information across splits. -
xxx.{split}.seeded.cu.c: The compilation unit with Hayroll's macro tags (seeds). -
xxx.{split}.seeded.rs:.{split}.seeded.cu.ctranslated to Rust by C2Rust (expanded macros plus seeds). -
xxx.{split}.reaped.rs: Hayroll Reaper's Rust output for this split (pre-merge). -
xxx.{split}.merged.rs: Cumulative merge result up to this split (present for splits after the first), useful for inspecting the merging process. -
xxx.rs: The final merged Rust output across all splits, after running the Hayroll Cleaner to strip any leftover seeds or scaffolding. This is the cleaned form of the last.{split}.merged.rs.
This section shows how to run Hayroll on version 1.2.0 of the LibmCS mathematical library.
If you installed Hayroll via prerequisites.bash, this step should have already
been done automatically.
git clone --branch 1.2.0 https://gitlab.com/gtd-gmbh/libmcs.git
cd libmcs
# Passing an explicit empty string to --cross-compile prevents the script
# from prompting for a tool-chain path; all other options are disabled to
# match Hayroll’s requirements.
./configure \
--cross-compile="" \
--compilation-flags="" \
--disable-denormal-handling \
--disable-long-double-procedures \
--disable-complex-procedures \
--little-endianmake clean && bear -- makeThis command creates a compile_commands.json file of this form:
[
{
"arguments": [
"/usr/bin/gcc",
"-c",
"-Wall",
"-std=c99",
"-pedantic",
"-Wextra",
"-frounding-math",
"-g",
"-fno-builtin",
"-DLIBMCS_FPU_DAZ",
"-DLIBMCS_WANT_COMPLEX",
"-Ilibm/include",
"-Ilibm/common",
"-Ilibm/mathd/internal",
"-Ilibm/mathf/internal",
"-o",
"build-x86_64-linux-gnu/obj/libm/mathf/sinhf.o",
"libm/mathf/sinhf.c"
],
"directory": "/home/<username>/libmcs",
"file": "/home/<username>/libmcs/libm/mathf/sinhf.c",
"output": "/home/<username>/libmcs/build-x86_64-linux-gnu/obj/libm/mathf/sinhf.o"
},
...
]LibmCS uses complex numbers, but c2rust does not have full support for complex
numbers.
If you installed and configured LibmCS via prerequisites.bash, it is expected
that no entires in compile_commands.json should point to source files under
libm/complexf/.
In case you configured it manually, please remove such entries.
/PATH/TO/hayroll compile_commands.json hayroll-output/In the hayroll-output/ directory, you will find files such as xxx.rs.
To see the difference between Hayroll and C2Rust output, you can use diff to
compare the intermediate and final Rust files:
diff hayroll-output/xxx.{split}.seeded.rs hayroll-output/xxx.rsxxx.{split}.seeded.rsis generated by C2Rust and contains Rust code with C macros expanded and tagged by Hayroll. The macros are expanded as plain code, but Hayroll inserts special markers (seeds) to indicate macro regions.xxx.rsis the final output from Hayroll, where macros have been extracted and converted into Rust functions or macros based on the tags. This file is typically much simpler and more readable than the direct C2Rust output.
Hayroll is published at PLDI 2026. You can find the paper at here.
@inproceedings{peng2026hayroll,
author={Peng, Haoran and Kasikci, Baris and Bernstein, Gilbert Louis and Ernst, Michael D.},
title={{H}ayroll: A Modular Wrapper for Translating {C} Macros and Conditional Compilation to {R}ust},
booktitle = {PLDI 2026: Proceedings of the {ACM} {SIGPLAN} 2026
Conference on Programming Language Design and Implementation},
address = {Boulder, CO, USA},
month = jun,
year = {2026}
}