Rust 1.99 C Variadic Tutorial: Define a C-Style Function in Rust
Rust 1.99 makes C-ABI variadic function definitions stable. This tutorial shows how to build, test, export, and safely call a C-style variadic function from Rust and C.
On this page
Rust 1.99.0, released on October 1, 2026, makes one long-standing foreign-function interface task available on stable Rust: you can now define a C-ABI variadic function in Rust rather than only calling one supplied by C. That matters when Rust needs to implement an existing C interface such as a logging callback, plugin entry point, or compatibility layer. This tutorial builds a small variadic function, tests its argument handling, and then shows what changes when C code calls the Rust implementation.
What Rust 1.99 changed for C variadic functions
A variadic function accepts a variable number of arguments. The familiar C example is printf, whose first parameter is a format string followed by however many values that format requires. Before Rust 1.99, stable Rust could declare and call an externally implemented C variadic function, but defining the variadic function itself in Rust was not stable. Rust 1.99 stabilizes definitions using the "C" and "C-unwind" application binary interfaces, or ABIs, which specify how the function is called at the machine-code boundary.
The distinction is easy to miss because both directions use .... A Rust program calling C's printf is consuming an existing variadic interface; Rust 1.99 lets your Rust function become the implementation on the other side of that interface. Inside the function, Rust represents the variable argument list as a VaList, and next_arg retrieves the next value with the type you request. That does not make arbitrary C arguments type-safe, though: the caller and callee still have to agree about how many values exist and what their types are.
Update to Rust 1.99 before writing the function
Start by checking the compiler you are actually using. If Rust was installed with rustup, the official release instructions use rustup update stable to move to Rust 1.99.0. Then verify the result with rustc --version. The important part is that the reported stable compiler is 1.99.0 or newer.
rustup update stable rustc --version Create a small library project because the second half of this tutorial will expose the Rust function to a C program. Cargo's library template creates src/lib.rs, while the current default project edition is Rust 2024.
cargo new --lib rust_variadic cd rust_variadic If the command succeeds, you should have a normal Cargo package with a Cargo.toml manifest and a src/lib.rs source file. No nightly compiler and no c_variadic feature gate are required for the stable Rust 1.99 implementation.
Define a Rust function that accepts variable arguments
The simplest useful example is a function that receives a count followed by that many C-compatible integers. The count solves a problem that ... does not solve by itself: a variadic function has no automatic way to know how many values the caller supplied. The caller therefore has to follow the contract established by the function.
use std::ffi::c_int; pub unsafe extern "C" fn sum_i32( count: c_int, mut args:... ) -> c_int { let mut total = 0; for _ in 0..count { total += unsafe { args.next_arg::() }; } total } The extern "C" part selects the C calling convention, while the final args:... declares the variable argument list. Rust exposes that parameter inside the body as a VaList. Calling next_arg:: advances through the list and retrieves the next argument as a C-compatible integer. The Rust Reference requires a C-variadic definition to be unsafe, because the compiler cannot independently prove that the caller supplied the expected number and types of arguments.
There is an important boundary here. The type parameter passed to next_arg is your assertion about the argument you are reading; it is not a runtime parser that examines the caller's value and fixes a mismatch. If the caller supplies a different incompatible type or too few arguments, the safety contract has been violated. That is why the function's safety documentation should describe exactly what callers must provide.
Add a safety contract before calling the variadic function
A variadic function becomes much easier to review when its safety requirements are written directly above the definition. In this example, the caller must provide a non-negative count and then exactly that many c_int values. The function itself consumes one value for every iteration, so a count larger than the number of supplied arguments would make it read beyond the valid argument list.
use std::ffi::c_int; /// Adds `count` C `int` values from the variable argument list. /// /// # Safety /// /// The caller must pass exactly `count` additional arguments, /// and every additional argument must be compatible with `c_int`. pub unsafe extern "C" fn sum_i32( count: c_int, mut args:... ) -> c_int { let mut total = 0; for _ in 0..count { total += unsafe { args.next_arg::() }; } total } The function is intentionally small because the interesting part is the FFI contract rather than the addition. The Rust Reference also specifies that the variadic parameter cannot escape the function through a longer caller-provided lifetime. In practical terms, treat the VaList as temporary state belonging to the current variadic call rather than something to store for later use.
Call the variadic function from Rust
Once the function exists, you can exercise it from a normal Rust test. The call itself is unsafe because the compiler cannot verify the variadic contract for you. In this test, the count is three and exactly three c_int values follow it, so the function should return 42.
#[cfg(test)] mod tests { use super::*; use std::ffi::c_int; #[test] fn sums_three_values() { let result = unsafe { sum_i32( 3, 10 as c_int, 20 as c_int, 12 as c_int, ) }; assert_eq!(result, 42); } } Run the test with Cargo.
cargo test A passing test confirms that the Rust caller and the Rust implementation agree on the argument count and types for this case. It does not prove that every possible caller will obey the contract. That distinction is especially important for FFI code because the compiler checks each side separately, while the ABI contract connects them.
Export the function so C can call it
The feature becomes more useful when Rust implements an interface that an existing C program already expects. For that scenario, give the function a stable exported symbol and configure Cargo to produce a static library. With the Rust 2024 edition, the attribute that disables Rust's normal symbol-name transformation is written as an unsafe attribute.
use std::ffi::c_int; /// Adds `count` C `int` values. /// /// # Safety /// /// The caller must pass exactly `count` additional arguments, /// and every additional argument must be compatible with `c_int`. #[unsafe(no_mangle)] pub unsafe extern "C" fn sum_i32( count: c_int, mut args:... ) -> c_int { let mut total = 0; for _ in 0..count { total += unsafe { args.next_arg::() }; } total } Then add a library configuration to Cargo.toml:
[lib] crate-type = ["staticlib"] The staticlib output gives a C linker a Rust library it can consume. The Rust function's exported name is sum_i32, matching the declaration the C compiler will see.
Call the Rust variadic function from C
Create a small C source file beside the Rust project and declare the function with the same fixed parameter followed by .... The C compiler does not need to know how Rust walks the argument list; it only needs the ABI-compatible function declaration.
#include extern int sum_i32(int count,...); int main(void) { int result = sum_i32(3, 10, 20, 12); printf("result = %d\n", result); return 0; } Build the Rust library first:
cargo build --release The exact C linker command depends on your operating system and Rust installation because a Rust static library may require additional system libraries. On a Unix-like system, the resulting file is typically under target/release. Link that library with your C program using the compiler and libraries appropriate for your target.
The expected application-level result is result = 42. More importantly, this demonstrates the part Rust 1.99 newly makes stable: C code can cross the ABI boundary into a variadic function whose implementation is written in Rust. Rust's own release notes explicitly distinguish this from calling an externally defined variadic function, which was already possible.
Keep the C and Rust argument types aligned
The most dangerous mistake in a variadic interface is treating ... as if it carried type information. It does not. The function has to know what it expects, and the caller has to send values compatible with those expectations. Using Rust's std::ffi C-compatible types makes the intended ABI types clearer than choosing Rust-specific integer types without checking their C equivalents.
For example, this function reads every additional value as c_int:
unsafe extern "C" fn sum_i32( count: c_int, mut args:... ) -> c_int { let mut total = 0; for _ in 0..count { total += unsafe { args.next_arg::() }; } total } A C caller should therefore pass arguments appropriate for the declared contract. Do not change the C declaration to accept one type while the Rust implementation reads another. The Rust Reference specifically warns that unexpected argument counts or types in variadic interfaces can result in undefined behavior.
next_arg must match the contract established by the caller. If your API needs arbitrary user data, define an explicit representation instead of relying on a variadic list to carry unknown types.Know where Rust 1.99 variadics are supported
Rust's stable support is broader than a single desktop target, but it is still tied to targets that have the required C-variadic calling convention support. The current Rust Reference lists stable C-variadic function-definition support for x86 and x86-64, ARM, AArch64 and Arm64EC, RISC-V 32-bit and 64-bit except the ilp32e ABI, LoongArch, s390x, PowerPC and PowerPC64, AMDGPU, NVPTX, Wasm32 and Wasm64, C-SKY, Xtensa, Hexagon, SPARC64, and MIPS. It also notes that targets such as BPF do not support C-variadic function definitions.
That list matters when a crate is intended to build across many architectures. A function that compiles on x86-64 Linux is not automatically proof that every target supported by your project can implement the same C-variadic entry point. If portability matters, test the actual target set rather than assuming the ABI behaves identically everywhere.
When this Rust 1.99 feature is useful
The practical audience is narrower than the headline suggests. Most ordinary Rust applications do not need variadic functions because Rust APIs can usually express their inputs with slices, iterators, enums, structs, or other typed interfaces. C variadics become relevant when compatibility with an established C ABI is itself the requirement: a Rust implementation replacing part of a C library, a C-compatible plugin interface, low-level logging infrastructure, or another boundary where an existing caller already expects ....
There is also a useful architectural rule to take from the feature: keep the variadic boundary thin. Validate the fixed parameters first, read only the number of arguments promised by the contract, convert the values into an ordinary Rust representation, and perform the rest of the application logic using normal typed Rust code. That keeps the unsafe portion close to the ABI boundary instead of allowing unverified arguments to spread through the rest of the program.
What Rust 1.99 does not make safe
Stable support removes the need for a nightly feature gate, not the fundamental hazards of C variadic interfaces. Rust still requires variadic definitions to be unsafe, and VaList is tied to the lifetime of the current call. The compiler also cannot determine whether a C caller supplied the number and types of values that your implementation expects.
That makes the new capability best understood as an interoperability improvement. Rust 1.99 closes a gap where the language could consume C variadic APIs but could not stably provide the corresponding implementation. If you maintain a C-facing Rust library, the next step is to identify the exact C signature you need to preserve, write down its argument contract, and test that boundary from both sides rather than treating ... as a generic escape hatch.
Written by


