Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-097 · Case file with fixtures · Case 69 of 694 · Compiler evidence

E0045: C Variadic Functions Need a Compatible ABI

Varargs layout and default promotions are ABI rules, not syntax alone. Use a compatible foreign ABI and match the exact header, or place a typed C wrapper in front of the variadic function.

Reviewed
Rust
Rust 1.98.1
Targets
ABI-dependent targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Variadic argument layout and promotions belong to specific platform ABIs, while Rust's native ABI provides no compatible C-varargs contract.
First discriminating check
Match the declaration to the foreign header and use a supported ABI such as C only when the target platform and promoted argument types agree.

An ellipsis is not supported by every calling convention:

unsafe extern "Rust" {
    fn log_many(first: i32, ...);
}

Rust 1.98.1 reports E0045: C-variadic functions with the Rust calling convention are not supported. The failing fixture records the exact diagnostic.

The name “C-variadic” is important. The callee receives a fixed parameter list followed by arguments whose types and count are described by some external convention, often a format string. How those extra values are passed is part of an ABI.

The Rust ABI makes no varargs contract

Rust's native calling convention is not a stable interface for foreign code and does not define C-style variadic argument handling. An ellipsis cannot be added independently from the ABI.

The E0045 documentation explains that a variadic function needs a compatible calling convention. The Reference lists ABIs that can be used for variadic declarations, including C and platform-specific compatible variants.

Changing the string to C is correct only when the foreign function is actually declared with the target's C ABI.

The syntax repair reflects the header

The repaired fixture declares:

unsafe extern "C" {
    fn log_many(first: i32, ...);
}

This compiles as a declaration. It does not prove that a linked symbol named log_many exists or that arbitrary Rust arguments may be passed safely. Those checks appear at link time and at the call contract.

I never change an ABI based only on the compiler suggestion. I compare the exact declaration with the C header and target documentation. Windows APIs may use system; 32-bit targets can distinguish calling conventions that are equivalent elsewhere.

Default argument promotions matter

In C varargs, smaller integer types undergo promotions, and floating-point values can be promoted as well. Passing a Rust value using its convenient source type may not produce the representation the callee expects.

The fixed arguments in the declaration also matter because the ABI may use them to determine register or stack behavior. A wrong fixed type is not repaired by a correct ellipsis.

Format functions add another layer: the format string must agree with the number and promoted types of every following argument. Rust's normal formatting macros check their own typed arguments and are safer when foreign formatting is not required.

A typed C shim is often the best boundary

When a Rust application needs one specific use of a variadic C API, I prefer writing a small C wrapper with a fixed signature:

void log_status(int status) {
    foreign_log("status=%d", status);
}

Rust declares only log_status(i32). The C compiler owns varargs promotion and format checking, and the Rust boundary becomes a normal typed call.

This is especially valuable for callbacks, platform APIs, or repeated calls. It narrows unsafe Rust and provides a natural place for target-specific details.

Declaration-only evidence has a boundary

The Atlas fixture intentionally does not call or link the made-up function. It proves the compiler's ABI restriction and the accepted declaration form. A production verification must go further: compile the foreign implementation, link it on every supported target, and run representative calls under sanitizers where practical.

This distinction is part of trustworthy evidence. A compile-pass fixture should not pretend to verify a binary that is absent.

Avoid transmute between function pointer ABIs

A function pointer's ABI is part of its type. Transmuting a Rust-ABI function pointer into an extern-C variadic pointer does not change how the function was compiled. It creates a call through the wrong convention and can corrupt registers or the stack.

The same warning applies to loading symbols dynamically and assigning them an invented signature. Dynamic lookup gives an address; the programmer must still establish the correct function type.

My review checklist

For a variadic boundary I verify:

  1. The exact foreign header and library version.
  2. The target-specific ABI.
  3. The fixed parameter types and widths.
  4. Promotions for each variadic argument.
  5. The external mechanism describing count and types.
  6. Error, ownership, and thread-safety behavior.
  7. Whether a fixed-signature shim can remove varargs from Rust.

E0045 prevents one clear mismatch: using the Rust ABI where no C-varargs rule exists. The larger lesson is that ... is a protocol interpreted by a calling convention. A compiling declaration is only trustworthy when it matches the foreign implementation and every call follows that protocol exactly.