6

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.

Yu Hao
  • 119,891
  • 44
  • 235
  • 294
Rémi Peyre
  • 410
  • 3
  • 12

3 Answers3

2

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?).

this
  • 5,229
  • 1
  • 22
  • 51
cremno
  • 4,672
  • 1
  • 16
  • 27
  • Hello cremno, I have not grasped all the details, but I have the impression that your point is linked to the following clause from the standard: “The argument `type` is a type name specified so that the type of a pointer to an object that has the specified type can be obtained simply by adding a `*` to `type`”. But nothing guarantees that `va_list` would be such a type, as e.g. an array type would not work... Is it correct? On the other hand, this does not seem to explain how the “void*” trick of Eric Tsui worked... – Rémi Peyre Jun 24 '15 at 00:46
  • 1
    @Nancy-N: It's a **trick**. But he has to explain that one as that's part of his answer. It'll probably work everywhere you'll ever care about. THat doesn't change the fact it's UB (the description posted by him is also erroneous). [Here](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=50581)'s a GCC bug report, whose reporter wants to do something similar or even the same as you. It even mentions an edge case in C90! Maybe reading it will clear things a bit more up than my answer. It also mentions using a structure as a possible solution. – cremno Jun 24 '15 at 01:57
  • @cremno, thanks for the correction. So, I did more studies for things about `va_list` and `va_list*` under different `va_list` implementation, that's indeed the problem. – Eric Tsui Jun 24 '15 at 04:44
  • @cremno: Thanks for this further explanation. Thanks to it and to Eric Tsui's posts, now I do understand what went wrong. – Rémi Peyre Jun 24 '15 at 09:13
  • 1
    @cremno I'm not sure it is correct to wrap a `va_list` in a struct and pass it to a function. As you say, it may be an array type and so it is implicitly passed by reference; whereas the struct wrap option will make a byte-wise copy. `va_copy` is supposed to be used any time that a `va_list` should be copied, so this could trash the list. – M.M Jun 24 '15 at 10:17
1
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.

Community
  • 1
  • 1
Eric Tsui
  • 1,924
  • 12
  • 21
  • 1
    That works indeed; terrific! :-D If you have any more detailed explanation, I am much eager to hear it: Why did the previous line fail? Why does the new one work? It this “void* trick” portable?... – Rémi Peyre Jun 23 '15 at 23:19
  • It remains unclear to me. When `printf_variant` calls `vffprintf`, the variable argument list for `vffprintf` is {`stdout`, `format`, `vlist_0`}, with `vlist_0` being the variable arguments list of `printf_variant`, namely {`1929`}. So, when calling `va_arg (vlist_1, va_list)` (I have put suffixes `_0` and `_1` to make the difference between both `vlist` clear), the next argument of `vlist_1` is {`1929`}, which concretely may be a `struct {char *, int}`. But this is not an int, is it ?... And anyway, how might that be converted to a `void *`?! Sorry for not understanding more easily... – Rémi Peyre Jun 24 '15 at 00:34
  • 1
    @Nancy-N, what I tried to do is something like this `va_copy (auxvlist, va_arg (vlist, va_list*))`, but it gives the warning. So, I change it to ``va_copy (auxvlist, va_arg (vlist, void*))`` – Eric Tsui Jun 24 '15 at 03:23
  • @Nancy-N Thanks. I also learned more from the question. And I posted several helpful references which I studied, especially the 2nd link [Is GCC mishandling a pointer to a va_list passed to a function?](http://stackoverflow.com/questions/8047362/is-gcc-mishandling-a-pointer-to-a-va-list-passed-to-a-function) – Eric Tsui Jun 24 '15 at 04:28
  • @ Eric Tsui - Thank you for your complementary explanations and your references; now I think I have got it. – Rémi Peyre Jun 24 '15 at 09:00
  • @ Eric Tsui: I have marked cremno's answer as accepted rather than yours, as cremno's answer directly stresses out the key point; but your posts were at least as helpful to me as cremno's for my understanding! – Rémi Peyre Jun 24 '15 at 09:16
  • @Nancy-N: I agree with you :) This question itself is quite good and involves many underneath mechanism. – Eric Tsui Jun 24 '15 at 09:26
1

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.

this
  • 5,229
  • 1
  • 22
  • 51