8.15 Variadic Functions and Macros

C allows variadic functions – functions that can accept a variable number of arguments, like printf or scanf, using the ... syntax and stdarg.h macros.

// C Example (for comparison)
#include <stdio.h>
#include <stdarg.h>

// 'count' indicates how many numbers follow.
void print_ints(int count, ...) {
    va_list args;
    va_start(args, count); // Initialize args to retrieve arguments after 'count'.
    printf("Printing %d integers: ", count);
    for (int i = 0; i < count; i++) {
        int value = va_arg(args, int); // Retrieve next argument as an int.
        printf("%d ", value);
    }
    va_end(args); // Clean up.
    printf("\n");
}

int main() {
    print_ints(3, 10, 20, 30); // Call with 3 variable arguments.
    print_ints(5, 1, 2, 3, 4, 5); // Call with 5 variable arguments.
    return 0;
}

Variadic functions in C are powerful but lack type safety for the variable arguments, which can lead to runtime errors if the types or number of arguments retrieved using va_arg don’t match what was passed.

Rust does not support defining C-style variadic functions directly in safe code. You can call C variadic functions from Rust using FFI (Foreign Function Interface) within an unsafe block, but you cannot define your own safe variadic functions using ....

The idiomatic way to achieve similar functionality in Rust (accepting a varying number of arguments) is through macros. Macros operate at compile time, expanding code based on the arguments provided. They are type-safe and more flexible than C variadics.

// Define a macro named 'print_all'
macro_rules! print_all {
    // Match one or more expressions separated by commas
    ( $( $x:expr ),+ ) => {
        // Repeat the following code block for each matched expression '$x'
        $(
            print!("{} ", $x); // Print each expression
        )+
        println!(); // Print a newline at the end
    };
}

fn main() {
    print_all!(1, "hello", true, 3.14); // Call the macro with different types
    print_all!(100, 200);                 // Call with just integers
}

Macros like println! itself are prime examples of this pattern. They provide a type-safe, compile-time mechanism for handling variable arguments, which aligns better with Rust’s safety goals than C-style variadics. Macros are a more advanced topic covered later in the book.