Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-585 · Case file with fixtures · Case 557 of 694 · Compiler evidence

Rust Calls to C Variadic Functions Require Default Argument Promotions

An ellipsis removes per-argument type declarations but not the C ABI's promotion rules. Promote values explicitly and keep format/type agreements auditable.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
targets with a compatible C ABI and printf
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The ellipsis was treated as accepting arbitrary Rust values, ignoring default argument promotions and the external format/type agreement.
First discriminating check
Check the authoritative C declaration and promote every variadic argument to the required C ABI type while keeping format metadata consistent.

C variadic functions accept arguments after an ellipsis without listing their types in the function declaration. That flexibility does not remove ABI rules. The failing fixture passes an f32 to printf, and Rust reports E0617 because C variadic calls require the value to be promoted to the double representation.

The callee relies on an external agreement

For normal Rust functions, the signature tells compiler and caller each argument's type. For a C ellipsis, the callee uses information outside that declaration—often a format string—to decide how to read bytes from registers or the stack. A type mismatch can make it read the wrong width or register class.

C's default argument promotions include promoting float to double and promoting sufficiently small integer types to int or unsigned int according to their range. The E0617 explanation highlights this ABI requirement for Rust callers.

Rust rejects known invalid argument types instead of allowing a call whose machine-level interpretation is wrong. The unsafe block acknowledges FFI obligations, but unsafe does not turn an invalid ABI call into a valid one.

The repair supplies the promoted type

The repaired fixture passes 1.5_f64. On the demonstrated target this matches C double, represented in Rust by c_double. The C string literal provides a nul-terminated format without a temporary allocation.

I generally use the platform C aliases from std::ffi or std::os::raw when describing foreign signatures. They document intent and adapt where C type widths vary. I verify the actual library declaration from its header and generated bindings rather than assuming a Rust primitive on every target.

The Reference section on variadic external functions documents which Rust types may be passed and notes the required explicit casts for promotable values.

Format strings form a second type system

Making the value f64 solves only one half of a printf call. %f expects a promoted double in this context; other specifiers expect different widths, signedness, pointers, or strings. Rust cannot generally prove that a runtime format string agrees with every variadic argument.

I avoid exposing raw variadic formatting throughout an application. A small wrapper with typed Rust parameters can own the format and conversion. Better, I use Rust formatting when output does not need to pass through a C API.

If the foreign library provides a non-variadic alternative, it is easier to bind safely. Bindgen-generated declarations can reduce transcription mistakes, though wrappers must still enforce lifetime, ownership, and semantic rules that headers cannot express.

Promotions are not ordinary numeric preference

The change from f32 to f64 is required by the calling convention, not a claim that application calculations need double precision. I can keep domain computation in f32 and cast only at the FFI call boundary. That local cast makes the promotion visible and prevents wider types spreading without reason.

For integers, I follow the compiler's diagnostic and the C declaration. Casting every value to c_int mechanically can change unsigned values or truncate wider ones. I validate ranges before a narrowing conversion and test boundary values.

Pointers carry further requirements: nul termination for C strings, validity for the duration of the call, correct mutability, alignment, and ownership. Variadic acceptance does not weaken any of them.

Evidence must include the real target ABI

The fixture proves rustc's type gate on Rust 1.98.1 and demonstrates a compatible call on this environment. FFI code needs CI on every supported target, correct linking, and preferably a C-side integration test compiled from the authoritative header.

I use sanitizers or platform tools where practical, but they do not replace a reviewed signature. Variadic bugs can produce plausible output before failing under another optimisation level or architecture.

My E0617 checklist

  • Is the callee genuinely a C-compatible variadic function?
  • Which default promotion applies to each ellipsis argument?
  • Does the format or discriminator agree with width, signedness, and pointer type?
  • Are C aliases used where platform widths can differ?
  • Can a typed non-variadic wrapper contain the unsafe boundary?
  • Are numeric range and string-lifetime requirements checked?
  • Was the binding generated or compared against the current header?
  • Do integration tests run on every ABI and architecture the product supports?

The core principle is that missing parameter types in syntax do not mean missing machine contracts. C variadics shift type agreement into promotions and caller-controlled metadata. I keep that boundary small, explicit, and tested so Rust code does not depend on an invisible ABI guess.