Skip to main content

Module codegen

Module codegen 

Source
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 "…".