cinrs — write C inside Rust
cinrs is a procedural macro that takes C source written directly in a Rust
file — C89 to C23, with the GNU extensions — translates it to Rust while the
crate compiles, and hands you what it defines as ordinary Rust items.
use c99;
c99! Vec2;
double dot
void greet
__attribute__ int fact
}
That is examples/readme.rs: cargo run --example readme.
Why
-
No C toolchain in the build. The macro is the compiler: no
build.rs, nocc, nobindgen, nothing to install on the build machine. The standard headers are bundled, and a call such asprintflinks against the platform's C library like any otherextern "C"declaration. -
What the C defines is Rust. A
structis a#[repr(C)]type you can construct, a function is anextern "C" fnyou can call, a global is astatic. Nothing is declared twice, and nothing crosses an FFI boundary that the optimiser cannot see through. -
Errors point at the C. Every generated token carries the span of the C token it came from, so
cargoand rust-analyzer put the caret where the mistake is — for this crate's diagnostics and forrustc's own:error: use of undeclared identifier 'j' --> src/main.rs:7:25 | 7 | total += xs[j]; | ^ -
Real C, not a subset.
goto(computed ones too),switchwith fallthrough, bit-fields with GCC's layout, variable length arrays, variadic functions,_Complex,_Atomic,<threads.h>, K&R definitions, the whole preprocessor with#includeand#embed— and the GNU extensions real code uses: statement expressions,typeof,__attribute__((cleanup)), nested functions,__int128,__builtin_*, and inline assembly, for the operand kindsasm!has. What has no honest translation (setjmp) is a located error, never a guess. -
The SIMD intrinsics, by name.
#include <immintrin.h>and write_mm_add_epi32(a, b): 6,075 of Intel's intrinsics, SSE through AVX2 and AVX-512 with FMA, AES, GFNI, VAES, SHA and the BMI scalar ones, mapped straight ontocore::arch::x86_64, whose signatures the bundled headers were generated from.__m128ipunnes through aunionlike any 16-byte type, GCC'sa * b,v[i]and{a, b}on the vector types are the intrinsics that do the same, an immediate operand becomescore::arch'sconstgeneric, and__attribute__((target("avx2")))becomes#[target_feature]. BLAKE3's C implementation — four SIMD kernels and acpuiddispatcher in inline assembly — builds unedited and picks AVX-512. -
Measured, not claimed. 98 % of c-testsuite, 89–93 % of GCC's torture tests and 82 % of Clang's C conformance tests — about 2,270 cases, every failure listed by name with its reason, and not one of them a known bug. See Conformance and speed.
-
As fast as a C compiler. Over 48 whole programs the median run time is 1.01× that of
gcc -O2, with byte-identical output — the SIMD-intrinsics entries of the Benchmarks Game included. -
Safety you can opt into. Mark a function
[[cinrs::safe]](or__attribute__((cinrs_safe))) and it is generated withoutunsafe, sorustcchecks the translation and Rust calls it asfact(10). -
core-only output. The expansion names nothing butcore— a variable length array needsalloc, and a_Thread_localobject needsstd— so it works in a#![no_std]crate.
Installation
cargo add cinrs
Rust 1.99 or later — the release that made c_variadic stable, which is
what a variadic function definition is translated to.
Linux, macOS and Windows. Linux (x86-64) is where cinrs is developed and
where the whole test suite and the conformance corpora run; macOS (arm64) and
Windows (x86-64, MSVC) build, link and run the examples and the portable tests
on every push. Other targets are compile-checked; see
Cross-compilation.
Two features: complex (on by default) is C's complex types, as
num_complex::Complex — default-features = false drops it and its one small dependency; nightly moves a diagnostic about
a string-literal body onto a caret inside the literal.
Using it
Pick the language
| C89 | C99 | C11 | C17 | C23 | |
|---|---|---|---|---|---|
| ISO C | c89! |
c99! |
c11! |
c17! |
c23! |
| with GNU extensions | gnu89! |
gnu99! |
gnu11! |
gnu17! |
gnu23! |
a whole .c file |
include_c89! |
include_c99! |
include_c11! |
include_c17! |
include_c23! |
include_gnu89! … include_gnu23! exist too. A feature of a later revision
used in an earlier block is an error that names the macro to write instead.
Everything spelled with a leading double underscore (__attribute__,
__typeof__, __builtin_*) works in the strict macros as well, exactly as in
gcc -std=c99.
Three ways to write the C
c99! // raw tokens
c99! // a string literal
include_c99!; // a file
Raw tokens give the best diagnostics. The few things Rust's lexer refuses —
hexadecimal floats, 'ab', L"…", \ line continuations, ## — go in a
string literal, and C that already lives in a file goes in whole. Details:
input forms, including a C file.
Calling it from Rust
- Functions are
pub unsafe extern "C" fn; safe functions drop theunsafe. - A C name that is a Rust keyword is a raw identifier:
int match(int)is called asr#match(1). More on names. - A bit-field has no address, so it is a pair of methods:
h.length()andh.set_length(200). - Each invocation is one translation unit, expanded into a private module that
is glob re-exported; an ordinary Rust
modaround the invocation gives its items a path (packet::Header).#pragma cinrs exportgives its functions real C symbols, so another block — or a C library — can call them.
Headers
The ISO C standard headers are bundled and written against a model of the
target, so a block means the same thing on every machine. Your own headers are
found next to the .rs file (#pragma cinrs include_path "…" adds
directories), and editing one rebuilds the crate. POSIX and the platform's own
headers — <unistd.h>, <pthread.h>, struct stat, DIR — are one pragma
away:
See system headers, and the pragma reference for
all eight #pragma cinrs options and the environment variables that go with
them.
Calling a C library
A header's declarations are the binding: a function a header declares and the unit does not define is callable from Rust under its own C name, and so are the header's types.
// unsafe { z::crc32(0, buf.as_ptr(), buf.len() as z::uInt) }
More in calling a C library from Rust.
Cross-compilation
sizeof, layouts and #if are worked out while the macro expands, from a
model of the target. A procedural macro cannot ask rustc what the target is,
so a crate that is cross-compiled says so from its build script:
// build.rs
Without it a cross build fails a compile-time assertion instead of computing the wrong thing. Cross-compilation has the supported targets.
What is supported
What works is the tour, construct by construct.
C standard status is the table — every feature of C89 to C23 with
its state, in the style of Clang's c_status — and GNU
extensions is the same for GCC's.
Limitations
The ones most likely to matter; the full list has the rest.
- Not supported, each as a located error:
setjmp/longjmp, the memory operands andasm gotoof inline assembly (which is otherwisecore::arch::asm!, x86 only), the GNU vector extensions (the Intel intrinsics are the SIMD that is here),_BitInt,_Imaginary, an_Atomicaggregate. long doubleisdouble.- The SIMD intrinsics are x86's, and only the baseline instruction set is
predefined: a procedural macro cannot see
-C target-feature, so#ifdef __AVX2__and#ifdef __AVX512F__are false (until a#pragma GCC target("avx2")defines them, as GCC's does) and__builtin_cpu_supports("avx2")is the question to ask. No MMX. - Variable length arrays and
allocalive on the heap, in a bump arena per function call (Rust cannot move the stack pointer); what the C can observe is unchanged. va_argof astructworks for records up to sixteen bytes on x86-64 System V only.- Each invocation is its own translation unit: two blocks that include one
header get two distinct Rust types for each
structin it. - The platform's include directories are not searched unless the unit asks.
Conformance and speed
| Corpus | Correct | Entry point |
|---|---|---|
| c-testsuite — whole programs with expected output | 215 of 218 (98.6 %) | c99! |
| GCC's C torture tests — 1,776 self-checking programs | 1,643 of 1,769 (92.9 %) | gnu11! |
| Clang's C conformance tests — what must be refused, line by line | 167 of 203 (82.3 %) | per test |
| glibc's own headers through the front end | 66 of 67 | gnu11!, c11! |
"Correct" means the case passed, or the entry point is required to refuse it and did. Every remaining case is listed by name as unimplemented or not planned — not one is tagged as a bug. The conformance suites says how they are run.
Benchmarks: 48 whole C programs — the single-threaded C entries
of the Benchmarks Game, including the eight written with SIMD intrinsics,
Dhrystone, Whetstone and two dozen kernels that isolate one construct each —
built as gcc -O2, clang -O2 and a cinrs block under
rustc -C opt-level=3. The median cinrs/gcc ratio is 1.01×, 42 of the
48 are within 10 % of gcc or faster, and every output is identical across the
three builds. A goto costs nothing: an outward one is a labelled break or
continue, and anything else is read back into loops and branches by a
relooper, so an interpreter loop written as a switch full of gotos — the
SQLite VDBE, say — runs at the speed gcc gives it.
Documentation
| What works | the language, the preprocessor, the extensions, construct by construct |
| What the C becomes | what is generated for each construct — signatures, accessors, the module — for Rust code on the other side |
| Limitations | what is refused, and what differs from a C compiler |
| C standard status · GNU extensions | feature tables |
| Pragmas | #pragma cinrs …, the other pragmas, the attributes, the environment variables |
| Including a C file | include_c99! and its siblings |
| System headers | /usr/include and what glibc's headers do here |
Cross-compilation · no_std |
targets and data models; what the expansion needs |
| Conformance · Benchmarks · Real programs | how the numbers above are measured; SQLite, BLAKE3, xxHash and CRoaring against gcc -O2 and clang -O2 |
| API documentation | the macro reference: c99! and each of its siblings |
How it works
The macro recovers the C source text of its own invocation (by slicing the
.rs file, or by decoding the string literal) together with a map from byte
offsets back to proc_macro2 spans, then runs a C front end over it — lexer,
preprocessor, parser, semantic analysis with C's conversion rules made
explicit — and emits Rust, c2rust-style: #[repr(C)] records, raw pointers,
wrapping arithmetic where C defines wrap-around, pub unsafe extern "C" fn for
each function. Every token it emits is stamped with the span of the C it came
from, which is what makes the errors land where they should.
0.1 means the macro surface may still change. The companion crates
cinrs-core, cinrs-macros and cinrs-rt are implementation details of this
one and carry no stability promise.
License
Licensed under either of
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.