Skip to main content

Crate cinrs

Crate cinrs 

Source
Expand description

§cinrs — write C inside Rust

CI crates.io docs.rs

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.

§Why

  • No C toolchain in the build. The macro is the compiler: no build.rs, no cc, no bindgen, nothing to install on the build machine. The standard headers are bundled, and a call such as printf links against the platform’s C library like any other extern "C" declaration.

  • What the C defines is Rust. A struct is a #[repr(C)] type you can construct, a function is an extern "C" fn you can call, a global is a static. 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 cargo and rust-analyzer put the caret where the mistake is — for this crate’s diagnostics and for rustc’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), switch with fallthrough, bit-fields with GCC’s layout, variable length arrays, variadic functions, _Complex, _Atomic, <threads.h>, K&R definitions, the whole preprocessor with #include and #embed — and the GNU extensions real code uses: statement expressions, typeof, __attribute__((cleanup)), nested functions, __int128, __builtin_*, and inline assembly, for the operand kinds asm! 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 onto core::arch::x86_64, whose signatures the bundled headers were generated from. __m128i punnes through a union like any 16-byte type, GCC’s a * b, v[i] and {a, b} on the vector types are the intrinsics that do the same, an immediate operand becomes core::arch’s const generic, and __attribute__((target("avx2"))) becomes #[target_feature]. BLAKE3’s C implementation — four SIMD kernels and a cpuid dispatcher 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 without unsafe, so rustc checks the translation and Rust calls it as fact(10).

  • core-only output. The expansion names nothing but core — a variable length array needs alloc, and a _Thread_local object needs std — 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

C89C99C11C17C23
ISO Cc89!c99!c11!c17!c23!
with GNU extensionsgnu89!gnu99!gnu11!gnu17!gnu23!
a whole .c fileinclude_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

ⓘ
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 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 the unsafe.
  • A C name that is a Rust keyword is a raw identifier: int match(int) is called as r#match(1). More on names.
  • A bit-field has no address, so it is a pair of methods: h.length() and h.set_length(200).
  • Each invocation is one translation unit, expanded into a private module that is glob re-exported; an ordinary Rust mod around the invocation gives its items a path (packet::Header). #pragma cinrs export gives 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:

#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.

§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.

ⓘ
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.

§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
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.

§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 and asm goto of inline assembly (which is otherwise core::arch::asm!, x86 only), the GNU vector extensions (the Intel intrinsics are the SIMD that is here), _BitInt, _Imaginary, an _Atomic aggregate.
  • long double is double.
  • 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 alloca live on the heap, in a bump arena per function call (Rust cannot move the stack pointer); what the C can observe is unchanged.
  • va_arg of a struct works 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 struct in it.
  • The platform’s include directories are not searched unless the unit asks.

§Conformance and speed

CorpusCorrectEntry point
c-testsuite — whole programs with expected output215 of 218 (98.6 %)c99!
GCC’s C torture tests — 1,776 self-checking programs1,643 of 1,769 (92.9 %)gnu11!
Clang’s C conformance tests — what must be refused, line by line167 of 203 (82.3 %)per test
glibc’s own headers through the front end66 of 67gnu11!, 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 worksthe language, the preprocessor, the extensions, construct by construct
What the C becomeswhat is generated for each construct — signatures, accessors, the module — for Rust code on the other side
Limitationswhat is refused, and what differs from a C compiler
C standard status · GNU extensionsfeature tables
Pragmas#pragma cinrs …, the other pragmas, the attributes, the environment variables
Including a C fileinclude_c99! and its siblings
System headers/usr/include and what glibc’s headers do here
Cross-compilation · no_stdtargets and data models; what the expansion needs
Conformance · Benchmarks · Real programshow the numbers above are measured; SQLite, BLAKE3, xxHash and CRoaring against gcc -O2 and clang -O2
API documentationthe 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.

Re-exports§

pub use cinrs_rt as rt;

Macros§

c11
Compiles a C11 translation unit written inside Rust.
c17
Compiles a C17 translation unit written inside Rust.
c23
Compiles a C23 translation unit written inside Rust.
c89
Compiles a C89 (C90) translation unit written inside Rust.
c90
Compiles a C90 translation unit written inside Rust.
c99
Compiles a C99 translation unit written inside Rust.
gnu11
Compiles a C11 translation unit with the GNU extensions switched on.
gnu17
Compiles a C17 translation unit with the GNU extensions switched on.
gnu23
Compiles a C23 translation unit with the GNU extensions switched on.
gnu89
Compiles a C89 translation unit with the GNU extensions switched on.
gnu99
Compiles a C99 translation unit with the GNU extensions switched on.
include_c11
Compiles the C11 file at path; see include_c99! and c11!.
include_c17
Compiles the C17 file at path; see include_c99! and c17!.
include_c23
Compiles the C23 file at path; see include_c99! and c23!.
include_c89
Compiles the C89 (C90) file at path.
include_c90
Compiles the C90 file at path; see include_c99! and c90!.
include_c99
Compiles the C99 file at path — one translation unit, exactly as if its text had been written inside c99!.
include_gnu11
Compiles the C11 file at path with the GNU extensions switched on; see include_c99! and gnu11!.
include_gnu17
Compiles the C17 file at path with the GNU extensions switched on; see include_c99! and gnu17!.
include_gnu23
Compiles the C23 file at path with the GNU extensions switched on; see include_c99! and gnu23!.
include_gnu89
Compiles the C89 file at path with the GNU extensions switched on; see include_c99! and gnu89!.
include_gnu99
Compiles the C99 file at path with the GNU extensions switched on; see include_c99! and gnu99!.