Search code examples
cvariadic-functions

Sharing va_list between my functions and vprintf-like library functions


I would like to add some special processing to formatted printing (with variadic arguments), and I found that the use of va_lists in my code does not comply with the C standard. Although it does not cause any error when compiled with GCC, I would like it to be compliant. The following is a simplified version:

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

typedef struct S_ {
    int i;
    va_list *v;
} S;

void fb( S* s )
{
    int x = va_arg( *s->v, int );             // Line A
    vfprintf( stdout, "c:%c s:%s\n", *s->v ); // Line B
    int y = va_arg( *s->v, int );             // Line C
    fprintf( stdout, "d:%d\n", s->i * x * y );
}

void fa( int i, ... )
{
    S s = { i };
    va_list v;
    va_start( v, i );
    s.v = &v;
    fb( &s );
    va_end( v );
}

void main()
{
    fa( 1, 2, 'c', "string", 3 );
}

The standard says on page 249:

If access to the varying arguments is desired, the called function shall declare an object (generally referred to as ap in this subclause) having type va_list. 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. (It is permitted to create a pointer to a va_list and pass that pointer to another function, in which case the original function may make further use of the original list after the other function returns.)

As I understand, there is no problem if Line A is followed by Line B since the caller (fb) uses va_arg before invoking the callee (vfprintf). I suppose (though did not find it in the text) the reason for allowing this is that the va_list is in the scope of the caller, therefore any change done by va_arg will be reflected in the object's state when it is passed to the callee (vfprintf).

However, I reckon that Line B followed by Line C is not in line with the standard since the callee (vfprintf) received a va_list, not a va_list*, therefore the value of *s->v is indeterminate after Line B, and should not be used as in Line C (rather it should be closed with va_end). I suppose (though did not find it in the text) the reason for not allowing the use of va_list in the caller (fb) after the callee (vfprintf) returned is that *s->v may have been (depending on the implementation) copied to a local variable of vfprintf (at the function call), and in this case, since that copy is destroyed when vfprintf returns, the changes it made (consuming a char and a char*) are not reflected in the original object, so y will not be assigned 3 but 'c' (probably in some distorted way as a char is narrower than an int).

Unfortunatelly, I cannot make vfprintf accept va_list* but need to "mix it in" (possibly call va_arg before and after calling it with the same va_list).

Is my argument correct and my code incorrect? If so, how can I fix it?


Solution

  • Your reasoning is correct and your code is incorrect. Now, it might happen to work on some architectures and not work on some architectures. For example, if an architectures implements va_list as a pointer to underlying state, then when it's passed to a function and that function updates the iteration state, that might be visible in the calling scope, and then it would work for you to continue the iteration there. But if the architectures implements it as some kind of struct which has the iteration state by value, then when it's passed to a function and that function updates the iteration state, that will not be visible in the calling scope, so your code would not work. That's what happens with undefined behavior -- there is no guarantee that it works or does not work.

    I see no general way to achieve this other than to basically re-implement vfprintf(), but take and operate on a va_list * rather than a va_list. Once you passed a va_list by value to a function, you cannot do va_arg on that va_list in the calling scope anymore. In the C standard's section on vfprintf(), it has a footnote that confirms this:

    As the functions vfprintf, vfscanf, vprintf, vscanf, vsnprintf, vsprintf, and vsscanf invoke the va_arg macro, the value of arg after the return is indeterminate.

    va_copy() doesn't help here. You can't va_copy() after the call to vfprintf(), because the va_list is already indeterminate at that time. And if you va_copy() before the call to vfprintf(), the copied va_list would still be in the state before the arguments used for the vfprintf() are read. So you would need to somehow "skip over" the arguments used for the vfprintf(), in order to get to the argument after those arguments. In order to do that, you would need to know the number of arguments used for the vfprintf() and their types. So basically you would need to re-implement the format string parsing logic of vfprintf() to do that. (And if you're going to do that, you might as well just re-implement vfprintf() to take a va_list *.)