Rust 1.99.0 was released on October 1. It allows C-variadic functions with the extern "C" and extern "C-unwind" ABIs to be defined in stable Rust.
Calling externally defined variadic functions such as libc::printf from Rust was already possible. What is stabilized now is writing such functions in Rust itself.
The release announcement gives this example. As its comment says, guaranteeing at least two i32 arguments is the caller's job.
/// SAFETY: must be called with (at least) 2 i32 arguments.
unsafe extern "C" fn sum(mut args: ...) -> i32 {
// SAFETY: guaranteed by the caller.
let a = unsafe { args.next_arg::<i32>() };
let b = unsafe { args.next_arg::<i32>() };
a + b
}According to the Reference, a function definition that takes a variable argument list must be unsafe, and the variadic parameter must be last. In the body that parameter's type is VaList, which is ABI-compatible with C's va_list.
The types that can be read from a VaList are limited by the VaArgSafe trait. Defining naked variadic functions with non-"C" ABIs was stabilized as well; these must be written with inline assembly.
Caveats: when next_arg is safe
The Reference says reading an argument of type T with VaList::next_arg is safe only if another argument remains and the actual argument type is compatible with T as the Reference defines it. If both are integer types, the passed value must be representable in both.
The compiler does not check these conditions. Even with a type that satisfies VaArgSafe, passing a different type or fewer arguments is undefined behavior.
Definition support is stable only on the architectures the Reference lists, such as x86 and x86-64, AArch64 and RISC-V. Unsupported targets such as BPF produce a compile error. This article did not compile the example or test an FFI boundary.
Keeping the variadic boundary narrow
Argument extraction can be gathered into one small boundary function, with the expected argument order and types written in its safety comment.
A safe wrapper then handles everything outside it, checking whatever input can be checked before the unsafe reads begin.
Rust 1.99's C-variadic stabilization removes the dependence on nightly, while the unsafe contract to match argument types and counts still rests with the author.