views:

147

answers:

5

Hi folks,

I wrote a variadic C function which mission is to allocate the needed memory for a buffer, and then sprintf the args given to this function in that buffer. But I'm seeing a strange behavior with it. It works only once. If I have two calls for this function it segfaults.

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

char *xsprintf(char * fmt, ...)
{
    va_list ap;
    char *part;
    char *buf;
    size_t len = strlen(fmt)+1;

    va_start(ap, fmt);
    while (part = va_arg(ap, char *))
        len += strlen(part);
    va_end(ap);

    buf = (char*) malloc(sizeof(char)*len);

    va_start(ap, fmt);
    vsprintf(buf, fmt, ap);
    va_end(ap);

    return buf;
}

int main(int argc, const char *argv[])
{
    char *b;
    b = xsprintf("my favorite fruits are: %s, %s, and %s", "coffee", "C", "oranges");
    printf("size de buf is %d\n", strlen(b)); //this works. After it, it segfaults.
    /*
    free(b);
    b = NULL;
    */
    b = xsprintf("my favorite fruits are: %s, %s, and %s", "coffee", "C", "oranges");
    printf("size de buf is %d\n", strlen(b));
    printf("%s", b);
    return 0;
}

here's the output of this program:

size de buf is 46
[1]    4305 segmentation fault  ./xsprintftest

Am I doing something wrong? Shouldn't I have used va_start multiple times in a single function? Do you have any alternatives? Thanks a lot! :)

+1  A: 

First off, try using vsnprintf. It's just a good idea.

That's not your problem, though. Your problem is that you can't call va_arg more times than there are arguments. It doesn't return the number of arguments. You must either pass in a parameter telling it how many there are, or extract the number of special tokens in the format string to figure out how many there must implicitly be.

That's the reason why printf can choke your program if you pass it too few arguments; it will just keep pulling things off the stack.

Borealid
Aha! So the while (Not NULL) isn't a good idea? I should know before iterating how many are there? Thanks, I'll try that! (OTOH, I tried vsnprintf and it's not working (well by replacing the vsprintf call with a call to vsnprintf(buf, len, ap))
sandra
Oh maybe, I should count the % in *fmt and use that number in my loop?!
sandra
@sandra -- that won't work in the general case. You could have a %% (escaped so that a % shows up in the output) in `*fmt` and you would consume too many args.
bstpierre
Also keep in mind that `vsnprintf` behavior isn't 100% portable. (Even though C99 provides a standard, not all implementations are conforming.) See, for example, http://perfec.to/vsnprintf/
bstpierre
+3  A: 

Pass NULL as the last arg to xsprintf():

b = xsprintf("my favorite fruits are: %s, %s, and %s",
             "coffee", "C", "oranges", (void*)0);

Then your while() loop will see the NULL and terminate properly.

As R.. mentions in the comment below and in another answer, the xsprintf function will fail if there are other format arguments. You are better off using vsprintf as explained in the other answer.

My intent here was simply to demonstrate the use of a sentinel with va_arg.

bstpierre
LOVELY! :) Thanks! (Edit: I should remember to add NULL in every call I make though... Yet it's a nice solution! Thanks again!)
sandra
This will still crash with any non-trivial format strings (anything other than just literal text and `%s` specifiers).
R..
@R.. - yes, my intent was to address the problem with the variadic args. Will edit to clarify.
bstpierre
Since `NULL` can be a macro simply defined as `0`, technically you need to cast it to `(char *)` or `(void *)` when passing it as the sentinel to this function (otherwise it might be passed as an `int`).
caf
Ah, good catch.
bstpierre
+5  A: 

You should be using vsnprintf. Use it twice. Once with a NULL destination/zero size to find out the length of the buffer you need to allocate, then a second time to fill the buffer. That way your function will work even if all the arguments are not strings.

As written, it will fail if there are any non-string arguments (%d, %x, %f, etc.). And counting the number of % characters is not a valid way to get the number of arguments. Your result could be too many (if there are literal % characters encoded as %%) or too few (if arguments are also needed for %*s, %.*d, etc. width/precision specifiers).

R..
I prefer to use an allocated buffer of a predefined size in the first `vsnprintf` and check if it overflows, in which case I `realloc` to the real size and redo the `vsnprintf`. This avoids in most cases the double call.
tristopia
Indeed, this is the approach I actually take in my implementation of the GNU `vasprintf` function. I just omitted it from my answer for simplicity's sake.
R..
+2  A: 

The problem is that in the bit of code where you're accessing the va_arg() list without a particular defined end:

va_start(ap, fmt);
while (part = va_arg(ap, char *))
    len += strlen(part);
va_end(ap);

The stdargs.h facilities don't have any built-in method for determining when the end of the va_list() occurs - you need to have that explicitly done by a convention you come up with. Either using a sentinel value (as in bstpierre's answer), or by having a count provided. A count can be an explicit parameter that's provided, or it can be implicit (such as by counting the number of format specifiers in the format string like the printf() family does).

Of course, you also have the issue that your code currently only supports the one kind of format-specifier (%s), but I assumed that that's intentional at this point.

Michael Burr
A: 

Hi guys,

Thanks a lot for your answers and ideas! So I rewrote my function like this:

void fatal(const char *msg)/*{{{*/
{
  fprintf(stderr, "program: %s", msg);
  abort ();
}/*}}}*/

void *xmalloc(size_t size)/*{{{*/
{
  register void *value = malloc(size);
  if (value == 0)
    fatal ("Virtual memory exhausted");
  return value;
}/*}}}*/

void *xrealloc(void *ptr, size_t size)/*{{{*/
{
  register void *value = realloc(ptr, size);
  if (value == 0)
    fatal ("Virtual memory exhausted");
  return value;
}/*}}}*/

char *xsprintf(const char *fmt, ...)/*{{{*/
{
    /* Heavily inspired from http://perfec.to/vsprintf/pasprintf */
    va_list args;
    char *buf;
    size_t bufsize;
    char *newbuf;
    size_t nextsize;
    int outsize;
    int FIRSTSIZE = 20;

    bufsize = 0;

    for (;;) {
        if(bufsize == 0){
            buf = (char*)  xmalloc(FIRSTSIZE);
            bufsize = FIRSTSIZE;
        }
        else{
            newbuf = (char *)xrealloc(buf, nextsize);
            buf = newbuf;
            bufsize = nextsize;
        }

        va_start(args, fmt);
        outsize = vsnprintf(buf, bufsize, fmt, args);
        va_end(args);

        if (outsize == -1) {
            /* Clear indication that output was truncated, but no
             * clear indication of how big buffer needs to be, so
             * simply double existing buffer size for next time.
             */
            nextsize = bufsize * 2;

        } else if (outsize == bufsize) {
            /* Output was truncated (since at least the \0 could
             * not fit), but no indication of how big the buffer
             * needs to be, so just double existing buffer size
             * for next time.
             */
            nextsize = bufsize * 2;

        } else if (outsize > bufsize) {
            /* Output was truncated, but we were told exactly how
             * big the buffer needs to be next time. Add two chars
             * to the returned size. One for the \0, and one to
             * prevent ambiguity in the next case below.
             */
            nextsize = outsize + 2;

        } else if (outsize == bufsize - 1) {
            /* This is ambiguous. May mean that the output string
             * exactly fits, but on some systems the output string
             * may have been trucated. We can't tell.
             * Just double the buffer size for next time.
             */
            nextsize = bufsize * 2;

        } else {
            /* Output was not truncated */
            break;
        }
    }
    return buf;
}/*}}}*/

And it's working like a charm! Thanks a million times :)

sandra