The question \'Pass va_list or pointer to va_list?\' has an answer which quotes the standard (ISO/IEC 9899:1999 - §7.15 \'Variable arguments , f
As others have noted, this issue manifests itself when va_list is an array type. This is allowed by the standard, which only says that va_list must be an "object type".
You can fix the test_val() function like so:
static void test_val(const char *fmt, va_list args)
{
va_list args_copy;
/* Note: This seemingly unnecessary copy is required in case va_list
* is an array type. */
va_copy(args_copy, args);
test_ptr(fmt, &args_copy);
va_end(args_copy);
}
I think va_list must be declared as an array type, which “boils down” to a pointer type when declared as a parameter of a function. Therefore, the & applied to the va_list type in test_val yields a pointer to a pointer type, not a pointer to an array type, however, the test_ptr function declares one of its parameters as a pointer to array type, which is what is actually provided in the test function.
This is a known problem. On some architectures (in particular x86-64), va_list needs to be more complex than a simple pointer to the stack, for example because some arguments might be passed in registers or out-of-band in some other way (see this answer for the definition of va_list on x86-64).
On such architectures, it is common to make va_list an array type so that parameters of type va_list will be adjusted to pointer types, and instead of the whole structure, only a single pointer needs to be passed.
This should not violate the C standard, which only says that va_list must be a complete object type and even explicitly accounts for the fact that passing a va_list argument might not actually clone the necessary state: va_list objects have indeterminate value if they are passed as arguments and consumed in the called function.
But even if making va_list an array type is legal, it still leads to the problems you experienced: As parameters of type va_list have the 'wrong' type, eg struct __va_list_tag * instead of struct __va_list_tag [1], it will blow up in cases where the difference between arrays and pointers matter.
The real problem is not the type mismatch gcc warns about, but the by-pointer instead of by-value argument passing semantics: &args in test_val() points to the intermediate pointer variable instead of the va_list object; ignoring the warning means that you'll invoke va_arg() in test_ptr() on the pointer variable, which should return garbage (or segfault if you're lucky) and corrupt the stack.
One workaround is to wrap your va_list in a structure and pass that around instead. Another solution I've seen in the wild, even here on SO, is to use va_copy to create a local copy of the argument and then pass a pointer to that:
static void test_val(const char *fmt, va_list args)
{
va_list args_copy;
va_copy(args_copy, args);
test_ptr(fmt, &args_copy);
va_end(args_copy);
}
This should work in practice, but technically it might or might not be undefined behaviour, depending on your interpretation of the standard:
If va_copy() is implemented as a macro, no parameter adjustments are performed, and it might matter that args is not of type va_list. However, as it is unspecified whether va_copy() is a macro or a function, one might argue that it at least could be a function and parameter adjustments are implicitly assumed in the prototype given for the macro. It might be a good idea to ask the officials for clarification or even file a defect report.
You could also use your build system to deal with the issue by defining a configuration flag like HAVE_VA_LIST_AS_ARRAY so you can do the right thing for your particular architecture:
#ifdef HAVE_VA_LIST_AS_ARRAY
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) ((va_list *)(arg))
#else
#define MAKE_POINTER_FROM_VA_LIST_ARG(arg) (&(arg))
#endif
static void test_val(const char *fmt, va_list args)
{
test_ptr(fmt, MAKE_POINTER_FROM_VA_LIST_ARG(args));
}
The problem is not specific to va_list. The following code results in a similar warning:
typedef char *test[1];
void x(test *a)
{
}
void y(test o)
{
x(&o);
}
The problem stems from the way C handles function variables that are also arrays, probably due to the fact that arrays are passed as reference and not by value. The type of o is not the same as the type of a local variable of type test, in this case: char *** instead of char *(*)[1].
Returning to the original issue at hand, the easy way to work around it is to use a container struct:
struct va_list_wrapper {
va_list v;
};
and there would be no typing problems passing a point to it.