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, including struct mulle_buffer, the MULLE_BUFFER_DATA macro, and inline helpers for length calculations.
  • src/mulle-buffer.c – Contains the core implementation of mulle_buffer_create(), mulle_buffer_grow(), and all addition functions. This file manages the realloc logic that enables the growable behavior.
  • test/buffer/ – Contains reference implementations such as growstress.c and flexible.c that 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() or mulle_buffer_add_bytes() to append data; the buffer automatically reallocates via mulle_buffer_grow() when capacity is exceeded.
  • Extract results with mulle_buffer_extract_string() to obtain a null-terminated C string, or mulle_buffer_get_bytes() for raw pointer access.
  • Always pair mulle_buffer_create() with mulle_buffer_destroy() to prevent memory leaks; free strings returned by mulle_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →