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.