Search code examples
c

What is the cross-platform way to pass a va_list by reference?


I wrote a function that accepts a va_list, and that is meant to be invoked iteratively by its caller. It should modify the va_list and changes should persist back in the caller so that the next call to the function will proceed with the next argument.

I can't post that code specifically, but here's a snippet that reproduces the situation (godbolt link):

#include <stdarg.h>
#include <stdio.h>

void print_integer(va_list ap) {
    printf("%i\n", va_arg(ap, int));
}

void vprint_integers(int count, va_list ap) {
    for (int i = 0; i < count; ++i) {
        print_integer(ap);
    }
}

void print_integers(int count, ...) {
    va_list ap;
    va_start(ap, count);
    vprint_integers(count, ap);
    va_end(ap);
}

int main() {
    print_integers(3, 1, 2, 3);
}

This works (prints "1 2 3") on my x86 platform because va_list is passed by "reference" (it's probably declared as an array of one element, so va_list arguments decay to a pointer). However, it does not work on my ARM platform (prints "1 1 1"), where va_list seems to be defined as a pointer to something. On that platform, va_arg always returns the first argument.

The next best option seems to be to make ap a pointer:

#include <stdarg.h>
#include <stdio.h>

void print_integer(va_list *ap) {
    printf("%i\n", va_arg(*ap, int));
}

void vprint_integers(int count, va_list ap) {
    for (int i = 0; i < count; ++i) {
        print_integer(&ap);
    }
}

void print_integers(int count, ...) {
    va_list ap;
    va_start(ap, count);
    vprint_integers(count, ap);
    va_end(ap);
}

int main() {
    print_integers(3, 1, 2, 3);
}

This works on ARM (with va_arg(*ap, ...)), but it does not compile on x86. When I try print_integer(&ap) on x86, Clang says:

error: incompatible pointer types passing 'struct __va_list_tag **' to parameter of type 'va_list *' (aka '__builtin_va_list *')

This only seems to happen when taking the address of a va_list passed as an argument, not when it's taken from a local variable. Unfortunately, I do need my v variant to take a va_list object and not a pointer to it.

It's easy to get consistent cross-platform value semantics for va_list using va_copy. Is there a cross-platform way to get consistent reference semantics for va_list?


Solution

  • The thing at issue here is that va_list, on the x86 platform, is defined as an array of 1 element (let's call it __va_list_tag[1]). It decays to a pointer when accepted as an argument, so &ap is wildly different depending on whether ap is a parameter of the function (__va_list_tag**) or a local variable (__va_list_tag(*)[1]).

    One solution that works for this case is simply to create a local va_list, use va_copy to populate it, and pass a pointer to this local va_list. (godbolt)

    void vprint_integers(int count, va_list ap) {
        va_list local;
        va_copy(local, ap);
        for (int i = 0; i < count; ++i) {
            print_integer(&local);
        }
        va_end(local);
    }
    

    In my case, vprint_integers and va_copy are necessary because the interface of vprint_integers accepts a va_list and that cannot change. With more flexible requirements, changing vprint_integers to accept a va_list pointer is fine too.

    va_list isn't specified to be anything in particular, but it's defined to be an object type, so there's not really a reason to believe that you can't take or pass its address. Another very similar solution that entirely bypasses the question of whether you can take the address of a va_list is to wrap the va_list in a struct and pass a pointer to that struct.