9

I am using a va_list to construct a string that is rendered.

void Text2D::SetText(const char *szText, ...)

This is all fine and good, but now the user has the ability to change the language while the application is running. I need to regenerate all the text strings and re-cache the text bitmaps after initialization. I would like to store the va_list and use it whenever the text needs to be generated.

To give you some more background, this needs to happen in the case where the key string that I'm translating has a dynamic piece of data in it.

"Player Score:%d"

That is the key string I need to translate. I would like to hold the number(s) provided in the va_list for later use (outside the scope of the function that initializes the text) in the case that it needs to be re-translated after initialization. Preferably I would like to hold a copy of the va_list for use with vsnprintf.

I've done some research into doing this and have found a few ways. Some of which I question whether it is a appropriate method (in terms of being stable and portable).

fish2000
  • 4,289
  • 2
  • 37
  • 76
resolveaswontfix
  • 1,115
  • 2
  • 10
  • 11
  • 3
    Could you provide a better description of what you mean by "later"? I think it should be pretty clear to anyone that 'va_list' can only be valid as long as the corresponding variadic function invocation is still active (i.e. from the low-level, implementation-dependent point of view, as long as the corresponding stack frame with parameters is alive). Any attempts to access it after the function has returned is a recipe for disaster. – AnT stands with Russia Oct 13 '09 at 21:45
  • 2
    use Boost.Format instead to build the string. There's no reason to blow up type safety if you can avoid it. – jalf Oct 14 '09 at 19:08
  • Yeah we are going to end up doing something similar to this, because this method (the one in the question) is just plain flawed. – resolveaswontfix Oct 14 '09 at 19:50
  • I wouldn't write "flawed." I would write "implementation dependent." ;) I wish somebody would try the Mac compiler with the code I posted. I'm curious. – Heath Hunnicutt Oct 15 '09 at 03:07

5 Answers5

11

This question has really piqued my interest. Also I will be facing a similar problem in my own work, so the solution devised here may help me, too.

In short, I wrote proof-of-concept code which caches variable arguments for later use -- you can find it below.

I was able to get the below code to work correctly on both Windows and intel-based Linux. I compiled with gcc on Linux and MSVC on Windows. There is a twice-repeated warning about abusing va_start() from gcc -- which warning you could disable in your makefile.

I'd love to know if this code works on a Mac compiler. It might take a little tweaking to get it to compile.

I realize this code is:

  • Extreme in its abuse of va_start() as defined by the ANSI C standard.
  • Old-school byte-oriented C.
  • Theoretically non-portable in its use of the va_list variable as a pointer.

My use of malloc() and free() was very deliberate, as va_list macros are from the C standard and are not C++ features. I realize your question title mentions C++, but I have attempted to produce a fully C-compatible solution, other than using some C++-style comments.

This code also no doubt has some bugs or non-portabilities in the format string processing. I provide this as a proof of concept which I hacked together in two hours, not a finished code sample ready for professional use.

That disclaimer said, I hope you find the result as delightful as I did! This was a lovely question to hack around in. The sick and twisted nature of the result gives me a deep belly-laugh. ;)

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

#define VERBOSE 0

#ifdef WINDOWS
#define strdup _strdup
#endif

/*
 * struct cached_printf_args
 *
 * This is used as the pointer type of the dynamically allocated
 * memory which holds a copy of variable arguments.  The struct
 * begins with a const char * which recieves a copy of the printf()
 * format string.
 *
 * The purpose of ending a struct with a zero-length array is to
 * allow the array name to be a symbol to the data which follows
 * that struct.  In this case, additional memory will always be
 * allocted to actually contain the variable args, and cached_printf_args->args
 * will name the start address of that additional buffer space.
 *
 */
struct cached_printf_args
{
    const char * fmt;
    char  args[0];
};


/*
 * copy_va_args -- Accepts a printf() format string and va_list
 *                 arguments.
 *
 *                 Advances the va_list pointer in *p_arg_src in
 *                 accord with the specification in the format string.
 *
 *                 If arg_dest provided is not NULL, each argument
 *                 is copied from *p_arg_src to arg_dest according
 *                 to the format string.
 *
 */
int copy_va_args(const char * fmt, va_list * p_arg_src, va_list arg_dest)
{
    const char * pch = fmt;

    int processing_format = 0;

    while (*pch)
    {
        if (processing_format)
        {
            switch (*pch)
            {
            //case '!': Could be legal in some implementations such as FormatMessage()
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '.':
            case '-':

                // All the above characters are legal between the % and the type-specifier.
                // As the have no effect for caching the arguments, here they are simply
                // ignored.
                break;

            case 'l':
            case 'I':
            case 'h':
                printf("Size prefixes not supported yet.\n");
                exit(1);

            case 'c':
            case 'C':
                // the char was promoted to int when passed through '...'
            case 'x':
            case 'X':
            case 'd':
            case 'i':
            case 'o':
            case 'u':
                if (arg_dest)
                {
                     *((int *)arg_dest) = va_arg(*p_arg_src, int);
                     va_arg(arg_dest, int);
                }
                else
                    va_arg(*p_arg_src, int);
#if VERBOSE
                printf("va_arg(int), ap = %08X, &fmt = %08X\n", *p_arg_src, &fmt);
#endif
                processing_format = 0;
                break;

            case 's':
            case 'S':
            case 'n':
            case 'p':
                if (arg_dest)
                {
                    *((char **)arg_dest) = va_arg(*p_arg_src, char *);
                    va_arg(arg_dest, char *);
                }
                else
                    va_arg(*p_arg_src, char *);
#if VERBOSE
                printf("va_arg(char *), ap = %08X, &fmt = %08X\n", *p_arg_src, &fmt);
#endif
                processing_format = 0;
                break;

            case 'e':
            case 'E':
            case 'f':
            case 'F':
            case 'g':
            case 'G':
            case 'a':
            case 'A':
                if (arg_dest)
                {
                    *((double *)arg_dest) = va_arg(*p_arg_src, double);
                    va_arg(arg_dest, double);
                }
                else
                    va_arg(*p_arg_src, double);
#if VERBOSE
                printf("va_arg(double), ap = %08X, &fmt = %08X\n", *p_arg_src, &fmt);
#endif
                processing_format = 0;
                break;
            }
        }
        else if ('%' == *pch)
        {
            if (*(pch+1) == '%')
                pch ++;
            else
                processing_format = 1;
        }
        pch ++;
    }

    return 0;
}

/*
 * printf_later -- Accepts a printf() format string and variable
 *                 arguments.
 *
 *                 Returns NULL or a pointer to a struct which can
 *                 later be used with va_XXX() macros to retrieve
 *                 the cached arguments.
 *
 *                 Caller must free() the returned struct as well as
 *                 the fmt member within it.
 *
 */
struct cached_printf_args * printf_later(const char *fmt, ...)
{
    struct cached_printf_args * cache;
    va_list ap;
    va_list ap_dest;
    char * buf_begin, *buf_end;
    int buf_len;

    va_start(ap, fmt);
#if VERBOSE 
    printf("va_start, ap = %08X, &fmt = %08X\n", ap, &fmt);
#endif

    buf_begin = (char *)ap;

    // Make the 'copy' call with NULL destination.  This advances
    // the source point and allows us to calculate the required
    // cache buffer size.
    copy_va_args(fmt, &ap, NULL);

    buf_end = (char *)ap;

    va_end(ap);

    // Calculate the bytes required just for the arguments:
    buf_len = buf_end - buf_begin;

    if (buf_len)
    {
        // Add in the "header" bytes which will be used to fake
        // up the last non-variable argument.  A pointer to a
        // copy of the format string is needed anyway because
        // unpacking the arguments later requires that we remember
        // what type they are.
        buf_len += sizeof(struct cached_printf_args);

        cache = malloc(buf_len);
        if (cache)
        {
            memset(cache, 0, buf_len);
            va_start(ap, fmt);
            va_start(ap_dest, cache->fmt);

            // Actually copy the arguments from our stack to the buffer
            copy_va_args(fmt, &ap, ap_dest);

            va_end(ap);
            va_end(ap_dest);

            // Allocate a copy of the format string
            cache->fmt = strdup(fmt);

            // If failed to allocate the string, reverse allocations and
            // pointers
            if (!cache->fmt)
            {
                free(cache);
                cache = NULL;
            }
        }
    }

    return cache;
}

/*
 * free_printf_cache - frees the cache and any dynamic members
 *
 */
void free_printf_cache(struct cached_printf_args * cache)
{
    if (cache)
        free((char *)cache->fmt);
    free(cache);
}

/*
 * print_from_cache -- calls vprintf() with arguments stored in the
 *                     allocated argument cache
 *
 *
 * In order to compile on gcc, this function must be declared to
 * accept variable arguments.  Otherwise, use of the va_start()
 * macro is not allowed.  If additional arguments are passed to
 * this function, they will not be read.
 */
int print_from_cache(struct cached_printf_args * cache, ...)
{
    va_list arg;

    va_start(arg, cache->fmt);
    vprintf(cache->fmt, arg);
    va_end(arg);
}

int main(int argc, char *argv)
{
    struct cached_printf_args * cache;

    // Allocates a cache of the variable arguments and copy of the format string.
    cache = printf_later("All %d of these arguments will be %s fo%c later use, perhaps in %g seconds.", 10, "stored", 'r', 2.2);

    // Demonstrate the time-line with some commentary to the output.
    printf("This statement intervenes between creation of the cache and its journey to the display.\n"

    // THIS is the call which actually displays the output from the cached printf.
    print_from_cache(cache);

    // Don't forget to return dynamic memory to the free store
    free_printf_cache(cache);

    return 0;

}
Heath Hunnicutt
  • 18,667
  • 3
  • 39
  • 62
  • I think it's a long and complicated one for simple tasks. I don't know exactly what your snippet serves, it could do and maintain other features but I've done a simple allocating/storing for agrs values. ` ARGS *f2_args = (ARGS*) malloc(1*sizeof(int8_t)+1*sizeof(uint8_t)+ 1*sizeof(int)+1*sizeof(const char*)); // args initialization *f2_args = (ARGS){(int8_t)-10,(uint8_t)9,(int)90000,(const char*)p0};` What you think ? – R1S8K Aug 15 '21 at 10:38
8

Storing the va_list itself is not a great idea; the standard only requires that the va_list argument work with va_start(), va_arg() and va_end(). As far as I can tell, the va_list is not guaranteed to be copy constructable.

But you don't need to store the va_list. Copy the supplied arguments into another data structure, such as a vector (of void*, probably), and retrieve them later in the usual way. You'll need to be careful about the types, but that's always the case for printf-style functions in C++.

David Seiler
  • 9,557
  • 2
  • 31
  • 39
  • I've been wondering about the copy constructible/assignable qualities of va_list. I can't seem to find any discussion of it in the standard. – D.Shawley Oct 13 '09 at 21:35
  • That would be because it is implementation specific I guess. I know of some CPUs that store the first few arguments in registers instead of a stack, which would make copying the va_list a bit trickier. Also, va_list does not know how many arguments there are. You really need a structure that has quantity information, simple random access and well defined copy semantics. Some languages may require the arguments in a different order! – Skizz Oct 13 '09 at 21:47
3

You can use va_copy(), here is an example:

va_list ap;
va_list tmp;
va_copy(tmp, ap);
//do something with tmp
va_end(tmp);
quantum
  • 3,672
  • 29
  • 51
PawMarc
  • 39
  • 1
  • 5
    The keyword in the question was "later use". After the function returns, both tmp and ap variables can not be used. – kaspersky Jul 29 '13 at 13:00
2

What you describe about "holding the number(s) provided in the va_list" is the way to approach this.

The va_list maintains pointers to temporary memory on the stack (so-called "automatic storage" in the C standard). After the function with variable args has returned, this automatic storage is gone and the contents are no longer usable. Because of this, you cannot simply keep a copy of the va_list itself -- the memory it references will contain unpredictable content.

In the example you give, you will need to store two integers that are re-used when re-creating that message. Depending on how many different format strings you have to deal with, your approach might vary.

For a completely general type of approach, you will need to:

  • Write a "cache_arguments()" function which creates a dynamic-memory buffer of the values found in variable arguments.
  • This cache_arguments() would use the printf()-style format string, along with the va_start, va_arg, va_end macros. You will need to retrieve the types according to the printf() type-specifiers, because sizeof(double) != sizeof(int).
  • Store the arguments in your memory cache with the same alignment and padding expected by va_arg() on your platform. (Read your varargs.h file.)
  • Get your calls to vsnprintf() working with this cached memory buffer instead of a pointer created by va_start().

The above items are all possible on most platforms, including Linux and Windows.

An item you may wish to consider about translation is the word-order concern. What is written in English as:

Player Sam has scored 20 points.

Could in some (human) languages only be written fluently with a word order analagous to:

20 points have been scored by Player Sam.

For this reason, the Win32 FormatMessage() API uses a printf()-like format string, with the functional difference that parameters are numbered, as in:

Player %1 has scored %2!d! points.
%2!d! points have been scored by player %1.

(The string type is assumed for each argument, so %1 is equivalent to %1!s!)

Of course you may not be using the Win32 API, but the functionality of altering the word order of the formatted arguments is what I am attempting to introduce as a concept. You may want to implement this in your software, also.

Heath Hunnicutt
  • 18,667
  • 3
  • 39
  • 62
  • You bring up a completely valid point at the end there, I should have simply provided an example with only one % in the source string. So is the format of a va_list standard once you start it so that I could send in a buffer I created (via cache_arguments) into vsprintf. Just want to make sure I am understanding you correctly. This needs to work on Windows, Linux and Mac. If not I will just have to make my own printf that takes a vector of values I store as opposed to a ... . – resolveaswontfix Oct 14 '09 at 04:36
  • The implementation of va_list is not standard, but the value returned from va_arg() is. To size the memory required, make a "trial run" through the arguments without actually reading their values. Use va_arg() to return their addresses. You will need to call va_arg() one extra time to retrieve an imaginary final char. By subtracting the final return value of va_arg() from the initial, you can obtain the number of bytes required. Allocate that number of bytes + sizeof(int). Make a second pass through the arguments, now compying them to the allocated buffer -- begin at byte _sizeof(int)_ – Heath Hunnicutt Oct 14 '09 at 05:30
  • Ah, yes... due to the way va_start/va_arg need to be implemented (returning the contents of a pointer), it will be legal to use &va_arg(ap) on all platforms -- which is required to get the parameter addresses. – Heath Hunnicutt Oct 14 '09 at 05:34
0

The way to do it in C is to send a struct of arguments to the function instead. You should pass the struct by reference and then copy (memcpy) the struct to a common location which will allow you to reuse it later. You unravel the struct at the destination in the same manner you sent it. You keep the template of the struct for 'setting and getting'.