DF24
DF24

Reputation: 53

C variadic function: need to keep multiple va_list but old ones are overwritten by latest one

I run this in MSVC v142. I need to keep multiple va_list and then pass all of them in an array to another API. However, the old va_list were getting overwritten by the newer one.

void toVaList(va_list *out, int count, ...)
{
    va_list args;
    va_start(args, count);
    vprintf("toVaList: %d %d\n", args);
    *out = args;
}

int main()
{
    va_list args1;
    toVaList(&args1, 2, 11, 22);
    vprintf("first va_list: %d %d\n", args1);
    va_list args2;
    toVaList(&args2, 2, 33, 44);
    vprintf("first va_list: %d %d\n", args1); //now args1 are overwritten
    vprintf("second va_list: %d %d\n", args2);
    va_end(args1);
    va_end(args2);

    return 0;
}

The output is

toVaList: 11 22
first va_list: 11 22
toVaList: 33 44
first va_list: 33 44
second va_list: 33 44

Is it possible to resolve this issue?

Edit:

Now memcpy works for me. I copied the va_list to a separate memory and kept the original va_list in the function body wrapped by va_start and va_end. It looks like the copied va_list outside of the function body works well.

vprintf doesn't invalidate the va_list args, since it might have made a va_copy inside its implementation.

void toVaList1(va_list *out, int count, ...)
{
    va_list args;
    va_start(args, count);
    memcpy(out, args, 256);
    vprintf("toVaList1: %d %d\n", args);
    va_end(args);
}

int main()
{
    char args1[256];
    toVaList1((va_list *)args1, 2, 11, 22);
    vprintf("first va_list: %d %d\n", args1);
    char args2[256];
    toVaList1((va_list *)args2, 2, 33, 44);
    vprintf("first va_list: %d %d\n", args1);
    vprintf("second va_list: %d %d\n", args2);
    char args3[256];
    toVaList1((va_list *)args3, 2, 55, 66);
    vprintf("first va_list: %d %d\n", args1);
    vprintf("second va_list: %d %d\n", args2);
    vprintf("third va_list: %d %d\n", args3);
}

The output is

toVaList1: 11 22
first va_list: 11 22
toVaList1: 33 44
first va_list: 11 22
second va_list: 33 44
toVaList1: 55 66
first va_list: 11 22
second va_list: 33 44
third va_list: 55 66

Edit:

Briefly describe the scenario for this requirement. We have a list of messages with id defined in XML, and an internal API that takes msgId and va_list as arguments, which uses va_arg() to read va_list:

ID   Message
1    This is a message containing integer %d and string %s.
2    This is another message containing double %f.
.....


void internalAPI_PrintMsg(int msgId, va_list ap)
{
    char resultMsg[512];
    //lookup message id in XML and substitute arguments
    //in the loop through found message body...
    switch(char)
    {
    case 'd': 
        int i = va_arg(ap, int);
        //substitute the integer
        break;
    case 'f':
        double d = va_arg(ap, double);
        //substitute the double
        break;
    ....
    }
    PrintResultMessage(resultMsg);
}

We already have API to print single message:

void printMsg(int msgId, ...)
{
    va_list args;
    va_start(args, msgId);
    internalAPI_PrintMsg(msgId, args);
    va_end(args);
}

Now we need to support an API to print a group of messages:

typedef struct
{
    long id;
    char[256] args;
} msgData;
void printGroupMsg(msgData * msgArr, int msgCount) //this is the new API that requires multiple va_list
{
    //do some other stuff to start group messaging
    for (int i = 0; i < msgCount; ++i)
        internalAPI_PrintMsg(msgArr[i].id, msgArr[i].args);
    //do some other stuff to end group messaging
}

void initMsgData(msgData *msg, int msgId, ...) //this is equivalent to toVaList(...)
{
    msg->id = msgId;
    va_list args;
    va_start(args, msgId);
    memcpy(msg->args, args, 256);
    va_end(args);
}

//client code:
msgData *arr = malloc(sizeof(msgData)*2);
initMsgData(arr, 1, 88, "test");
initMsgData(arr+1, 2, 123.456);
printGroupMsg(arr, 2);

Upvotes: 2

Views: 383

Answers (2)

chqrlie
chqrlie

Reputation: 144780

va_copy must be used in place of *out = args; to makes a copy of the va_list but it does not change the scope where it is valid to enumerate it, which is the called function. Referring to the va_list after the function toVaList returns has undefined behavior.

This restriction is not specified explicitly in the C Standard. The relevent language is this:

7.16.1 Variable argument list access macros
...
Each invocation of the va_start and va_copy macros shall be matched by a corresponding invocation of the va_end macro in the same function.

This phrase means that va_end() must be called in the same function that called va_start or va_copy for the same va_list argument, hence this argument cannot be used once the function returns.

Your approach has undefined behavior per the C Standard. This is easy to understand from an implementation standpoint: calling a function with a variable number of arguments involves passing these arguments to the function in an implementation defined way that the variable argument accessing macros abstracts. Yet like any other function call, the arguments go out of scope when the function returns to its caller. If you want to access them at some later point, you must enumerate them and save their values to an array or structure with an appropriate scope and life time. The memcpy hack attempts to do this, but its behavior is undefined.

Here is an alternative approach:

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

int **array_from_args(int count, ...) {
    va_list ap;
    va_start(ap, count);
    int *a = malloc(sizeof(*a) * (1 + count));
    if (a) {
        a[0] = count;
        for (int i = 0; i < count; i++) {
            a[i + 1] = va_arg(ap, int);
        }
    }
    va_end(ap);
    return a;
}

void print_array(const char *s, const int *a) {
    printf("%s:", s);
    if (a) {
        for (int i = 0, n = a[0]; i < n; i++)
            printf(" %d", a[i + 1]);
    }
    printf("\n");
}

int main() {
    int *args1 = array_from_args(2, 11, 22);
    int *args2 = array_from_args(2, 33, 44);
    int *args3 = array_from_args(2, 55, 66);
    print_array("args1", args1);
    print_array("args2", args2);
    print_array("args3", args3);
    free(args1);
    free(args2);
    free(args3);
}

You could generalize this approach for arguments of different types by providing type information as an argument to the array_from_args(), like a printf format string, and construct an array of tagged structures.

Upvotes: 1

Jonathan Leffler
Jonathan Leffler

Reputation: 754060

Note that the C standard stipulates that if you invoke va_start in function, you must also invoke va_end in the same function:

§7.16.1 Variable argument list access macros

¶1 The va_start and va_arg macros described in this subclause shall be implemented as macros, not functions. It is unspecified whether va_copy and va_end are macros or identifiers declared with external linkage. If a macro definition is suppressed in order to access an actual function, or a program defines an external identifier with the same name, the behavior is undefined. Each invocation of the va_start and va_copy macros shall be matched by a corresponding invocation of the va_end macro in the same function.

[…Emphasis added…]

Your toVaList function is violating that requirement and therefore invokes undefined behaviour.

The preamble to §7.16 says:

3 The type declared is

    va_list

which is a complete object type suitable for holding information needed by the macros va_start, va_arg, va_end, and va_copy. 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.253)

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

Upvotes: 4

Related Questions