16 December 2010

snprintf + realloc = snprintf_realloc

Recently, in C99, I needed to format some arbitrary-length string content using snprintf. I could re-use the same buffer because I only needed one result at a time. I wanted to (hopefully) minimize the number of malloc calls necessary. Finally, I wanted to (mostly) maintain snprintf's interface including its error semantics.

My use case looked like this...

    char *buf = NULL;
    size_t len = 0;
    while (/* more work */) {
        if (0 > snprintf_realloc(&buf, &len, /*format specifier*/, ...)) {
            /* error occurred, report it, break */
        }
        /* use buf to perform work */
    }
    if (buf) free(buf);

Here is the snprintf_realloc method I created including the relevant Doxygen and FCTX-based unit tests. The source below should build and run provided that fct.h is also in the same directory:

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

#include "fct.h"

/**
 * Call snprintf(*str, *size, format, ...) and reallocate the buffer
 * pointed to by *str as appropriate to contain the entire result.  On
 * exit, *str and *size will contain a pointer to the
 * realloced buffer and its maximum usable size, respectively.
 *
 * The reallocation scheme attempts to reduce the reallocation calls when the
 * same str and size arguments are used repeatedly.  It is
 * valid to pass *str == NULL and *size == 0 and then have
 * the buffer allocated to perfectly fit the result.
 *
 * @param[in,out] str    Pointer to the buffer in which to write the result.
 * @param[in,out] size   Pointer to the initial buffer size.
 * @param[in]     format Format specifier to use in sprintf call.
 * @param[in]     ...    Variable number of arguments corresponding
 *                       to \c format.
 *
 * @return On success, the number of characters (not including the trailing
 *         '\0') written to *str.  On error, a negative value
 *         is returned, *str is freed and *size
 *         is set to zero.
 */
int snprintf_realloc(char **str, size_t *size, const char *format, ...);

int
snprintf_realloc(char **str, size_t *size, const char *format, ...)
{
    int retval, needed;
    va_list ap;
    va_start(ap, format);
    while (   0     <= (retval = vsnprintf(*str, *size, format, ap)) // Error?
           && *size <  (needed = retval + 1)) {                      // Space?
        va_end(ap);
        *size *= 2;                                                  // Space?
        if (*size < needed) *size = needed;                          // Space!
        char *p = realloc(*str, *size);                              // Alloc
        if (p) {
            *str = p;
        } else {
            free(*str);
            *str  = NULL;
            *size = 0;
            return -1;
        }
        va_start(ap, format);
    }
    va_end(ap);
    return retval;
}

FCT_BGN()
{
    FCT_FIXTURE_SUITE_BGN(snprintf_realloc)
    {
        const char s[] = "1234";
        char *ptr;
        size_t size;

        FCT_SETUP_BGN()
        {
            ptr  = NULL;
            size = 0;
        }
        FCT_SETUP_END();

        FCT_TEARDOWN_BGN()
        {
            if (ptr) free(ptr);
        }
        FCT_TEARDOWN_END();

        FCT_TEST_BGN(snprintf_realloc)
        {
            // Initial should cause malloc-like behavior
            fct_chk_eq_int(4, snprintf_realloc(&ptr, &size, "%s", s));
            fct_chk(ptr);
            fct_chk_eq_int(size, 5);
            fct_chk_eq_str(ptr, s);

            // Repeated size should not cause any new buffer allocation
            {
                char *last_ptr = ptr;
                fct_chk_eq_int(4, snprintf_realloc(&ptr, &size, "%s", s));
                fct_chk(last_ptr == ptr);
                fct_chk_eq_int(size, 5);
                fct_chk_eq_str(ptr, s);
            }

            // Request requiring more than twice the space should
            // realloc memory to fit exactly.
            fct_chk_eq_int(12, snprintf_realloc(&ptr, &size, "%s%s%s",
                                                s, s, s));
            fct_chk_eq_int(size, 13);
            fct_chk_eq_str(ptr, "123412341234");

            // Request requiring less than twice the space should
            // cause a doubling of the buffer size.
            fct_chk_eq_int(16, snprintf_realloc(&ptr, &size, "%s%s%s%s",
                                                s, s, s, s));
            fct_chk_eq_int(size, 26);
            fct_chk_eq_str(ptr, "1234123412341234");
        }
        FCT_TEST_END();
    }
    FCT_FIXTURE_SUITE_END();
}
FCT_END()

Valgrind, after suppressing some unrelated FCTX warnings, gives this test a clean bill of health. If anyone's interested, aside from passing NULL pointers in or relying upon undefined snprintf behavior, I'd love to hear of ways to break this particular snippet.

No comments:

Subscribe Subscribe to The Return of Agent Zlerich