Variadic Function Working in C

Variadic Function Working in C

If you've programmed in C, you've likely used functions like printf that accept a variable number of arguments. You might have wondered how this is possible. Well in this article, we will demystify the working of variadic function like printf.

❔ What is Variadic Function?

A variadic function is a function that accepts a variable number of arguments. In C, this is commonly seen with functions like printf and scanf.

These functions are useful in situations where the number of arguments needed is not known beforehand.

Variadic function are achieved through a special set of macros defined in the <stdarg.h> header file. These macros include va_list, va_start, va_arg, and va_end. These macros allows the programmer to access and manipulate the variable arguments passed to the function.

Variadic Function Declaration

To declare a variadic function, you use an ellipsis (...) in the parameter list to indicate that the function accepts a variable number of arguments.

// Example of a variadic function declaration
void printNumbers(int count, ...);

In this example, printNumbers is a variadic function that takes an integer count followed by a variable number of additional arguments.

Accessing Arguments

To access the variable arguments, you need to include the <stdarg.h> header file and use the following macros:

  • va_list: A type to hold the information about the variable arguments.
  • va_start: A macro to initialize the va_list variable.
  • va_arg: A macro to retrieve each argument from the list.
  • va_end: A macro to clean up the va_list variable.

Macros:

These macros are typically define in <stdarg.h> header file. We can define these macros right from scratch or could use the built-in macros provided by the gcc.

Below is the way to define them right from the scratch.

1 Own Implementation from Scratch:

1️⃣ va_list:

It is a type that acts as a pointer or handle to the variable argument list. Its definition is compiler-specific. Typically, it stores the current position in the argument list. It is a pointer type that can iterate over the arguments.

  • Typically it is pointer to char data type.
typedef char* va_list;

2️⃣ va_start:

It initializes the va_list variable to point to the first variable argument. It requires the last fixed argument to determine where the variable arguments start.

It takes two parameters:

  1. the va_list variable.
  2. the last fixed parameter before the variable arguments.

Its Definition is as follows:

#define va_start(ap, last) (ap = (va_list)(&last + sizeof(last)))

Explanation:

  • &last gives the address of the last fixed argument.
  • &last + last moves the pointer to the next argument in the list, which is the first variable argument.
  • ap is set to this address.
// OR

/*
 * This macro calculates the size of a type n rounded
 * up to the
 nearest multiple of sizeof(int).
 * This is necessary for
 proper alignment.
 */
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))

/* This macro initializes the va_list variable ap to
 * point to the
 first variable argument.
 * It does this by advancing past the last
 named
 * parameter v.
 */
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))

Visualization:

Consider we have the function:

void printNumbers(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize va_list to retrieve the arguments

   //... Process arguments
}

And we call this function as follows:

printNumbers(3, 10, 20, 30);

Here, 3 is the fixed argument, while 10, 20, 30 are variable arguments.

When the printNumbers(3, 10, 20, 30) is called, the stack layout in x86 architecture might look like this:

Stack AddressValue
......
0x…1430
0x…1020
0x…0C10
0x…083
......
Note:
  • In x86 Architecture:
    • The stack grows from higher memory to low memory.
    • Arguments are passed onto the stack in order from right to left. The most left argument would be the first one pushed onto the stack followed by the second argument from the left and so on.

The arguments are pushed onto the stack in reverse order:

  • 30 at address 0x...14
  • 20 at address 0x...10
  • 10 at address 0x...0C
  • 3 (the count) at address 0x...08

Next we declared a variable va_list args;, which is then passed to the va_start along with the fixed parameter.

  • va_list args is declared, which is a char*.
  • va_start(args, count) is called, where args is the va_list variable, and count is the last fixed argument.

Address Calculation:

  • The address of count is 0x...08.
  • &count + 1 computes the address immediately following count.
    • In pointer arithmetic, &count is 0x...08.
    • &count + 1 moves to 0x...08 + sizeof(int), which is 0x...08 + 4 = 0x...0C.
    • After this step args variable would be pointing to the very first variable argument.

1 Stack Before va_start:

Higher Memory Address
+--------------+   <--- Stack grows downwards (higher to
|      ...     |		lower memory)
+--------------+
|     30       |   <--- 0x...14 (arg3)
+--------------+
|     20       |   <--- 0x...10 (arg2)
+--------------+
|     10       |   <--- 0x...0C (arg1)
+--------------+
|      3       |   <--- 0x...08 (count)
+--------------+
|      ...     |
+--------------+

Lower Memory Address

2 Stack After va_start(args, count):

  • va_start(args, count) sets args to point to the first variable argument.
  • Address calculation: &count is 0x...08, so &count + 1 correctly points to 0x...0C.
Higher Memory Address
+--------------+ <--- Stack grows downwards (higher to
|      ...     |		lower memory)
+--------------+
|     30       | <--- 0x...14 (arg3)
+--------------+
|     20       | <--- 0x...10 (arg2)
+--------------+
|     10       | <--- 0x...0C (arg1) <-- args
|              |             (after va_start)
+--------------+
|      3       | <--- 0x...08 (count)
+--------------+
|      ...     |
+--------------+
Lower Memory Address

3️⃣ va_arg:

The va_arg macro is used to retrieve the next argument in the list of arguments provided to a variadic function. It updates the va_list to point to the next argument and returns the current argument, cast to the specified type.

It takes two parameters;

  • The va_list variable.
  • The type of the argument to retrieve.

Its definition is as follows:

#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
  • It does the following things:
    • Incrementing the pointer to point to the next argument.
    • Casting the current pointer to the specified type.
    • Returning the value at the current pointer.
// OR

/* This macro retrieves the next argument of type t
 * from the va_list
 and advances the va_list
 * pointer ap.
 */
#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

Visualization:

Consider the example given in the va_start section. Let's modify that example to process the variable arguments using va_arg.

void printNumbers(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize va_list to retrieve the arguments

	for (int i = 0; i < count; ++i) {
        int num = va_arg(args, int);  // Retrieve the next argument
        printf("%d ", num);
    }
   // Todo clean up the va_list
}

In this example we are using va_arg macro in the loop, whose iteration is based on the count which is received as the fixed argument. The va_arg will be called three times for i = 0, i = 1, and i = 2.

We call this function as follows:

printNumbers(3, 10, 20, 30);
Working:

As we call the function, the following stack memory layout is formed. As we all know that in x86 architecture parameters are pushed into the stack in reverse order (right to left) and stack grows from higher memory to lower memory. This means that the firstly pushed item would be at higher memory address than to items pushed after it.

Stack AddressValue
......
0x…1430
0x…1020
0x…0C10
0x…083
......
  • Initialization with va_start:
    • va_start(args, count) sets args to point to the first variable argument.
    • args initially points to 0x...0C. The address of the first variable argument.
  • First va_arg(args, int) Call, i = 0:
    • va_arg(args, int):
      • Increment Pointer:
        • ap += sizeof(int): Moves args from 0x...0C to 0x...10.
      • Adjust Pointer Back and Dereference:
        • (ap - sizeof(int)) gives the current address.
        • *(int*)(0x...0C): Dereferences the value at 0x...0C, retrieving 10.

Updated args:

Higher Memory Address
+--------------+<--- Stack grows downwards
|      ...     |
+--------------+
|     30       |<--- 0x...14 (arg3)
|              |
+--------------+
|     20       |<--- 0x...10 (arg2) <--args (after 
|              |						first va_arg)
+--------------+
|     10       |<--- 0x...0C (arg1)
|              |
+--------------+
|      4       |<--- 0x...08 (count)
|              |
+--------------+
|      ...     |
+--------------+
Lower Memory Address
  • Second va_arg(args, int) call, i = 1:
    • args pointing at 0x...10
    • va_arg(args, int):
      • Increment Pointer:
        • ap += sizeof(int) moves args from 0x...10 to 0x...14
      • Subtract and Dereference:
        • (ap - sizeof(int)) gives the current value which is 0x...10 by subtracting the size of int.
        • *(int*)(0x...10) = dereferences and returns the value at 0x...10 which is 20.

Updated args:

Higher Memory Address
+--------------+<--- Stack grows downwards
|      ...     |
+--------------+
|     30       |<--- 0x...14 (arg3) <--- args (after second
|              |							va_arg)
+--------------+
|     20       |<--- 0x...10 (arg2)
|              |
+--------------+
|     10       |<--- 0x...0C (arg1)
|              |
+--------------+
|      4       |<--- 0x...08 (count)
|              |
+--------------+
|      ...     |
+--------------+
Lower Memory Address
  • Third va_arg(args, int) call, i = 2:
    • args pointing at 0x...14
    • va_arg(args, int):
      • Increment Pointer
        • ap += sizeof(int) moves args to point to the next integer address which is 0x...18.
      • Subtract the size of int and dereference:
        • (ap - sizeof(int)) gives the current value which is 0x...14 by subtracting the size of int.
        • *(int*)(0x...14) = dereferences and returns the value at 0x...14 which is 30.

Updated args:

Higher Memory Address
+--------------+<--- Stack grows downwards
|      ...     |
+--------------|
| garbage value|<--- 0x...18 (garbage value) <-- args
|              |                (after third va_arg)
+--------------+
|     30       |<--- 0x...14 (arg3)
|              |
+--------------+
|     20       |<--- 0x...10 (arg2)
|              |
+--------------+
|     10       |<--- 0x...0C (arg1)
|              |
+--------------+
|      4       |<--- 0x...08 (count)
|              |
+--------------+
|      ...     |
+--------------+
Lower Memory Address

4️⃣ va_end:

The va_end macro is used to clean up a va_list variable after it has been used in a variadic function. It is a necessary part of using variadic functions because it ensures that any resources allocated for the va_list are properly released.

It takes one parameter:

  1. va_list

It's definition is as follows:

#define va_end(ap) (ap = (va_list)0)

//OR

#define va_end(ap) (ap = (va_list) NULL)
  • It sets received args to NULL indicating the end of argument processing.
Complete Example:
#include <stdio.h>

typedef char* va_list;

#define va_start(ap, last) (ap = (va_list)(&last + 1))
#define va_arg(ap, type) (*(type*)((ap += sizeof(type)) - sizeof(type)))
#define va_end(ap) (ap = (va_list)0)

void printNumbers(int count, ...) {
    va_list args;
    va_start(args, count); // Initialize va_list to retrieve the arguments

    for (int i = 0; i < count; ++i) {
        int num = va_arg(args, int); // Retrieve the next argument
        printf("%d ", num);
    }

    va_end(args);   // Clean up the va_list
    printf("\n");
}

int main() {
    printNumbers(4, 10, 20, 30, 40);
    printNumbers(3, 5, 15, 25);
    return 0;
}

5️⃣ va_copy:

The va_copy macro is used to create a copy of a va_list variable. This can be particularly useful when you need to iterate over the same set of arguments multiple times within a function.

Syntax:
va_copy(dest, src); // Copies va_list variable src to the va_list variable dest.
Implementation:
#define va_copy(dest, src)   (dest = src)
Example:
#include <stdio.h>

// Define a type for the argument list (assuming a pointer implementation)
typedef char* va_list;

// Define the size of a pointer for alignment
#define VA_ARG_SIZE(type) (((sizeof(type) + sizeof(int) - 1) / sizeof(int)) * sizeof(int))

// Define the custom macros for variadic argument handling
#define va_start(ap, last)   (ap = (va_list)&last + VA_ARG_SIZE(last))
#define va_arg(ap, type)     (*(type*)((ap += VA_ARG_SIZE(type)) - VA_ARG_SIZE(type)))
#define va_end(ap)           (ap = (va_list)0)
#define va_copy(dest, src)   (dest = src)

// Variadic function that sums all integer arguments twice for demonstration
int sum_twice(int count, ...) {
    va_list args1, args2;
    va_start(args1, count);

    // Copy args1 to args2 using custom va_copy
    va_copy(args2, args1);
    
    int total1 = 0;
    for (int i = 0; i < count; i++) {
        total1 += va_arg(args1, int);
    }

    int total2 = 0;
    for (int i = 0; i < count; i++) {
        total2 += va_arg(args2, int);
    }

    va_end(args1);
    va_end(args2);

    return total1 + total2;
}

int main() {
    printf("Sum twice: %d\n", sum_twice(4, 1, 2, 3, 4)); // Output: Sum twice: 20
    return 0;
}

2 Implementation Using Built-in Macros:

Some compilers provide built-in macros that can be used to implement variadic functions in more direct way.

GCC provides built-in macros like __builtin_va_list, __builtin_va_start, __builtin_va_arg, and __builtin_va_end which can be used instead of defining own va macros.

// Define a type for the argument list
typedef __builtin_va_list va_list;

// Define the built-in macros for variadic argument handling
#define va_start(ap, last)   __builtin_va_start(ap, last)
#define va_arg(ap, type)     __builtin_va_arg(ap, type)
#define va_end(ap)           __builtin_va_end(ap)

#define va_copy(dest, src)   __builtin_va_copy(dest, src)
Explanation:
  • typedef __builtin_va_list va_list;: This defines va_list using GCC's built-in __builtin_va_list.
  • #define va_start(ap, last) __builtin_va_start(ap, last): This defines va_start using the built-in __builtin_va_start.
  • #define va_arg(ap, type) __builtin_va_arg(ap, type): This defines va_arg using the built-in __builtin_va_arg.
  • #define va_end(ap) __builtin_va_end(ap): This defines va_end using the built-in __builtin_va_end.
  • #define va_copy(dest, src) __builtin_va_copy(dest, src): This defines va_copy using the built-in __builtin_va_copy.
Note:
  • Using built-in macros can be compiler-specific. The example above is specific to GCC.
  • Ensure that your compiler supports these built-in macros before using them.

#include <stddef.h>
#include <stdarg.h>

static void itoa(int value, char* str, int base) {
    char* ptr = str;
    char* ptr1 = str;
    char tmp_char;
    int tmp_value;

    if (value == 0) {
        *str++ = '0';
        *str = '\0';
        return;
    }

    while (value != 0) {
        tmp_value = value % base;
        *ptr++ = (tmp_value < 10) ? (tmp_value + '0') : (tmp_value - 10 + 'a');
        value /= base;
    }

    *ptr-- = '\0';

    while (ptr1 < ptr) {
        tmp_char = *ptr;
        *ptr = *ptr1;
        *ptr1 = tmp_char;
        ptr--;
        ptr1++;
    }
}

int vsprintf(char* buffer, const char* format, va_list args) {
    char* buf_ptr = buffer;
    const char* fmt_ptr = format;
    char ch;
    char tmp[32];

    while ((ch = *fmt_ptr++) != '\0') {
        if (ch != '%') {
            *buf_ptr++ = ch;
            continue;
        }

        ch = *fmt_ptr++;
        switch (ch) {
            case 'd': {
                int value = va_arg(args, int);
                itoa(value, tmp, 10);
                for (char* tmp_ptr = tmp; *tmp_ptr != '\0'; tmp_ptr++) {
                    *buf_ptr++ = *tmp_ptr;
                }
                break;
            }
            case 'x': {
                int value = va_arg(args, int);
                itoa(value, tmp, 16);
                for (char* tmp_ptr = tmp; *tmp_ptr != '\0'; tmp_ptr++) {
                    *buf_ptr++ = *tmp_ptr;
                }
                break;
            }
            case 's': {
                char* str = va_arg(args, char*);
                while (*str != '\0') {
                    *buf_ptr++ = *str++;
                }
                break;
            }
            case 'c': {
                char value = (char)va_arg(args, int);
                *buf_ptr++ = value;
                break;
            }
            default: {
                *buf_ptr++ = ch;
                break;
            }
        }
    }

    *buf_ptr = '\0';
    return buf_ptr - buffer;
}

 

void printf(const char* format, ...) {
    va_list args;
    va_start(args, format);

    char buffer[256]; // Example buffer size
    vsprintf(buffer, format, args);

    va_end(args);

    puts(buffer);
}
void puts(const char* str) {
    while (*str) {
        putchar(*str++);
    }
}