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 cinrs::c99;
c99! {
#include <stdio.h>
typedef struct { double x, y; } Vec2;
double dot(Vec2 a, Vec2 b) {
return a.x * b.x + a.y * b.y;
}
void greet(const char *name) {
printf("hello, %s\n", name);
}
__attribute__((cinrs_safe)) int fact(int n) {
return n == 0 ? 1 : n * fact(n - 1);
}
}
fn main() {
// C functions are foreign functions: calling one is `unsafe`…
let d = unsafe { dot(Vec2 { x: 1.0, y: 2.0 }, Vec2 { x: 3.0, y: 4.0 }) };
unsafe { greet(c"cinrs".as_ptr()) };
// …unless the C says `cinrs_safe`, and then `rustc` checks the body.
println!("dot = {d}, fact(10) = {}", fact(10));
}That is examples/readme.rs: cargo run --example readme.
-
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, andsetjmp/longjmpas a Rust unwind. What has no honest translation is a located error, never a guess. -
The SIMD intrinsics, by name.
#include <immintrin.h>and write_mm_add_epi32(a, b): 6,081 of Intel's intrinsics, SSE through AVX2 and AVX-512 with FMA, AES, GFNI, VAES, SHA, RDRAND 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.
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.
| 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.
cinrs::c99! { int twice(int x) { return 2 * x; } } // raw tokens
cinrs::c99! { r#" double eight(void) { return 0x1p3; } "# } // a string literal
cinrs::include_c99!("vendor/parser.c"); // a fileRaw 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.
- 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.
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:
#pragma cinrs system_include
#include <unistd.h>
#include <sys/stat.h>See system headers, and the pragma reference for
all eight #pragma cinrs options and the environment variables that go with
them.
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.
mod z {
cinrs::c99! {
#pragma cinrs system_include
#pragma cinrs link "z"
#include <zlib.h>
/* `deflateInit` is a macro, so it needs a line of C — written here. */
int z_deflate_init(z_stream *s, int level) { return deflateInit(s, level); }
enum { ZDEMO_OK = Z_OK };
}
}
// unsafe { z::crc32(0, buf.as_ptr(), buf.len() as z::uInt) }More in calling a C library from Rust.
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
fn main() {
println!("cargo:rustc-env=CINRS_TARGET={}", std::env::var("TARGET").unwrap());
}Without it a cross build fails a compile-time assertion instead of computing the wrong thing. Cross-compilation has the supported targets.
ccinrs is the same front end as a command with GCC's command line:
each C file becomes Rust, which rustc compiles and links, so a C project
builds with make CC=ccinrs or CMake told that ccinrs is its C compiler —
with Rust's run-time checks on, and a panic naming the C file and line. It
takes -c, -E, -M and its relatives, -shared, --target (WebAssembly
through wasm32-wasip1 and -p2 included) and -march; lz4, cJSON, cmark and
brotli build with their own Makefiles and CMake files and pass their own
tests.
cargo install ccinrs
make CC=ccinrs
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.
The ones most likely to matter; the full list has the rest.
- Not supported, each as a located error: the memory
operands and
asm 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.
| 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,654 of 1,769 (93.5 %) | gnu11! |
| Clang's C conformance tests — what must be refused, line by line | 169 of 203 (83.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.
| 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 |
ccinrs |
the C compiler: its options, defaults, targets and shared libraries, and the projects it has built |
| API documentation | the macro reference: c99! and each of its siblings |
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.
Licensed under either of
at your option.
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.