Put a va_list variable inside... a variable argument list (!)

2k Views Asked by At

I would like to design a function which takes a variable number of arguments, one of these arguments being itself a va_list; but something gets wrong in my code and I do not understand what...

WARNING — My question is not about designing a code doing what I wish to do (I have found a way to bypass the problem), but only about understanding what I did wrong...

To explain my question, let us start with a simple example: namely, a function ffprintf which acts like fprintf, but writing its contents onto several strings, strings whose number is indicated by the first argument of ffprintf, and whose identities are given by the very next arguments (the number of these arguments may vary from one call to another, so you have to use a variable argument list). Such a function would be used like this:

FILE *stream0, *stream1, *stream2;
int a, b;
ffprintf (3, stream0, stream1, stream2, "%d divided by %d worths %f", a, b, (double)a / b);

And its code would be:

void ffprintf (int z, ...)
 {va_list vlist, auxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *); // Getting the next stream argument
   }
  char const *format = va_arg (vlist, char const *); // Getting the format argument
  for (int i = 0; i < z; ++i)
   {va_copy (auxvlist, vlist); // You have to work on a copy "auxvlist" of "vlist", for otherwise "vlist" would be altered by the next line
    vfprintf (streams[i], format, auxvlist);
    va_end (auxvlist);
   }
  va_end (vlist);
  free (streams);
 }

That works fine. Now, there is also the standard function vfprintf, whose prototype is vfprintf (FILE *stream, char const* format, va_list vlist);, and which you use like this to create another function having a variable argument list:

void fprintf_variant (FILE *stream, char const* format, ...)
 {
  va_list vlist;
  va_start (vlist, format);
  vfprintf (stream, format, vlist);
  va_end (vlist);
 }

That works fine too. Now, my goal is to combine both ideas to create a function which I would call vffprintf, which you would use like this:

FILE *stream0, *stream1, *stream2;
void fprintf_onto_streams012 (char const *format, ...)
 {va_list vlist;
  va_start (vlist, format);
  vffprintf (3, stream0, stream1, stream2, format, vlist);
  va_end (vlist);
 }

I have designed the following code:

void vffprintf (int z, ...)
 {va_list vlist, auxvlist, auxauxvlist;
  va_start (vlist, z);
  FILE **streams = malloc (z * sizeof(FILE *));
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *);
   }
  char const *format = va_arg (vlist, char const *);
  va_copy (auxvlist, va_arg (vlist, va_list)); // Here I get the next argument of "vlist", knowing that this argument is of "va_list" type
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist, auxvlist);
    vfprintf (streams[i], format, auxvlist);
    va_end (auxauxvlist);
   }
  va_end (auxvlist);
  va_end (vlist);
  free (streams);
 }

This code compiles without a hitch, but it does not work properly... For instance, if I write the following complete code:

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

void vffprintf (int z, ...)
 {va_list vlist, auxvlist, auxauxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist, FILE *);
   }
  char const *format = va_arg (vlist, char const *);
  va_copy (auxvlist, va_arg (vlist, va_list));
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist, auxvlist);
    vfprintf (streams[i], format, auxauxvlist);
    va_end (auxauxvlist);
   }
  va_end (auxvlist);
  va_end (vlist);
  free (streams);
 }

void printf_variant (char const *format, ...)
 {va_list vlist;
  va_start (vlist, format);
  vffprintf (1, stdout, format, vlist);
  va_end (vlist);
 }

int main (void)
 {printf_variant ("Ramanujan's number is %d.\n", 1729);
  return 0;
 }

I get a segfault!... Why?!

P.-S.: Sorry for that very long question; but I wanted it to be perfectly clear, for it is rather technical...

P.-S.2: I used deliberately both tags "va-list" and "variableargumentlists" for this question, because which interests me is va_list, seen as a type, inside a (other) variable argument list, seen as a list... So these are really two different concepts here.

3

There are 3 best solutions below

5
On BEST ANSWER

The description of va_arg in the final draft of C11 (N1570) contains (type is the second argument):

if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined

va_list is allowed to be an array type (the standard requires it to be a so-called “complete object type”) and it seems your implementation makes use of this possibility. You probably know that in C arrays can't be passed as arguments as they decay into pointers, and the type of such a pointer isn't compatible with the original array type.

For example: int * isn't compatible with int [1]. So if you really need to pass an array or a va_list portably, then define a struct with a va_list member and pass that (see Why can't we pass arrays to function by value?).

7
On
void vffprintf (int z, ...)
 {
  //...
  va_copy (auxvlist, va_arg (vlist, va_list));//this line has the problem 
   //...
 } 

Just a quick and tricky way like this, it will work.

  void vffprintf (int z, ...)
 {
  //...
  va_copy (auxvlist, va_arg (vlist, void*));
   //...
 }

Here are some references about var_arg and va_list, which should have provided detailed and thorough explanation.

1) Pass va_list or pointer to va_list?

2) Is GCC mishandling a pointer to a va_list passed to a function?

3) What is the format of the x86_64 va_list structure?

Hope they are helpful.

3
On

You might need to wrap the type va_list into a struct, if you want to retrieve it using va_arg():

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

typedef struct
{
    va_list list ;  
} my_list ;

void vffprintf (int z, ...)
 {my_list vlist, auxvlist, auxauxvlist;
  FILE **streams = malloc (z * sizeof(FILE *));
  va_start (vlist.list, z);
  for (int i = 0; i < z; ++i)
   {streams[i] = va_arg (vlist.list, FILE *);
   }
  char const *format = va_arg (vlist.list, char const *);
  my_list parent = va_arg (vlist.list, my_list) ;
  va_copy (auxvlist.list, parent.list );
  for (int i = 0; i < z; ++i)
   {va_copy (auxauxvlist.list, auxvlist.list);
    vfprintf (streams[i], format, auxauxvlist.list);
    va_end (auxauxvlist.list);
   }
  va_end (auxvlist.list);
  va_end (vlist.list);
  free (streams);
 }

void printf_variant (char const *format, ...)
 {my_list vlist;
  va_start (vlist.list, format);
  vffprintf (1, stdout , format, vlist);
  va_end (vlist.list);
 }

int main (void)
 {printf_variant ("Ramanujan's number is %d.\n", 1729 );
  return 0;
 }

The problem stems from the fact that array and a pointer of the same type are not compatible, and va_list is defined as an array. Then you try to get that type:

va_arg (vlist, va_list)

So you tell va_arg you are getting an array but if fact the passed va_list has decayed to a pointer. You should use the pointer version of va_list, but you don't know the real definition of va_list, in the first place so you cannot obtain the pointer version of it.

The solution is to wrap va_list into a type you control, a struct.