Skip to content

v0.3.0

Latest

Choose a tag to compare

@tanakh tanakh released this 13 Oct 07:30

0.3.0 — 2026-10-13

This release adds a second way to use cinrs: ccinrs, a C compiler with
GCC's command line
, in a fifth crate versioned with the other four. Each C
file becomes Rust that rustc compiles into an ordinary object, and rustc
links the program, so make CC=ccinrs — or a configure script or CMake told
that ccinrs is the C compiler — builds a C project with no C compiler
involved, and with Rust's run-time checks on. Twenty-seven open-source
programs were built that way from their release tarballs, from bzip2 and
SQLite to git, nginx, Redis, OpenSSL, FFmpeg and CPython, and their own test
suites run; see doc/ccinrs-real-programs.md.
What they needed is most of the rest of this release, and the macros have it
too: setjmp and longjmp, GCC's vector extensions, weak symbols, transparent
unions, thread-local objects shared between units, inline functions that
provide no external definition, volatile accesses that are volatile, and
many smaller fixes.

Upgrading from 0.2

  • An enumeration with no negative enumerator is unsigned int, as in GCC
    and Clang, so the Rust alias of such an enum type is a c_uint where it
    was a c_int; the enumerators stay c_int. Rust code that passes an
    enumerator where the type is expected, or compares a value of the type with
    one, needs an as. See Changed.
  • On macOS an __asm__ label is the assembler's symbol, as in GCC and
    Clang, so it spells Mach-O's leading underscore itself: __asm__("strlen")
    now names the symbol strlen, and the C library's is
    __asm__("_strlen"), which __USER_LABEL_PREFIX__ spells portably. Other
    targets are unchanged. See Fixed.
  • __DATE__ and __TIME__ are the moment of translation, not
    placeholders, so a block that uses them expands differently from one build
    to the next; SOURCE_DATE_EPOCH fixes the moment for a reproducible build.
  • For direct users of cinrs-core: Options and the IR's Program,
    Function, Object, RecordDef, Field, PointerType and ArrayType
    have new public fields, so a struct literal of one needs them; Options::new
    followed by field updates does not. See Added.

Conformance and speed, as measured for this release

  • c-testsuite: 215 of the 218 cases c99! is eligible for are correct
    (98.6 %)
    , 218 of 220 under c23! and every GNU dialect, as in 0.2.0;
    ccinrs runs the suite through its command line to the same 218 of 220.
    See doc/c-testsuite.md.
  • GCC's C torture tests: 1,698 of the 1,769 run are correct (96.0 %)
    under gnu11!, up from 1,643, and 1,693 (95.7 %) under gnu89!, up from
    1,636; most of the gain is cases that use GCC's vector extensions or
    setjmp.
    See doc/gcc-torture.md.
  • Clang's C conformance tests: 169 of the 203 revisions run are correct
    (83.3 %)
    , up from 167, and 582 of the 620 expected-error lines land on
    the right line, up from 557.
  • Not one case in any of the three is tagged [bug].
  • 48 whole C programs built as gcc -O2, clang -O2 and a cinrs block:
    median cinrs/gcc -O2 ratio 1.00×, 42 of 48 within 10 % of gcc -O2
    or faster, every program's output identical across the three builds
    (doc/benchmarks.md, measured on Rust 1.99.0).
    spectral-norm-sse2 is still the one row slower than both compilers
    (1.12×), the price of signed arithmetic that wraps. dhrystone takes 0.83×
    GCC's time but twice Clang's: a local char[31] is now 16-byte aligned as
    the psABI asks (see Changed), which in Rust makes the object 32
    bytes, and the strcmp of it that Clang folds — and cinrs folded before —
    is a call again, as in GCC.
  • ccinrs on real programs: 27 open-source programs built from their
    release tarballs with their own build systems, their own test suites run
    against GCC's build (doc/ccinrs-real-programs.md);
    SQLite, Lua, git, Redis, libuv, zstd, jq and OpenSSL were built and tested
    again with this release, with the same results. With the run-time checks
    on, most take 1.0 to 1.5 times GCC's time, and about GCC's own without
    them; interpreters pay most (CPython 1.70× with the checks, 1.30× without).
    Building takes longer than with GCC: git 2.2× GCC's time at make -j2
    (114.6 s against 53.1 s), libuv 2.1×, SQLite 1.2×.

Added

  • ccinrs, a C compiler with GCC's command line, in a fifth crate
    versioned with the other four; see doc/ccinrs.md, and
    doc/ccinrs-real-programs.md for the 27
    programs it has built. Each C file is translated by cinrs and compiled by
    rustc into an ordinary object, and rustc links the program, so make CC=ccinrs — or CMake told that ccinrs is the C compiler — builds a C
    project with no C compiler involved. cargo install ccinrs installs it,
    and what it needs to build a program is what cargo build needs.
    • GCC's defaults: gnu17, GCC 14.2's predefined macros, the platform's
      headers before the bundled ones, every non-static definition a symbol.
      Rust's run-time checks are on unless -fno-cinrs-checks says otherwise,
      and a panic names the C file and line: the Rust is printed on the C's
      lines. At -O1 and above LLVM's inline thresholds are raised, so that
      the checks do not change which functions are inlined; without the checks
      they still make CPython's interpreter about 3 % faster.
    • -c objects carry the rustc that made them, and a link with another
      refuses them in one sentence; -S writes the Rust; -E prints the
      preprocessed C with GCC's line markers, and -dM the macros;
      -fsyntax-only checks and writes nothing; -M, -MM, -MD, -MMD,
      -MF, -MT, -MQ and -MP write Makefile rules under GCC's names;
      -include and standard input (-) are read as GCC reads them.
    • --target= for another machine, with nothing to install but the Rust
      target: wasm32-wasip1 and wasm32-wasip2; musl, linked by rust-lld
      itself into a static program on a machine with no C compiler at all;
      and wasm32-unknown-unknown, as a module that exports the C's functions
      and a malloc family and imports what the C leaves undefined. -march=
      and -m<feature> as rustc's target CPU and features, with the feature
      macros to match.
    • -flto compiles each file to an rlib and has the link optimise them as
      one program, cinrs's runtime and Rust's standard library included.
    • -shared links an ELF shared library that exports the C symbols, those
      of an archive between -Wl,--whole-archive and -Wl,--no-whole-archive
      included, as libtool links a convenience library into one, and those of
      an object another compiler or an assembler made, read off its symbol
      table;
      -static, -rdynamic, --version, -dumpversion, -dumpmachine, and
      the -print-* questions libtool asks, -Xlinker and -l:file. GCC's
      warning options are taken without a word; an unknown one is a warning,
      and an error under -Werror, which is what a configure probe looks
      for, and which makes every warning an error, #warning included. A
      version script is found however it is given, libtool's
      -Wl,--version-script -Wl,file included, and a wildcard in its global:
      list stands for the C symbols, not for Rust's standard library linked in
      beside them. -E takes a header. -fuse-ld= chooses a
      program's linker (GNU ld for bfd, where rustc's own is LLD), and
      -pie, -no-pie and the hardening options distributions pass are taken.
      /dev/null is a C file like any other, and a file for the linker that
      does not exist is an error even when nothing is linked, as in GCC 15.
    • _Complex works: cinrs-rt's source is compiled with the user's rustc
      the first time a program needs it and kept in a cache directory.
    • scripts/check-ccinrs.sh builds lz4, cJSON, cmark, brotli, zlib and
      expat with their own build systems — Makefiles, CMake, zlib's configure,
      autoconf and libtool — and runs their tests, and runs c-testsuite through
      the command line (218 of 220). -v ends with GCC's gcc version … line,
      which is what a configure script reads to decide that a compiler takes
      GCC's options.
  • setjmp and longjmp, as a Rust unwind: longjmp is resume_unwind
    to the activation the jmp_buf names, and a function that calls setjmp
    runs its control-flow graph inside catch_unwind, with its locals hoisted
    outside — so they keep their latest values — re-entering at the setjmp
    the jump came back to. setjmp, _setjmp, sigsetjmp (signal mask
    included), __builtin_setjmp and their longjmps, from the bundled
    <setjmp.h> (now a real header) or the platform's, and longjmp's address.
    A setjmp where C17 7.13.1.1p4 allows it, plus r = setjmp(buf),
    int r = setjmp(buf), if ((r = setjmp(buf)) == 0) and, in the condition
    of an if or a loop, an operand of && or || (OpenSSL's if (!r || !_setjmp(env))), and anywhere else a located error. libpng's default build, TurboJPEG, xz's unit tests and Lua
    now build and pass their suites with ccinrs, and six gcc-torture cases
    pass. See doc/limitations.md for
    what works and what does not.
    • A longjmp that cannot be done stops the program with a message of
      cinrs's own before anything is unwound: out of a signal handler that
      interrupted the program's own code, onto a buffer whose setjmp has
      returned or ran on another thread or stack, or through a frame without
      unwind tables. It walks the stack first, which is part of its cost —
      about 1.2 µs and 130 ns a frame, against GCC's tens of nanoseconds.
    • A cleanup attribute does not run when a longjmp (or a Rust panic)
      leaves its scope, as in GCC.
    • Refused on WebAssembly, under #pragma cinrs no_std and with
      panic = "abort".
  • A thread-local object shared between units: one with external linkage
    that a unit gives a symbol (#pragma cinrs export, every file of
    ccinrs) exports an accessor, name.cinrs_tls, returning the calling
    thread's copy, and extern _Thread_local (extern __thread) in another
    unit reaches the object through it. Both were refused; Redis's
    __thread sds thread_reusable_qb and jemalloc's configure TLS probe need
    them. An object a C compiler made cannot share one.
  • ccinrs: a long double function of the program's own — declared in
    its own files rather than in the platform's headers — is called with
    cinrs's long double, the double its definition, compiled by ccinrs
    too, takes; Redis's ld2string and string2ld were refused as if they were
    the C library's. Options::own_declarations_are_cinrs is the switch; a
    c99! block keeps refusing, since it may be calling a C library a C
    compiler built through that library's own header.
  • Uninitialised local arrays, on request: ccinrs -ftrivial-auto-var-init=uninitialized (GCC's option; zero is the default
    and pattern is taken as zero with a warning) and #pragma cinrs auto_var_init uninitialized. A local array declared without an initialiser
    is then a MaybeUninit, reached through a raw pointer, instead of being
    cleared on every entry to its block, and the arena behind variable length
    arrays and alloca stops clearing; reading an element nothing wrote is
    undefined behaviour, as in C. Scalars, structures and unions stay
    zero-filled. libuv's uv__io_poll in miniature goes from 1.04 s to 0.006 s
    (gcc 0.009 s). See doc/translation.md.
  • #pragma cinrs unwind, and extern "C-unwind" for a unit that calls
    setjmp or longjmp: every function, declaration and function pointer
    type of the unit, which a longjmp can then pass through. Other blocks
    stay extern "C". See doc/pragmas.md.
  • ccinrs's -fno-cinrs-unwind (and -fcinrs-unwind, the default):
    ccinrs makes every function extern "C-unwind", which costs 0.3 % of the
    instructions SQLite's speedtest1 runs (under 0.1 % without the run-time
    checks); the option makes them
    extern "C" and refuses setjmp and longjmp with a located error naming
    it.
  • A long double through a literal printf or scanf format.
    printf("%.2Lf", x) and sscanf(s, "%Le", &x) used to be refused where the
    platform's long double is wider than cinrs's (x87's, on x86-64 Linux and
    macOS). When the format is a string literal that names every long double
    argument with L, the Ls become l — %lf is %f to printf and a
    double * to scanf — and the platform reads the double it is given. Any
    other way of passing one is refused as before.
  • More of GCC's assembly. An rbx clobber — zstd's cpuid with "ebx" —
    keeps rbx in a scratch register around the template; a clobber may be GCC's
    register number, as mbedtls's "0", "1"; "m", "=m" and "+m" pass the
    lvalue's address, named in the template as (%reg) or not at all, as in
    mbedtls's bignum loops and its zeroize barrier; %c and %P print an
    immediate without its $, as xz's range decoder needs; and basic asm at
    file scope, __asm__("…");, is global_asm!.
  • __builtin_frame_address(0) and __builtin_stack_address(), as an address
    in the current function's frame, which is what QuickJS's stack-overflow
    check compares; another level is refused.
  • __builtin_ia32_pause(), GCC's own name for _mm_pause, which curl's spin
    lock calls under GCC: core::hint::spin_loop().
  • RDRAND and RDSEED: _rdrand16_step … _rdseed64_step in <immintrin.h>,
    mapped onto core::arch like every other intrinsic, the output pointer
    passed as the &mut it takes there. libsodium's configure probes for
    them.
  • __attribute__((transparent_union)): a parameter of such a union takes an
    argument of any member's type, or a null pointer constant, and is passed as
    the first member, which is GCC's ABI. glibc's bind, accept and
    getsockname under _GNU_SOURCE take a struct sockaddr_in * or NULL,
    as libuv calls them. A union whose members could not all be passed that way
    keeps GCC's warning and is an ordinary union. A function type with such a
    parameter is compatible with one that has a member's type there, so libuv
    passes getsockname as an int (*)(int, struct sockaddr *, socklen_t *).
  • __attribute__((weak)) on a declaration is a weak reference, as in GCC:
    zstd's if (ZSTD_trace_compress_begin != NULL) is a real test at every
    optimisation level, a program that does not define the hook links, and one
    that does — in another object, under -flto or in a shared library — calls
    it. It used to be ignored, and every program without the hook failed to
    link. WebAssembly and Windows refuse the declaration instead.
  • A weak definition — __attribute__((weak)) on a function or an object the
    unit defines, or on any declaration of one, as zstd's CLI defines the hooks
    its library header declares weak — is a real one in ccinrs on ELF without
    -flto: the body is a private item and the C symbol a weak alias of it,
    made with global_asm! (nm says W), every use in the file goes through
    the symbol, and another file's strong definition overrides it — jemalloc's
    tests override its weak malloc_conf — as does a program the weak symbol
    of a shared library. Elsewhere — a c99! block, -flto, macOS, Windows,
    WebAssembly — it is an ordinary definition with a warning, where it used to
    be refused; a second definition is then a duplicate symbol rather than an
    override. Options::weak_definitions is the switch.
  • GCC's vector extensions on vector types of the program's own:
    typedef int v4si __attribute__((vector_size(16)));, vector_size on an
    object, a member or through pointers and arrays, and the V4SI-style
    machine modes. A vector is a #[repr(C, align(N))] struct over its
    elements — core::simd is unstable — with GCC's size and alignment (capped
    at 16 bytes, 32 under -mavx and 64 under -mavx512f; an aligned
    typedef replaces it, which is what glibc's <link.h> needs), and every
    operator is written out over the elements: the arithmetic, bitwise and shift
    operators with GCC's scalar broadcast, comparisons giving -1 and 0, v[i]
    as an lvalue, ++, -- and the compound assignments, brace initialisers
    (static ones too), casts that keep the bytes, __builtin_convertvector and
    __builtin_shuffle. __m128i and the other Intel types convert to the GCC
    vector GCC defines them as, and an array of them may leave its braces out
    (__m128i m[4] = { 0U }, HACL*'s Blake2). Redis's crccombine.h,
    CPython's <link.h>, and 43 gcc-torture cases that were refused now build.
    A vector passed by value to a function the unit does not define, or through
    ..., is refused: GCC passes one in a vector register, and the struct does
    not go there.
  • __attribute__((ms_struct)) lays a record's bit-fields out by
    Microsoft's rules, as GCC's -mms-bitfields does, and gcc_struct by the
    System V ones. It used to be ignored, which moved fields without a word —
    1,560 of CPython's test_ctypes failures. #pragma ms_struct on is
    refused, naming the attribute.
  • max_align_t in the bundled <stddef.h> (C11), with the platform's size
    and alignment — 32 and 16 on x86-64 Linux — rather than one built on
    cinrs's eight-byte long double. autoconf's AC_CHECK_ALIGNOF(max_align_t)
    found none, and CPython fell back to an alignment of 8.
  • A directive among a function-like macro's arguments is processed, as GCC
    does: CPython's perf_jit_trampoline.c writes an #ifdef between the
    parentheses of a macro call, which was "expected expression, found '#'".
  • __attribute__((common)) on a tentative definition is a weak definition
    where ccinrs makes those, so that the copies every unit including the
    header defines merge into one, as common symbols do: Redis's
    redismodule.h declares its API's pointers so, and redis-server failed
    to link with "duplicate symbol". cinrs-core's FileTranslation says
    whether a unit makes a real weak definition (weak_definitions).
  • Predefined macros GCC has: __PIC__ and __pic__ (2) wherever rustc
    compiles position-independent code, __PIE__ and __pie__ (2) where it
    links position-independent executables, __FXSR__ with the rest of the
    x86-64 baseline, and in the GNU dialects only linux, unix and i386.
  • cinrs-core, for a command-line driver: translate_file and
    translate_source, which hand back the Rust, the diagnostics, the headers
    read and the symbols defined; preprocess_file and preprocess_source,
    the preprocessor alone, with the macros defined at the end;
    Options::export (every unit under #pragma cinrs export),
    Options::macros (-D and -U), Options::includes (-include) and
    Options::target_features (-m<feature>); GCC_VERSION.
  • cinrs-rt can be compiled without Cargo: num-complex is an optional
    (default) feature, without which the crate has a #[repr(C)] Complex of its
    own, and SOURCES (feature sources) is its source as text.

Changed

  • __DATE__ and __TIME__ are the moment of translation, as C17
    6.10.8.1 asks and GCC has them, where they were the placeholders "??? ?? ????" and "??:??:??" — which CPython's sys.version parser refuses.
    SOURCE_DATE_EPOCH, when it is set, is that moment in UTC, so a
    reproducible build still gives the same output twice; GCC's error, at the
    first use, when it is not a number of seconds up to the end of 9999.
    Otherwise it is now, in the local zone — TZ or /etc/localtime, read by
    cinrs itself (TZif and POSIX rules), and UTC where neither says anything.
    __TIMESTAMP__ is the modification time of the file being read, as in
    GCC, and the moment of translation for a c99! block's own text. In a
    block, the moment is the expansion: a crate that is not rebuilt keeps the
    date it was built with. Options::translation_time fixes the moment, and
    cinrs_core::clock is the calendar and the zone.
  • ccinrs -std=c89 … -std=c17 take what GCC's do: the constraint
    violations GCC only warns about — a stray ; at file scope, an enumerator
    outside int (glibc's <sys/epoll.h>), sizeof (void), a cast to a union
    type and the rest of the leniency table in doc/gnu-extensions.md — and
    #embed before C23, with __has_embed defined. CPython builds with
    -std=c11 and FFmpeg with -std=c17. What makes the dialect ISO stays:
    asm and typeof are not keywords, __STRICT_ANSI__ is defined and
    trigraphs are read. -pedantic-errors (or -Werror=pedantic) gives the
    strict entry points' errors back, with a note that says so; -pedantic
    alone changes nothing. Options::gnu_leniencies is the switch, off by
    default, so c11! and the other strict macros are unchanged.
  • An array variable of 16 bytes or more is 16-byte aligned on x86-64
    (System V), as the psABI says (3.1.2) and GCC and Clang do: automatic,
    static and file-scope ones, and variable length arrays, through the
    wrapper an _Alignas object is generated in. SIMD code relies on it —
    _mm_load_ps(table) is an aligned load, and faulted on a cinrs array four
    bytes off. The type and __alignof__ are unchanged. In a c99! block a
    file-scope array with external linkage keeps its plain Rust type, and
    alignment, unless Options::abi_align_public_arrays is set, as ccinrs
    sets it.
  • ccinrs -flto on x86-64 Linux is the linker's link-time optimisation:
    the objects are LLVM bitcode and rust-lld optimises them (ThinLTO, -C linker-plugin-lto), as GCC's linker plugin does, where it was rustc's
    -C lto. The linker sees every object, so objects and archives compiled
    without -flto link beside the optimised ones, and a file that makes a
    weak or common definition is compiled without it — said under -v only
    — instead of losing the definition's weakness: Redis's default build, with
    its -flto, links redis-server (122 of its 125 files optimised).
    Elsewhere rustc's LTO is as it was. -fuse-ld naming another linker is
    ignored, with a warning, at a link that has -flto bitcode in it.
  • A function whose jumps the relooper structures is no longer made the
    whole-function state machine for a nesting it does not have.
    The depth
    check counted a labelled block for every shape in a run, where the output
    has one only for a shape something breaks forwards to, and its limit is 400
    such blocks rather than 200. Tcl's bytecode interpreter (TEBCresume, 230
    deep where 563 were counted) is relooped: Tcl's expr benchmark runs in
    0.98 s rather than 2.77 s (gcc: 0.67 s), and tclExecute.c compiles in
    2.6 s rather than 30.6 s.
  • A program or library ccinrs links stops at a panic — a run-time check
    that failed — with Rust's message and abort, through a hook its link
    installs. Now that every function is extern "C-unwind", the panic would
    otherwise unwind looking for a handler and end with "fatal runtime error:
    failed to initiate panic"; before, it ended with a backtrace of "panic in a
    function that cannot unwind".
  • A source file — a header, an include_c99! file — no longer has to be
    UTF-8. A byte that is not part of a UTF-8 sequence is passed through a
    narrow string literal as the byte it was, read as Latin-1 in a wide one and
    ignored in a comment, as GCC does; anywhere else it is "unexpected byte 0xE9
    in program (the file is not UTF-8)". Such a file used to be "cannot read".
  • A macro redefined with a different replacement list is a warning, and the
    later definition stands, as in GCC and Clang; a redefinition in a system
    header — glibc's <assert.h> after a program's own assert — says nothing.
    It used to be an error, which stopped zstd.
  • An enumeration with no negative enumerator is unsigned int, the type
    GCC and Clang make it compatible with, and its Rust alias is a c_uint; the
    enumerators stay c_int. It was int, which was wrong wherever the two
    differ: e - 1 < 0, a division, a conversion to long, and — silently — a
    bit-field of a typedef of an anonymous enum, which read 4 back from
    QuickJS's 3-bit closure_type as -4.
  • The arena behind variable length arrays and alloca keeps its chunks per
    thread when a call returns, for the next call to take, instead of freeing
    them: a function that allocas on every call — QuickJS's interpreter — no
    longer pays for an allocation, a zeroed chunk and a free each time (a
    microbenchmark of it went from 0.85 s to 0.28 s; gcc takes 0.11 s). A unit
    under #pragma cinrs no_std frees them as before.
  • __builtin_ctz on x86-64 counts its 32-bit operand in a 64-bit register
    with bit 32 set — the same answer for every value — so that tzcnt writes
    the register it reads. LLVM's generic x86-64 code wrote whichever register
    was free and so waited for that register's previous value, a false
    dependency GCC breaks with an xor; libwebp's lossless entropy loop, whose
    ctz landed in the register of the previous iteration's table lookup, ran
    at 2.7× GCC's time and now runs at 1.05× (0.200 s → 0.075 s; gcc 0.071 s).
  • A subscript that cannot be outside an array object is an element of the
    array, table[op as usize], rather than pointer arithmetic: a constant, a
    value converted from a narrower type (an unsigned char into 256
    elements), what an assignment to a variable stores, or a mask, into a named
    array or a member or element of one. With debug assertions, offset checks
    that the address calculation does not wrap, which cannot fail inside an
    object and which LLVM cannot fold for a static, whose address it does not
    know; LLVM drops the element's bounds check, which the subscript's range
    proves. An interpreter's goto *table[opcode] is the case: CPython's eval
    loop is 6 % smaller and runs 2.6 % fewer instructions on a recursive
    benchmark, QuickJS's 6 % fewer on a numeric one, in about the same time.
    An array reached through a pointer, a thread-local one included, and a
    safe function keep the pointer arithmetic.
  • ccinrs generates Rust only for what an object needs: the
    definitions the linker sees — every symbol of the unit, what a section or a
    constructor list puts in the object — what they call, take the address of
    or name, through a static's initialiser and an asm template too, and the
    declarations and types those name. A static inline function a header
    defines and nothing calls, and a function a header only declares, are not
    generated at all, where before they were generated and the printed Rust
    pruned. Options::reachable_only asks cinrs-core for this, and
    cinrs_core::reach is the walk. git's abspath.c compiles in 0.30 s
    instead of 0.40 s; git's make -j2 takes 122 s instead of 153 s, and
    libuv's cmake --build -j2 56 s instead of 64 s, with every object of both
    defining and referencing the same C symbols as before.
  • The preprocessor's hide sets — which macros a token may no longer expand —
    are shared rather than rebuilt for every token of every expansion: a
    third of all the allocations translating a file made were copies of them.
    ccinrs's own time on git's abspath.c and diff.c and libuv's
    src/unix/core.c falls by 7 %, 6 % and 10 %, for the same Rust.

Fixed

  • volatile was ignored: a volatile object was read and written like
    any other, so LLVM merged, dropped and hoisted its accesses — a loop
    waiting for a volatile sig_atomic_t flag a signal handler sets spun for
    ever at -O2, and two stores in a row were one. Every read and write of
    a volatile lvalue is now a read_volatile or write_volatile of its
    address: an object declared volatile (through a typedef too, a local
    included), anything through a pointer to volatile, a member of a
    volatile record or one declared volatile, and an element of a
    volatile array. A compound assignment and ++/-- are one volatile
    read and one volatile write, whose value is the value stored; a bit-field
    is one volatile read and one volatile write of the bytes it lives in; a
    whole structure is copied in or out with one of each; a packed member goes
    through a packed wrapper. &v, sizeof v and an array's decay are
    unchanged. volatile int * and int * are no longer compatible types.
    See doc/translation.md.
  • On WebAssembly and Windows, extern int x __attribute__((weak));
    followed by the unit's own definition of x is that definition, with the
    warning, as on every other target; it was refused as a weak reference
    before the definition was seen. Only a weak reference nothing in the unit
    defines is refused there. (A function's definitions were already known
    in advance.)
  • An AArch64 Apple target defines __arm64__, __arm64 and
    __ARM64_ARCH_8__ besides __aarch64__, as Apple's Clang does: the
    macOS SDK's <sys/cdefs.h> stopped at "Unsupported architecture" without
    the first. Linux's AArch64 gets none of them, as with GCC. Reported by
    tasuren in #1.
  • An inline definition provides no external definition where the unit
    makes C symbols (ccinrs, #pragma cinrs export), as C99 6.7.4p7 and
    GNU89 say: a header's inline function that two files include is no
    longer defined twice ("duplicate symbol", "symbol … is already
    defined"). C99's rule — every file-scope declaration says inline and
    none says extern — in C99 and later; GNU89's — an extern inline
    definition is the inline-only one, a plain inline one an external
    definition — in c89! and gnu89!, under the new ccinrs -fgnu89-inline (and -fno-gnu89-inline, which __GNUC_GNU_INLINE__
    follows), and for any function a declaration of which says
    __attribute__((gnu_inline)): glibc's __extern_inline, and macOS's
    __header_always_inline in <sys/select.h>. The body is a private item
    the unit's own calls use, and the function's address is the external
    symbol, the same in every unit. Options::gnu89_inline and
    Function::inline_only are new. Reported by tasuren in #1.
  • A static named like a local of the variable-length-array arena — size,
    first, count, other — beside a variable length array or alloca
    compiles; the arena's code is in a module of its own now, where a binding
    of the same name does not shadow it (E0530).
  • A link with -flto that also takes an object or an archive ccinrs
    compiled without it links: the other machine code calls Rust's standard
    library by name, and rustc's LTO kept only what the -flto crates used —
    "undefined symbol: core::panicking::panic" when Redis linked redis-cli
    with its deps/ archives. See the -flto entry under Changed.
  • A wide string literal as an arm of ?: that needs a const pointer —
    CPython's return sep ? sep + 1 : L""; — compiles; the block holding the
    literal's static was read as a statement before its as.
  • A conditional with an integer constant condition is the address constant
    its chosen arm is, in a static initialiser: CPython's _Py_LATIN1_CHR in
    Argument Clinic's keyword tables. So is one whose arms are string literals
    and that is converted to the object's const char *, nested or not:
    OpenSSL's sizeof(long) == 4 ? "four" : sizeof(long) == 8 ? "eight" : "".
  • ## may make a preprocessing number that is not a valid constant, as C17
    6.4.8 allows: FFmpeg's AV_VERSION_DOT(a, b, c) a ##.## b ##.## c goes
    through 63.1. on its way to 63.1.100, and its 32 ## p is pasted on into
    a function name. "pasting '63.1' and '.' does not give a valid token" was
    28 of FFmpeg's failing files. A number that reaches the parser that way is
    still the error it would have been written.
  • A static integer as wide as a pointer — uintptr_t, intptr_t, long,
    an _Atomic one — may be initialised with an address converted to it, which
    GCC takes as a relocated constant: FFmpeg's static atomic_uintptr_t av_log_callback = (uintptr_t)av_log_default_callback;. The item holds the
    address as a pointer and every use reads it as the integer. As a member of
    a table too — git's { .defval = (intptr_t)"all" }, nginx's
    (uintptr_t)ngx_ssl_get_protocol — where the item is a MaybeUninit of the
    table with the addresses written into it.
  • A brace-initialised local structure or union has zero padding, as GCC
    and Clang give it (C11 6.7.9p10, p21): the bytes are zeroed and only the
    members that are not zero stored, where a struct literal moved into place
    left the stack's bytes in the padding. git's ref-filter.c compares two
    { 0 } structs with memcmp; git for-each-ref died on a broken ref, and
    three of git's tests passed or failed by luck. Compound literals and
    definitions hoisted out of their block likewise, and q = p; of a padded
    type copies the bytes.
  • A record both packed and aligned(N) compiles, laid out as GCC lays it
    out — members packed, alignment N, size a multiple of N — where it was "a
    record cannot be both packed and given a stricter alignment": the kernel's
    <linux/bpf.h> has one, which nginx's configure includes. The Rust item
    stays packed and gets its size from a field; an enclosing record pads to
    N, and an object of the type is placed at N by an alignment wrapper.
  • __attribute__((noreturn)) on a variable of function pointer type is
    taken and dropped, as GCC gives it to the pointed-to type, where it was
    "'_Noreturn' is only allowed on a function" (git's NORETURN_PTR in
    usage.c). The keyword is still a function's alone.
  • A strict block below c23!, which refuses #embed, no longer defines
    __has_embed either, and __has_embed(…) answers "not found" there, so
    that a program which asks first — FFmpeg's checkasm, #ifdef __has_embed
    — takes its other road rather than the refusal. A GNU dialect takes both,
    as before, and so does ccinrs under every -std= (see Changed).
  • Under ccinrs, a diagnostic that says how to choose another standard or
    dialect names the command line — "(this file is compiled with
    -std=c17)", "GCC accepts this with a warning, and so does ccinrs without
    -pedantic-errors or with -std=gnu17" — rather than the gnu17! macro;
    Options::front_end says which front end the C came through.
  • A static pointing into an extern array of unknown size at an offset —
    FFmpeg's ff_ac3_enc_options + 2 — compiles: pointer arithmetic in a
    static's initialiser is wrapping_offset, where rustc's constant
    evaluation refused an offset past the zero bytes it knew of the array
    (E0080).
  • A file-scope static named bits, raw or value beside a record with
    bit-fields compiles: the accessors' own locals and the setter's parameter
    are __cinrs_ names now, where let bits was "let bindings cannot shadow
    statics" (E0530, FFmpeg's fastaudio.c). A C local, parameter or hoisted
    local of such a name was already renamed apart.
  • An array with static storage duration whose list decides its length may
    name itself in that list — static const struct node tree[] = { { 1, tree }, … };, FFmpeg's matroskadec.c — as its scope begins after its
    declarator (C17 6.2.1p7). It was "use of undeclared identifier".
  • __attribute__((aligned(1))) on a typedef of a record lowers its
    alignment, as GCC does for a scalar's: a read, a write or a member access
    through a pointer to the typedef is an unaligned one. OpenSSL's
    aes_block_t in its AES-IGE code; it was refused.
  • A diagnostic names an untagged record or enumeration "anonymous struct",
    "anonymous union" or "anonymous enum", not by the Rust item cinrs made
    for it.
  • static T x[64]; followed by static T x[] = { … }; defines one object of
    64 elements, as C11 6.9.2 and 6.2.7 make it, rather than "redefinition of
    'x'" (CPython's pyexpat.c).
  • __m128i m[4] = { 0U }; leaves the inner braces out, as GCC allows for a
    vector: the scalar is the first lane of the first element (HACL*'s Blake2
    in CPython).
  • s.a = s.b = 1 on two bit-fields of one record compiles; the setter's
    borrow of the record overlapped the inner assignment's (E0499, in libpng's
    pngfix).
  • __extension__ is followed by a cast expression, as in GCC's grammar:
    Lua's (__extension__ (lua_CFunction)(p)) was a call of a type.
  • A function declared through a typedef of a function type — typedef int handler(int); static handler f, g; — is a function. It was read as an
    object of an incomplete type and refused; expat declares its parser's state
    handlers that way.
  • A null pointer that is only tested is a constant: bool b = true | false | !nullptr;, autoconf's C23 probe, and int i = (void *)0 == 0; are static
    initialisers, as in GCC. They were "not a compile-time constant expression".
  • The name of a function defined without a prototype (char *e(p, i) char **p; int i; { … }) is a value of its unprototyped type, which converts to a
    prototyped pointer and fills a static table. Passing one where a
    char *(*)(char **, int) was wanted made rustc refuse the translation,
    so every autoconf 2.70+ configure found no C89, C99 or C11 compiler.
  • A block-scope static pointer to a variable length array, static int (*q)[m] = &B;, is allowed, as C allows it, and a variable length array type
    is compatible with an array of any length, so int (*p)[m] = &B; with an
    int B[100] needs no cast. autoconf's variable-length-array probe writes
    both, and configure concluded there were no VLAs.
  • __builtin_expect's second argument is evaluated, so __builtin_expect(x, z++) increments z; it was checked and dropped (GCC's execute/pr85156).
  • An attribute may follow a parameter's whole declarator — char *argv[] __attribute__((unused)) — where it was a syntax error (execute/stkalign).
  • An inline-assembly immediate is printed in its operand's width,
    sign-extended, as GCC and Clang print it: xz's "n"(UINT32_C(31) - UINT32_C(2048)) under %c is the displacement -2017, which the assembler
    takes, and not 4294965279, which it refused.
  • A function pointer with static storage duration may hold an object's
    address or an integer, as libwebp's static volatile VP8CPUInfo last = (VP8CPUInfo)&last; sentinel and SIG_IGN do: the item is the data pointer,
    read and written as the function pointer C declared. rustc's constant
    evaluation refused the translation of every libwebp DSP file. Inside an
    array or a structure such a constant is refused with the reason.
  • A pointer with static storage duration initialised with a negative integer
    — glibc's SIG_ERR, ((__sighandler_t) -1), and (void *) -1 — is all
    ones, as in GCC. It was written -1 as c_ulong, which rustc refused
    (E0600); curl's test servers keep SIG_ERR in a static.
  • A statement may begin with __extension__ and still be an expression:
    curl's typecheck-gcc.h wraps every curl_easy_setopt in
    __extension__({ … }), which was read as a declaration with no type.
  • Explicit asm labels on Darwin name the assembler symbol directly, without
    LLVM adding an extra leading underscore. Labels produced by Apple's
    system-header macros could otherwise reference __fputs instead of
    _fputs and fail to link. Reported in #2 and fixed in #4 by tasuren.

Toolchain and platforms

Rust 1.99 or later, tested on 1.99.0; CI builds on stable only.

The macros: as in 0.2.0, developed and fully tested on x86-64 Linux, with the
examples and the portable tests built, linked and run in CI on macOS (arm64)
and Windows (x86-64, MSVC); other targets are compile-checked.

ccinrs: developed and tested on x86-64 Linux, where every program in
doc/ccinrs-real-programs.md was built, and
for wasm32-wasip1, wasm32-wasip2, wasm32-unknown-unknown and musl
(aarch64 musl under qemu) through --target. Its shared libraries, weak
definitions and version scripts are ELF's. macOS and Windows hosts are not
tried yet.