Course contentsShow
← Rust
Lesson 1475 of 1,91012. Unsafe Rust and Low-Level AccessFree lesson

Handling Variadic C Functions

You will safely call C functions with variable argument lists using Rust's c_variadic feature.

In this lesson, you will learn how to interface with C functions that accept a variable number of arguments—such as printf—by utilizing Rust’s c_variadic feature.

The Challenge of Variadic Functions

In C, variadic functions (like int printf(const char *format, ...);) use a mechanism defined in stdarg.h to process an unknown number of arguments. Because Rust’s type system enforces strict signature matching, we cannot natively call these functions without special handling. Since Rust 1.37, we have been able to declare and call these functions using the C ABI and the ... syntax.

Defining and Calling Variadic Functions

To interact with a C variadic function, you must first declare it within an extern "C" block. You mark the end of the fixed arguments with ..., representing the variadic portion.

extern "C" {
    // Declaring a C variadic function
    fn printf(format: *const i8, ...) -> i32;
}

fn main() {
    let fmt = b"Value: %d\n\0".as_ptr() as *const i8;
    unsafe {
        // Calling the variadic function
        printf(fmt, 42);
    }
}

Safety Considerations

When calling these functions, you are operating entirely within an unsafe block. The compiler cannot verify the types or the number of arguments passed into the ... portion of the call. This places the burden of proof entirely on you:

  1. ABI Compatibility: You must ensure the types passed are compatible with what the C side expects. Passing a Rust String or a complex struct instead of a primitive type (like i32 or f64) will likely lead to undefined behavior or stack corruption.
  2. Promotion: C variadic functions perform "default argument promotions." For instance, f32 is typically promoted to f64. You must pass the promoted type, or your data will be misinterpreted.
  3. Null Termination: As always with FFI, ensure your format strings are null-terminated (\0), as C expects string pointers to terminate at the first null byte.

Because the compiler cannot check these constraints, consider wrapping these calls in a safe Rust function that handles the type conversion and safety requirements internally.

Key Takeaway: Use the ... syntax in extern "C" blocks to declare variadic functions, and always manually verify that the types you pass adhere to the C ABI’s default argument promotions.