Expand description
Code generation: the typed ir becomes Rust tokens.
Every token this module emits is stamped with the span of the C construct
it came from, resolved through the SourceMap. That is what makes an
error rustc raises about the generated code — a call with the wrong
argument type, say — point at the C the user actually wrote.
§Shape of the output
One expansion produces, in this order: the alignment wrappers an
over-aligned object needs, the struct and union items for every tag and
the flexible-array companions that go with them, the enum aliases and
their constants, the file-scope typedef aliases, an empty extern block
per library the unit links, one extern block for everything the unit only
declares, the static mut items, and finally the functions. A C function
becomes
pub unsafe extern "C" fn name(mut p: ::core::ffi::c_int) -> ::core::ffi::c_int {
unsafe { … }
}pub is dropped for a C static function, which is private to the module
crate::expand wraps all of this in — that module is what makes C’s
internal linkage mean something, and what lets two c99! blocks in one
Rust module both #include the same header. #[inline] is added for an
inline function, and the body is wrapped in a single unsafe block
because edition 2024 no longer treats the body of an unsafe fn as an
unsafe block. Nothing here carries a lint exemption of its own: everything
this module generates goes inside that one module, whose head carries a
single #![allow(…)] — an inner attribute, so every item under it
inherits it — for everything a naive translation provokes: unused
bindings, redundant parentheses, non-Rust naming, code a human can see is
unreachable, and so on. The list, and the reasons for each entry, are with
the code in lib.rs that wraps a unit in its module.
A function the unit marked safe —
[[cinrs::safe]], __attribute__((cinrs_safe)) or #pragma cinrs safe —
is the one exception: it is generated as pub extern "C" fn and its body is
not wrapped, so rustc checks every operation the translation of the C
needs and refuses the ones that are unsafe, with the caret on the C.
A unit that asked for #pragma cinrs export gives everything with external
linkage #[unsafe(no_mangle)] on top of that, so that its functions and
objects are real C symbols another unit can link against — with C’s own
risk of two definitions of one name, which only the linker will see.
§Pointers, arrays and records
Pointers are raw pointers — T * is *mut T, const T * is *const T,
void * is *mut c_void — and pointer arithmetic is offset, so nothing
in the output holds a reference and none of Rust’s aliasing rules are
involved. Arrays are [T; N] and decay to a pointer through
(&raw mut a).cast::<T>(), which is a raw pointer from the start; a &mut
would both change the meaning and trip static_mut_refs on a global. A
function pointer is Option<unsafe extern "C" fn(…) -> R>, so that a null
one is representable, and a call through it unwraps first.
§Places
Assignment, compound assignment, ++/-- and & all go through this
module’s place lowering, which turns an ir::Place into a setup
(statements that must run first, where the pointer arithmetic lands) plus an
access (a Rust place expression that may be evaluated more than once).
p[i()] += 1 therefore evaluates i() exactly once, and s.f, p->f and
a[i][j] are all the same three lines of code.
§Bit-fields
A bit-field has no address, so it is not a field of the generated item: a
run of them shares one [u8; K], and sema has already worked out which
bytes and bits each member owns (see crate::sema’s layout). This module
turns that into a pair of inherent methods per named member — plain inline
integer code, no helper type — and a place whose access is the record
rather than the member, read with .f() and written with .set_f(v). A
constant initialiser is folded into the storage bytes here, which is what
lets a static hold one; a non-constant one becomes a zeroed literal
followed by setter calls. A place rooted in a static mut goes through
&raw mut first, because the accessors borrow.
§Loops
Every loop gets a unique Rust label so that break and continue never
depend on where they sit relative to a switch:
while (c) B 'lN: while c { B } continue → continue 'lN
do B while (c); 'lN: loop { 'lN_body: { B } continue → break 'lN_body
if !(c) { break 'lN } }
for (i;c;s) B { i; 'lN: loop { if !(c) { break 'lN }
'lN_body: { B } s; } }The body label exists exactly where continue has work to do afterwards —
re-testing the condition of a do/while, or running the step of a for.
§Switch
Fallthrough is what makes switch interesting: control enters at one label
and then runs through every group after it. Rust has no such construct,
but labelled blocks compose into one. For groups g0 … gN the output is
'swK: {
'swK_cN: { … 'swK_c1: { 'swK_c0: { match v { … } } g0 } g1 … }
gN
}with the dispatch match innermost: break 'swK_ci lands immediately
before group i, and control then falls out of each enclosing block in
turn, running the groups after it in order. break inside the switch is
break 'swK, and the _ arm goes to the default: group, or out of the
whole statement when there is none.
§Functions that jump
Most gotos stay here too. regions has wrapped the
statements a label divides in an ir::Region, which is one of
'done: { … break 'done; … } 'retry: loop { … continue 'retry; … break 'retry; }named after the C label — with the number of the label appended when Rust
cannot spell the name as a label (loop: is a keyword) or when this module
already gives that name to a loop of its own. A Stmt::Goto left inside
one is the break or the continue; which it is comes from the region it
stands in.
A body sema lowered into a control-flow graph instead — because it holds a jump Rust cannot make at all — is emitted as a state machine over its blocks, with the function’s locals defined once at the top. Everything below the statement level is shared: the expressions, places and conversions are generated by exactly the same code in both modes.
§Calls without a prototype
int f(); says nothing about the parameters (C99 6.7.5.3p14), so the item
generated for it is unsafe extern "C" fn() -> R — that is all the
declaration said. What the call passes is decided at the call site
(6.5.2.2p6): sema has already applied the default argument promotions to
every argument, and this module transmutes the callee to the signature they
make before calling it,
::core::mem::transmute::<unsafe extern "C" fn() -> R,
unsafe extern "C" fn(T1, …, Tn) -> R>(f)(a1, …, an)with the Option unwrapped first for a function pointer, and no cast at all
when there are no arguments. Reinterpreting a function pointer like this is
exactly the contract C’s own ABI rests on: on every ABI this crate targets
the address is the same one, and the call is defined precisely when the
callee really was defined with parameters of those promoted types —
undefined otherwise, which is the risk the program took by leaving the
prototype out. The same route is taken whenever the argument count and the
parameter count disagree at all, which keeps a call checked
against an empty list from being emitted against a prototype a later
declaration supplied.
§Variadic definitions
R f(T a, ...) becomes unsafe extern "C" fn f(mut a: T, __cinrs_va: ...).
The extra parameter is the argument list as the caller left it and is never
advanced; va_start and every va_list local copy it, va_arg is
next_arg, va_copy and passing a list on are clone, and va_end is
nothing at all, because the list ends when its value is dropped. See
crate::sema’s va module for the model.
Constants§
- DOLLAR
- What a
$in a C identifier is written as in the generated Rust.
Functions§
- generate
- Generates the Rust items for a fully checked program.
- generate_
stubs - Generates signature-only items for a program that did not type check.
- is_
crate_ path - Whether a string is usable as the Rust path of a crate:
#pragma cinrs crate "…".