8

I have some code that converts variadic parameters into a va_list, then passes the list on to a function that then calls vsnprintf. This works fine on Windows and OS X, but it is failing with odd results on Linux.

In the following code sample:

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

char *myPrintfInner(const char *message, va_list params)
{
    va_list *original = &params;
    size_t length = vsnprintf(NULL, 0, message, *original);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

char *myPrintf(const char *message, ...)
{
    va_list va_args;
    va_start(va_args, message);

    size_t length = vsnprintf(NULL, 0, message, va_args);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, va_args);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    va_end(va_args);

    return final;
}

int main(int argc, char **argv)
{
    char *test = myPrintf("This is a %s.", "test");
    char *actual = "This is a test.";
    int result = strcmp(test, actual);

    if (result != 0)
    {
        printf("%d: Test failure!\r\n", result);
    }
    else
    {
        printf("Test succeeded.\r\n");
    }

    return 0;
}

The output of second vsnprintf call is 17, and the result of strcmp is 31; but I don't get why vsnprintf would return 17 seeing as This is a test. is 15 characters, add the NULL and you get 16.

Related threads that I've seen but do not address the topic:


With @Mat's answer (I am reusing the va_list object, which is not allowed), this comes squarely around to the first related thread I linked to. So I attempted this code instead:

char *myPrintfInner(const char *message, va_list params)
{
    va_list *original = &params;
    size_t length = vsnprintf(NULL, 0, message, params);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, *original);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

Which, per the C99 spec (footnote in Section 7.15), should work:

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.

But my compiler (gcc 4.4.5 in C99 mode) gives me this error regarding the first line of myPrintfInner:

test.c: In function ‘myPrintfInner’: 
test.c:8: warning: initialization from incompatible pointer type

And the resulting binary produces the exact same effect as the first time around.


Found this: Is GCC mishandling a pointer to a va_list passed to a function?

The suggested workaround (which wasn't guaranteed to work, but did in practice) is to use arg_copy first:

char *myPrintfInner(const char *message, va_list params)
{
    va_list args_copy;
    va_copy(args_copy, params);

    size_t length = vsnprintf(NULL, 0, message, params);
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, args_copy);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}
Community
  • 1
  • 1
Mahmoud Al-Qudsi
  • 28,357
  • 12
  • 85
  • 125
  • Your `myPrintf` function is missing a `return` statement. I would've expected your compiler to warn you about that. – Ilmari Karonen Mar 30 '12 at 05:42
  • 1
    bah, humbug! Copy and paste failure. – Mahmoud Al-Qudsi Mar 30 '12 at 05:43
  • 1
    Your new code is doing exactly the same thing as the old: `original` points to `params`, so passing `*original` is exactly the same as passing `params`. Your real problem seems to be that you don't understand how `va_list`s work: they're essentially pointers to the argument stack, and the pointer gets incremented as it's used. So when you use the same `va_list` twice, the second time you're incrementing the pointer past the end of the argument list. – Ilmari Karonen Mar 30 '12 at 06:15

2 Answers2

13

As Mat notes, the problem is that you're reusing the va_list. If you don't want to restructure your code as he suggests, you can use the C99 va_copy() macro, like this:

char *myPrintfInner(const char *message, va_list params)
{
    va_list copy;

    va_copy(copy, params);
    size_t length = vsnprintf(NULL, 0, message, copy);
    va_end(copy);

    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

On compilers that don't support C99, you may be able use __va_copy() instead or define your own va_copy() implementation (which will be non-portable, but you can always use compiler / platform sniffing in a header file if you really need to). But really, it's been 13 years — any decent compiler should support C99 these days, at least if you give it the right options (-std=c99 for GCC).

Community
  • 1
  • 1
Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
  • I'm using GCC w/ C99, but if you'll see my revised question, I have already tried this and the results are not good. Seems to be broken on x86_64. – Mahmoud Al-Qudsi Mar 30 '12 at 06:11
  • 1
    That's odd. I just tried the exact code I gave above on x86_64 Linux and it works for me. – Ilmari Karonen Mar 30 '12 at 06:20
  • Here's my exact experience: http://pastebin.ca/2133787 - I don't think I did any different than you. What version of gcc are you using? I updated my post with that info. – Mahmoud Al-Qudsi Mar 30 '12 at 06:26
  • You have some crud in your `myPrintf` function, presumably left over from earlier attempts. In fact, you're not calling `myPrintfInner`at all, which is probably why fixing it doesn't help. (Tip: When experimenting with code, _always_ keep a pre-experimentation copy around. Version control systems are good for this, but even a plain old renamed backup copy will work. And learn to use `diff`.) – Ilmari Karonen Mar 30 '12 at 06:31
  • Well, that's embarrassing. I guess I should treat scraps of code with a bit more respect. – Mahmoud Al-Qudsi Mar 30 '12 at 06:36
7

The problem is that (apart from the missing return statement) you're re-using the va_list parameter without resetting it. That's not good.

Try something like:

size_t myPrintfInnerLen(const char *message, va_list params)
{
    return vsnprintf(NULL, 0, message, params);
}

char *myPrintfInner(size_t length, const char *message, va_list params)
{
    char *final = (char *) malloc((length + 1) * sizeof(char));
    int result = vsnprintf(final, length + 1, message, params);

    printf("vsnprintf result: %d\r\n", result);
    printf("%s\r\n", final);

    return final;
}

char *myPrintf(const char *message, ...)
{
    va_list va_args;
    va_start(va_args, message);
    size_t length = myPrintfInnerLen(message, va_args);
    va_end(va_args);
    va_start(va_args, message);
    char *ret = myPrintfInner(length, message, va_args);
    va_end(va_args);
    return ret;
}

(And turn on your compiler's warnings.)

I don't think the footnote you point to means what you think it does. I read it as: if you pass a va_list directly (as a value, not a pointer), the only thing you can do in the caller is va_end it. But if you pass it as a pointer, you could, say, call va_arg in the caller if the callee didn't "consume" all the va_list.

You could try with va_copy though. Something like:

char *myPrintfInner(const char *message, va_list params)
{
    va_list temp;
    va_copy(temp, params);
    size_t length = vsnprintf(NULL, 0, message, temp);
    ...
Mat
  • 202,337
  • 40
  • 393
  • 406