How to Create a Growable C Char Array with mulle-buffer
Use mulle_buffer_create() for heap-allocated or MULLE_BUFFER_DATA() for stack-allocated flexible buffers, then append data with mulle_buffer_add_string() or mulle_buffer_add_bytes() to build a dynamically resizing char array.
The mulle-buffer library from the mulle-c organization provides a lightweight, portable solution for dynamic string and byte array construction in C. Unlike fixed-size arrays, a growable C char array created with mulle-buffer expands automatically as you append data, eliminating manual reallocations and buffer overflow risks.
Heap-Allocated vs. Stack-Allocated Buffers
Mulle-buffer supports two distinct allocation strategies for creating growable buffers. Both approaches implement the same underlying flexible storage mechanism found in src/mulle-buffer.c, but differ in where the buffer structure itself resides.
Creating a Heap-Allocated Buffer
For dynamic lifetimes or returning buffers from functions, allocate the buffer structure on the heap using mulle_buffer_create(). This function, defined in src/mulle-buffer.c at lines 44-51, returns a pointer to a fully initialized struct mulle_buffer configured with the default capacity (MULLE_BUFFER_DEFAULT_CAPACITY).
The underlying storage grows automatically when you invoke addition functions.
Creating a Stack-Allocated Buffer
For local, temporary growable arrays, declare the buffer on the stack and initialize it with the MULLE_BUFFER_DATA() macro. Defined in src/mulle-buffer.h at lines 117-125, this macro sets the _type flag to MULLE_BUFFER_IS_FLEXIBLE, ensuring the buffer reallocates its internal storage on demand while the struct itself remains stack-allocated.
Core Operations for Dynamic Growth
Once initialized, the buffer provides atomic operations that handle reallocation transparently. According to the implementation in src/mulle-buffer.c, these functions call mulle_buffer_grow() internally when capacity is exceeded.
Appending Data
mulle_buffer_add_byte(buf, c)– Appends a single character and triggers growth if the buffer is full.mulle_buffer_add_string(buf, "text")– Copies a C-string excluding the null terminator.mulle_buffer_add_bytes(buf, data, length)– Appends arbitrary binary data of specified length.
Ensuring Capacity
When writing directly into the buffer's internal storage, use mulle_buffer_guarantee(buf, n). This function grows the buffer if necessary to ensure n free bytes are available, returning a pointer to the writable area. After writing, call mulle_buffer_advance(buf, written) to update the cursor position.
For manual pre-allocation to avoid repeated reallocations during large operations, call mulle_buffer_grow(buf, capacity) before adding data.
Extracting Content
Retrieve the current content using:
mulle_buffer_get_bytes(buf)– Returns a raw pointer to the internal storage (not null-terminated).mulle_buffer_get_length(buf)– Returns the number of bytes currently stored.mulle_buffer_extract_string(buf)– Allocates a new null-terminated C string copy that you must free.
Practical Code Examples
The following examples demonstrate how to create and manipulate growable char arrays using both allocation strategies.
Example 1: Heap-Allocated Growable String
This example creates a flexible buffer on the heap, appends mixed text and binary data, then extracts the result as a C string.
#include "mulle-buffer.h"
int main(void)
{
/* allocate a flexible buffer (default allocator) */
struct mulle_buffer *buf = mulle_buffer_create( NULL );
/* Append some text */
mulle_buffer_add_string( buf, "Hello, " );
mulle_buffer_add_string( buf, "world!" );
/* Append raw bytes */
const char extra[] = { ' ', 'C', '+', '+' };
mulle_buffer_add_bytes( buf, (void *)extra, sizeof extra );
/* Get the current length */
size_t len = mulle_buffer_get_length( buf ); /* -> 13 */
/* Extract a NUL-terminated C string (allocates a copy) */
char *cstr = mulle_buffer_extract_string( buf );
printf("%s (len=%zu)\n", cstr, len); /* prints: Hello, world! C++ */
/* Clean up */
mulle_buffer_destroy( buf );
free( cstr ); /* the string returned by extract_string must be freed */
return 0;
}
Example 2: Stack-Allocated Buffer
Use this pattern for temporary string building without heap allocation for the buffer structure itself.
#include "mulle-buffer.h"
int main(void)
{
/* Initialise a buffer on the stack; it will grow as needed */
struct mulle_buffer buf = MULLE_BUFFER_DATA( NULL );
/* Use guarantee when you want to write directly into the free area */
char *dst = mulle_buffer_guarantee( &buf, 10 );
if (dst) {
strcpy( dst, "StackBuf" ); /* write up to 10 bytes */
/* advance the cursor to reflect the written data */
mulle_buffer_advance( &buf, strlen(dst) );
}
/* Append more data using the high-level helpers */
mulle_buffer_add_string( &buf, " + more" );
/* Obtain the full content */
char *final = mulle_buffer_extract_string( &buf );
puts( final ); /* prints: StackBuf + more */
/* No need to call mulle_buffer_destroy because the struct itself
lives on the stack; just free the extracted copy. */
free( final );
return 0;
}
Example 3: Pre-Allocating Capacity
Avoid repeated reallocations by pre-sizing the buffer for large data appends.
#include "mulle-buffer.h"
int main(void)
{
struct mulle_buffer *buf = mulle_buffer_create( NULL );
/* Reserve 1 MiB up front (useful for large concatenations) */
if (mulle_buffer_grow( buf, 1024 * 1024 ) == 0) {
/* Now all subsequent adds will not trigger further allocations */
for (int i = 0; i < 1000; ++i)
mulle_buffer_add_string( buf, "data;" );
}
printf("final size = %zu\n", mulle_buffer_get_length( buf ));
mulle_buffer_destroy( buf );
return 0;
}
Key Implementation Files
Understanding the source architecture helps when debugging or extending buffer behavior:
src/mulle-buffer.h– Defines the public API, includingstruct mulle_buffer, theMULLE_BUFFER_DATAmacro, and inline helpers for length calculations.src/mulle-buffer.c– Contains the core implementation ofmulle_buffer_create(),mulle_buffer_grow(), and all addition functions. This file manages therealloclogic that enables the growable behavior.test/buffer/– Contains reference implementations such asgrowstress.candflexible.cthat demonstrate edge cases and performance characteristics of growable buffers.
Summary
- Use
mulle_buffer_create()to allocate a heap-based growable array that can be returned from functions or shared between scopes. - Use
MULLE_BUFFER_DATA()for zero-overhead stack allocation when building temporary strings or processing buffers locally. - Call
mulle_buffer_add_string()ormulle_buffer_add_bytes()to append data; the buffer automatically reallocates viamulle_buffer_grow()when capacity is exceeded. - Extract results with
mulle_buffer_extract_string()to obtain a null-terminated C string, ormulle_buffer_get_bytes()for raw pointer access. - Always pair
mulle_buffer_create()withmulle_buffer_destroy()to prevent memory leaks; free strings returned bymulle_buffer_extract_string()separately.
Frequently Asked Questions
What is the default initial capacity of a mulle-buffer?
According to src/mulle-buffer.h, the default capacity is defined by the MULLE_BUFFER_DEFAULT_CAPACITY macro. When you call mulle_buffer_create(NULL), the buffer initializes with this default size and doubles as needed through mulle_buffer_grow().
Can I use mulle-buffer for binary data or only for strings?
Mulle-buffer handles arbitrary binary data safely. While functions like mulle_buffer_add_string() treat input as text, mulle_buffer_add_bytes() accepts void pointers and lengths, making the buffer suitable for any growable byte array, including serialized structures or network packets.
How do I prevent multiple reallocations when I know the final size?
Call mulle_buffer_grow(buf, required_size) before adding data. As implemented in src/mulle-buffer.c, this function ensures the internal storage meets the requested capacity immediately, eliminating intermediate allocations during bulk operations.
Is the buffer returned by mulle_buffer_extract_string() null-terminated?
Yes. Unlike mulle_buffer_get_bytes(), which returns a pointer to the internal buffer that may not include a terminator, mulle_buffer_extract_string() allocates a new buffer, copies all content, and appends a null byte. You must free() this returned pointer when done.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →