How mulle-buffer Uses Stack and Heap Memory for Dynamic Buffers in C
mulle-buffer initializes storage on the stack using alloca or user-provided static arrays, then transparently migrates to heap allocation via the mulle_allocator abstraction only when the buffer grows beyond its initial capacity.
The mulle-c/mulle-buffer library provides a dynamic byte buffer implementation for C that uniquely straddles stack and heap memory. Unlike standard heap-only solutions, it defaults to stack allocation for small data and automatically promotes to the heap when needed, minimizing allocation overhead for typical use cases while maintaining flexibility for larger payloads.
Stack-First Allocation Strategy
Default Stack Buffers with mulle_buffer_do
The primary entry point mulle_buffer_do allocates initial storage on the stack using alloca. In src/mulle-buffer.h, the default capacity is defined as MULLE_BUFFER_DEFAULT_CAPACITY (approximately 96 bytes) at lines 78-81.
When you write mulle_buffer_do( buf ) { … }, the macro expands to a block that declares a temporary struct mulle_buffer named buf__storage and calls:
mulle_buffer_init( &buf__storage, MULLE_BUFFER_DEFAULT_CAPACITY, NULL );
This initialization appears around line 2100 in src/mulle-buffer.h. The NULL allocator argument instructs the buffer to use the default allocator for future heap growth, but the initial storage resides entirely in the alloca-allocated region on the stack.
Explicit Static Storage Options
For larger or custom stack allocations, the library provides mulle_buffer_do_flexible and mulle_buffer_do_inflexible. These macros invoke mulle_buffer_init_with_static_bytes (implemented inline around line 447 in src/mulle-buffer.h), which stores a pointer to your user-supplied array in the internal _storage field.
- Flexible mode: The buffer may grow to the heap if it exhausts the static array.
- Inflexible mode: The buffer treats the static array as a hard limit and reports overflow if exceeded.
The repository includes SVG diagrams in the pix/ directory (mulle-buffer-alloca.svg and mulle-buffer-malloc.svg) that visually illustrate these stack-backed versus heap-backed memory layouts.
Automatic Heap Migration
The Growth Trigger
The transition from stack to heap occurs automatically the first time the buffer cannot satisfy a write request within its current storage. All public mulle_buffer_add_* functions check mulle_buffer_assert_writeable before forwarding to private _mulle__buffer_* helpers in src/mulle--buffer.c.
When growth is required, _mulle__buffer_grow (lines 345-347) calculates a new size and reallocates:
new_size = _mulle__buffer_get_new_allocation_length( buffer, growth );
p = mulle_allocator_realloc( allocator, malloc_block, new_size );
The mulle_allocator_realloc call allocates a new heap block, copies existing data from the stack region, and updates the buffer's internal pointers.
Heap-Only Creation
For scenarios requiring immediate heap allocation, mulle_buffer_create (lines 384-388 in src/mulle--buffer.c) allocates the struct mulle__buffer itself on the heap using mulle_allocator_malloc. Subsequent growth operations continue using the specified allocator, bypassing any stack storage phase.
Practical Usage Patterns
Simple Stack-Backed Buffer (Default 96 Bytes)
#include "mulle-buffer.h"
void demo_default(void)
{
mulle_buffer_do( buf )
{
/* The buffer lives on the stack (alloca). */
mulle_buffer_add_string( &buf, "Hello, " );
mulle_buffer_add_string( &buf, "world!" );
printf("%s\n", mulle_buffer_get_string( &buf ));
} /* buf is automatically destroyed here */
}
This pattern uses mulle_buffer_do to create a stack-backed buffer that automatically falls back to heap allocation if the combined string exceeds 96 bytes.
Flexible Stack Storage with Heap Fallback
void demo_flexible(void)
{
char stack[256]; /* 256 bytes on the stack */
mulle_buffer_do_flexible( buf, stack, sizeof(stack) )
{
/* Buffer starts on the stack. If >256 bytes are added it will realloc. */
for (int i = 0; i < 300; ++i)
mulle_buffer_add_byte( &buf, 'x' );
printf("len=%zu\n", mulle_buffer_get_length( &buf )); /* 300 */
}
}
The first 256 bytes remain on the stack; the remaining 44 bytes trigger a heap allocation via the default allocator.
Inflexible Stack Buffer (No Heap Fallback)
void demo_inflexible(void)
{
char stack[8]; /* 7 bytes + NUL */
mulle_buffer_do_inflexible( buf, stack, sizeof(stack) )
{
mulle_buffer_add_string( &buf, "VfL_" ); /* fits */
mulle_buffer_add_string( &buf, "Bochum" );/* overflow → ignored */
printf("%s\n", mulle_buffer_get_string( &buf )); /* prints "VfL_" */
}
}
Because the buffer is inflexible, attempts to exceed the static storage abort the write rather than allocating heap memory.
Explicit Heap-Only Buffer
void demo_heap(void)
{
struct mulle_buffer *buf = mulle_buffer_create( NULL ); /* malloc */
mulle_buffer_add_string( buf, "Allocated on the heap" );
puts( mulle_buffer_get_string( buf ) );
mulle_buffer_destroy( buf ); /* free */
}
mulle_buffer_create allocates the buffer structure itself on the heap, with all subsequent growth using the allocator directly.
Summary
- mulle-buffer defaults to stack allocation via
alloca(96 bytes) through themulle_buffer_domacro, minimizing heap fragmentation for small data. - Explicit static storage macros (
mulle_buffer_do_flexibleandmulle_buffer_do_inflexible) allow custom stack arrays with optional or prohibited heap growth. - Automatic promotion to heap memory occurs in
_mulle__buffer_grow(defined insrc/mulle--buffer.c) when stack capacity is exhausted, using themulle_allocatorabstraction for portability. - Heap-only mode is available via
mulle_buffer_createfor scenarios where stack allocation is undesirable or impossible.
Frequently Asked Questions
When does mulle-buffer switch from stack to heap memory?
The transition occurs the first time a write operation exceeds the available stack storage. The mulle_buffer_add_* family checks capacity through mulle_buffer_assert_writeable and invokes _mulle__buffer_grow in src/mulle--buffer.c, which calls mulle_allocator_realloc to move the buffer to a heap-allocated block.
What is the default stack size when using mulle_buffer_do?
The default capacity is defined by MULLE_BUFFER_DEFAULT_CAPACITY in src/mulle-buffer.h (approximately 96 bytes). This value represents the initial alloca size before any heap fallback occurs.
Can I prevent a mulle-buffer from ever allocating on the heap?
Yes. Use the mulle_buffer_do_inflexible macro with a user-provided static array. This sets the buffer to inflexible mode, causing write operations to abort rather than trigger heap allocation via _mulle__buffer_grow when capacity is exceeded.
How do I create a buffer that starts immediately on the heap?
Call mulle_buffer_create( NULL ) (or pass a specific allocator). This function, implemented in src/mulle--buffer.c, allocates the struct mulle__buffer itself using mulle_allocator_malloc, bypassing the stack allocation phase entirely. You must later call mulle_buffer_destroy to free this memory.
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 →