zneak
zneak

Reputation: 138211

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?

Upvotes: 1

Views: 768

Answers (4)

Jordan Brown
Jordan Brown

Reputation: 369

Here's a trick that I believe works in all versions of ANSI C: wrap the va_list in a structure, and pass around pointers to that.

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

struct va_list_wrapper {
        va_list ap;
};

void print_integer(struct va_list_wrapper *wap) {
    printf("%i\n", va_arg(wap->ap, int));
}

void vprint_integers(int count, struct va_list_wrapper *wap) {
    for (int i = 0; i < count; ++i) {
        print_integer(wap);
    }
}

void print_integers(int count, ...) {
    struct va_list_wrapper wap;
    va_start(wap.ap, count);
    vprint_integers(count, &wap);
    va_end(wap.ap);
}

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

Upvotes: 0

zneak
zneak

Reputation: 138211

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.

Upvotes: 2

M.M
M.M

Reputation: 141638

The problem in the second program is that if va_list is an array type, then the function parameter ap has its type adjusted to be a pointer type. So the argument has a different level of indirection in each case.

Since C11 we can solve this with a generic selector to test whether or not ap is still a va_list:

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

This solution was inspired by this answer which used a macro for the two cases that required manual configuration .

Notes about the _Generic:

  • We have to test &ap because, since C18, array-to-pointer decay is performed on the first expression.
  • Originally I had _Generic(&ap, va_list*: &ap, default: (va_list *)ap) however this is rejected by compilers which check for constraint violations in all branches for all inputs -- although do not go on to check constraint violations in the surrounding expressions for unselected branches.

Upvotes: 2

Schwern
Schwern

Reputation: 165396

You may be out of luck. 7.15(3) says what you're doing has indeterminate effects.

The object ap may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.

The call to va_arg in print_integer is causing ap in print_integers to be indeterminate. On one architecture it's being incremented, on another it isn't.

As for what va_list is, it could be anything...

...which is an object type suitable for holding information needed by the macros va_start, va_arg, va_end, and va_copy.

You may wish to consider a different approach to solving the underlying problem.

Upvotes: -1

Related Questions