Source-linked AI summary
Lifting the Preprocessor with Oxidize: Structure-Preserving C-to-Rust Translation (Technical Report)
Robbe De Greef, Théo Engels, Felix Van den Broucke, Ken Hasselmann, Antonio Paolillo
TL;DR
C-to-Rust translation often loses the macro structure developers wrote, while a route from structure preservation toward safe Rust remains a design question. Oxidize retains macros through an IR-based translation pipeline, and its output runs within 10% of Clang on 24 of 32 benchmarks, with runtime and memory comparisons against C2Rust showing no systematic preservation cost.
Problem
Oxidize addresses C-to-Rust translation while preserving macros as source-level structure rather than expanding them away.
Source: A Oxidize Implementation Details · p. 2Method
Oxidize uses an IR with incremental passes to retain macro structure and provide a basis for later safety-refinement passes.
Source: A Oxidize Implementation Details · p. 2, E Safety Roadmap · p. 6Results
Oxidize is within 10% of Clang on 24 of 32 benchmarks, and its runtime profile matches C2Rust’s while memory use nearly matches C2Rust’s.
Source: F Performance Overhead · p. 9Takeaways & Limitations
The IR-integrated route preserves abstractions for safety analyses, with proposed analyses including pointer provenance, alias sets, cast kinds, and conservative safe-macro classification.
Source: E Safety Roadmap · pp. 6–7
Abstract
from arXiv · showhide
This technical report accompanies our EuroSys 2027 paper "Lifting the Preprocessor with Oxidize: Structure-Preserving C-to-Rust Translation" (DOI 10.1145/3842654.3848533). Oxidize is a C-to-Rust translator that retains C macros as Rust macros instead of expanding them away, so that the translated code keeps the abstractions its developers wrote. The report collects material the paper cites but has no room for: the translator's intermediate representation, pass structure and back-end emission; the details of macro typing through helper traits and the generic cast macro; the sources and pinned revisions of the nine evaluated C codebases; the per-codebase macro population and how macros are attributed to classes; a roadmap from structure-preserving translation toward safe Rust; the performance overhead study and its methodology; and the configuration and prompt used for the LLM-based baselines.
A Oxidize Implementation Details
Oxidize uses a three-phase architecture centered on a source-level IR, with incremental passes that preserve macro structure while progressively translating C constructs into Rust. The appendix details the IR, pass schedule, control-flow restructuring, and Rust emission.
A Oxidize Implementation Details
- An explicit IR separates Oxidize’s front end, currently empty middle end, and Rust-emitting back end, supporting staged macro transformations and extension to other languages.The IR preserves source-level constructs relevant to maintainability rather than targeting machine code.Source: A Oxidize Implementation Details · p. 2
- Small, incremental IR passes handle macro contraction, nested-macro resolution, hygiene, and typing, enabling incremental validation and extension without rearchitecting the translator.Back-end passes convert contracted macros to Rust syntax or generate helper macros where direct IR constructs are insufficient.Source: A Oxidize Implementation Details · p. 2
- Oxidize’s AST-like IR mirrors Rust while admitting C-only constructs, then uses marker types and dedicated passes to keep those constructs out of the back end.Macro definitions and invocations remain first-class nodes; the marker-free IR is the same tree at a different type, and surviving markers are caught during conversion.Source: A Oxidize Implementation Details · p. 2, Table 1 · p. 3
- Each translation unit passes through six fixed stages, including marked-IR conversion, front-end lowering and macro checks, unmarking, unmarked-IR passes, an empty middle end, and back-end preparation.Four whole-program passes then unpack helper modules, build the Rust module tree, generate a main trampoline when applicable, and rename conflicting module names; the pipeline has 41 passes overall.Source: A Oxidize Implementation Details · pp. 2–3
- A separate stackifier restructures function bodies into basic blocks, using dispatch nodes for irreducible loops with multiple entries.Dominator information and topological order rebuild loops, if-scopes, and forward jumps as labelled blocks with break, supporting goto and switch.Source: A Oxidize Implementation Details · pp. 3–4
- The front end parses C with libclang’s detailed preprocessing record and a dedicated macro-body grammar; the back end emits Rust ASTs, macro_rules! definitions, helper traits, and formatted crate files.Each C file becomes a Rust file, and helper code is emitted as source rather than linked from a runtime library.Source: A Oxidize Implementation Details · p. 4
B Macro Typing Details
Macro typing uses generated helper traits to encode C operator behavior across supported operand types, while a generic cast macro dispatches among cast kinds. The designs keep shared logic centralized and allow cast-kind extensions.
B Macro Typing Details
- Generated helper traits cover 16 binary and three unary operators, with implementations across supported integer, floating-point, and pointer types that apply C’s implicit conversion rules.The cited C counterpart is [5, §6.3.1.8].Source: B Macro Typing Details · p. 4
- A single pre-generated helper-trait source avoids maintaining a separate Rust macro for every operand-type pair and avoids regenerating that module when downstream developers add a type.Macros outside the pre-generated table fall back to conservative expansion.Source: B Macro Typing Details · p. 4
- The generic helper_cast macro dispatches on six cast kinds, including regular, multicast, and transmute, and can host added cast semantics without changing the translator core.The cast-kind taxonomy is extensible under the same macro name.Source: B Macro Typing Details · p. 4
C Evaluated Codebase Sources
The macro-preservation evaluation uses nine open-source C codebases, with upstream sources and pinned revisions recorded in Table 2. The table also reports each build’s C-file and line counts.
C Evaluated Codebase Sources
- The evaluation’s nine real-world C codebases are open source, and two—FIGlet and DOOM—also illustrate the macro-class distribution.Their upstream URLs and evaluation-pinned commits are listed in Table 2.Source: C Evaluated Codebase Sources · p. 4, Table 2 · p. 5
- Table 2 records upstream sources, pinned revisions, and each codebase’s C source-file and line counts, totaling 146 files and 141 kLoC.Its rows are ordered by decreasing source-file count, matching the figures’ order.Source: Table 2 · p. 5
D Per-Codebase Macro Population
The nine-codebase macro census is dominated by constant literals, but identifier-valued object-like and function-like macros also form substantial parts of the population. Class attribution accounts for names whose definitions differ across files or conditional-compilation branches.
D Per-Codebase Macro Population
- 1,519 of 2,862 macro definitions (53%) are constant literals, the largest class across the nine codebases.These object-like macros have a single integer, string, character, or other literal as their body.Source: D Per-Codebase Macro Population · p. 6
- Identifier-valued object-like macros account for 502 definitions (18%) and 2,700 call sites, and translators retain them less often than other object-like macros.On the shared evaluation intersection, C2Rust retains 150 of 283 (53%), rust-bindgen 167 (59%), and Oxidize 250 (88%).Source: D Per-Codebase Macro Population · p. 6
- The codebases contain 277 function-like expression macros and 95 statement macros; libpng alone has 190, including 175 expression-like and 15 statement-like.Four codebases declare more than 25 function-like macros each, and they form a substantial share of the macro surface in every codebase larger than FIGlet.Source: D Per-Codebase Macro Population · p. 6
- Figure 1 counts each call site once and attributes it to the definition visible from its file; two branch-dependent names are excluded rather than assigned by guesswork.The 19 disputed sites are excluded from the figure’s call-site totals.Source: D Per-Codebase Macro Population · p. 6
E Safety Roadmap
The roadmap places safety refinement in Oxidize’s currently empty middle end, operating over an IR that retains macros and other abstractions. It proposes analysis and conservative macro classification, while presenting the IR-integrated route as prospective and unevaluated.
E Safety Roadmap
- Prior approaches pursue safety refinement, but the cited Scylla, CROWN, and refinement-pass work does not address macro preservation.The roadmap incorporates such techniques as middle-end passes over Oxidize’s IR.Source: E Safety Roadmap · p. 6
- Unlike a post-translation wrapper, an IR-integrated pass can analyze and rewrite a macro once at its definition; the authors expect this route to safety refinement to be more direct, but have not evaluated it.The two routes are described as complementary ways to achieve structure preservation.Source: E Safety Roadmap · pp. 6–7
- Cpp2Rust starts from safe Rust using runtime wrappers and removes wrappers when intra-procedural analysis proves them unnecessary, trading off structure preservation for that safety guarantee.Oxidize’s proposed route instead retains structure for future static safety refinement, while unsafe code remains in the initial translation.Source: E Safety Roadmap · p. 7
- Planned middle-end passes cover pointer provenance, alias sets, cast-kind disambiguation, and recognizers for recurring macro-level ownership idioms.The passes would promote pointers when provenance permits, use alias information to guide borrow-check-friendly translations, replace some unchecked casts, and rewrite recognized resource patterns.Source: E Safety Roadmap · p. 7
- The trivially-safe macro classifier labels a preserved definition “yes” only under a conservative scalar-operation criterion; other cases remain “don’t know.”Raw pointers, unions, mutable statics, FFI calls, and unsafe-function calls are among the cases assigned “don’t know.”Source: E Safety Roadmap · p. 7
F Performance Overhead
The performance study compares Oxidize with C2Rust, native compilers, and idiomatic Rust across 32 C programs. Oxidize is within 10% of Clang on 24 benchmarks, while its memory profile closely matches C2Rust.
F Performance Overhead
- Comparisons include C2Rust, Clang, GCC, and hand-written idiomatic Rust, with release optimization settings for each tool.The hand-written Rust baseline is included to separate translation overhead from differences inherent to the target language.Source: F Performance Overhead · p. 7
- The 32 programs comprise microbenchmarks, application-style programs, and macro-intensive sorting benchmarks, including seven sorting algorithms with 27 macro definitions.The macro-intensive suite measures preserved macros on hot paths as well as conservatively expanded statement and loop-header macros.Source: F Performance Overhead · pp. 7–8, Table 3 · p. 9
- 24 of 32 benchmarks put Oxidize within 10% of Clang at runtime; it is faster on six and more than 10% slower on two, while C2Rust has a similar profile.The two slower cases are lcs and fib; C2Rust is within 10% on 25 of 32 benchmarks and is more than 10% slower on the same two.Source: Figure 2 · p. 8, F Performance Overhead · p. 9
- Twelve of 32 benchmarks use 1.7–4.3× more memory under Oxidize than Clang, while Oxidize’s median memory difference from C2Rust is 0.01.On those twelve benchmarks, C2Rust and, where measured, idiomatic Rust use comparable or larger multiples; nbod reaches 6.4–6.7×.Source: Figure 2 · p. 8, F Performance Overhead · p. 9
- Idiomatic-Rust measurements are unavailable for A* search, the pi approximation, branch sort, and bucket sort.The study ran on one host, using the container’s nightly Rust compiler pinned to nightly-2025-09-25 for reproduction.Source: F Performance Overhead · p. 9
- The study measures runtime as median execution time relative to Clang’s mean and memory as average lifetime consumption, using ten executions per benchmark.Runtime processes were pinned to a fixed CPU core; memory was measured with Valgrind massif and includes stack usage.Source: F Performance Overhead · p. 9
G LLM Prompt Template
The report reproduces the identical prompt template used for Claude and Codex and specifies their execution environments, model settings, and run limits.
G LLM Prompt Template
- Claude and Codex use the same prompt template, with Claude pinned to anthropic/claude-opus-4.7 and Codex to openai/gpt-5.3-codex at high reasoning effort.Both tools access their models through OpenRouter; Codex runs in a freshly initialized Git repository.Source: G LLM Prompt Template · pp. 9–10
- Each translation runs in a fresh temporary directory with the source tree linked as project/, though the prompt itself does not mention that link.The directory contains input.c, tests.json (empty when tests are unavailable), and the requested output file.Source: G LLM Prompt Template · pp. 9–10
- Each run has a 600-second per-program cap, and exceeding it, exiting non-zero, or omitting the output file counts as failure.Successful outputs are compiled, run, and compared like other tools; each program is run once, and the tools are non-deterministic.Source: G LLM Prompt Template · p. 10
- C2SaferRust uses pinned C2Rust and repair-pipeline revisions, calling gpt-4o-mini through OpenRouter under the same 600-second cap.The report distinguishes this non-agentic tool from the two agentic prompt-based translators.Source: G LLM Prompt Template · p. 10
- The shared prompt requests a complete standalone Rust 2021 program, behavior preservation, standard-library-only code, and compilation and testing when possible.It permits unsafe blocks only when needed to preserve C semantics and specifies the output path.Source: G LLM Prompt Template · p. 10