# How mulle-buffer's Automatic Heap Growth Works: A Deep Dive into Dynamic Memory Expansion

> Discover how mulle-buffer's automatic heap growth works by doubling capacity on write operations. Learn about seamless byte appending without manual resizing in this deep dive.

- Repository: [mulle-c/mulle-buffer](https://github.com/mulle-c/mulle-buffer)
- Tags: deep-dive
- Published: 2026-03-07

---

**When a write operation exceeds the current allocation, mulle-buffer automatically triggers a heap reallocation that doubles the buffer's capacity (minimum 64 bytes), copies existing data to the new memory region, and updates internal pointers to enable seamless byte appending without manual resizing.**

The `mulle-buffer` library from the mulle-c organization provides a high-performance, growable byte buffer for C applications. Its **automatic heap growth** mechanism eliminates manual memory management by intercepting write operations that would overflow the current storage and transparently expanding the underlying allocation. This article explores the exact implementation details found in the source code of the `mulle-c/mulle-buffer` repository.

## The Three Core Mechanisms Behind Automatic Growth

The automatic growth system operates through three tightly-coupled building blocks that handle space guarantees, size calculations, and physical memory reallocation.

### _mulle__buffer_advance – The Public Entry Point

All high-level write operations ultimately delegate to `_mulle__buffer_advance`, which acts as the gateway to the growth mechanism. Located in [`src/mulle--buffer.h`](https://github.com/mulle-c/mulle-buffer/blob/main/src/mulle--buffer.h) (lines 62-73), this inline function requests space for a specific length and advances the cursor only if the guarantee succeeds.

```c
/* src/mulle--buffer.h – lines 62-73 */
static inline void *_mulle__buffer_advance( struct mulle__buffer *buffer,
                                           size_t length,
                                           struct mulle_allocator *allocator)
{
    unsigned char *reserved = _mulle__buffer_guarantee(buffer, length, allocator);
    if( reserved) buffer->_curr = &buffer->_curr[length];
    return reserved;
}

```

When `_mulle__buffer_guarantee` (lines 38-53) detects insufficient remaining space, it forwards the request to `_mulle__buffer_grow` with the exact number of additional bytes required.

### _mulle__buffer_get_new_allocation_length – The Growth Calculator

Before allocating memory, the library computes the optimal new size using `_mulle__buffer_get_new_allocation_length` in [`src/mulle--buffer.c`](https://github.com/mulle-c/mulle-buffer/blob/main/src/mulle--buffer.c) (lines 272-295). This function enforces a **"double-or-minimum"** growth strategy to prevent frequent reallocations while maintaining memory efficiency.

```c
/* src/mulle--buffer.c – lines 272-295 */
static size_t _mulle__buffer_get_new_allocation_length( struct mulle__buffer *buffer,
                                                        size_t growth)
{
    size_t plus = MULLE_BUFFER_MIN_GROW_SIZE;               // 64 (debug: 4)
    if( growth > plus) plus = growth;                      // ensure at least the requested growth

    if( ! buffer->_curr)                                   // first allocation
        new_size = plus < buffer->_size ? buffer->_size : plus;
    else {
        new_size = _mulle__buffer_get_allocation_length(buffer);
        new_size += plus < new_size ? new_size : plus;      // at least double the current size
    }
    return new_size;
}

```

The algorithm ensures the new capacity is at least **double the current size** and never smaller than `MULLE_BUFFER_MIN_GROW_SIZE` (64 bytes in release builds, 4 bytes in debug builds). For first-time heap allocations, it respects the buffer's initial capacity setting.

### _mulle__buffer_grow – The Reallocation Engine

The actual memory operation occurs in `_mulle__buffer_grow` ([`src/mulle--buffer.c`](https://github.com/mulle-c/mulle-buffer/blob/main/src/mulle--buffer.c), lines 301-359). This function distinguishes between flexible buffers (which can grow) and inflexible buffers (which cannot), handles the `realloc` call, and manages the transition from stack/static storage to heap memory.

```c
/* src/mulle--buffer.c – lines 301-359 */
int _mulle__buffer_grow( struct mulle__buffer *buffer,
                         size_t growth,
                         struct mulle_allocator *allocator)
{
    /* ... overflow and inflexible checks omitted ... */

    void *malloc_block = NULL;
    if( buffer->_storage != buffer->_initial_storage)
        malloc_block = buffer->_storage;                     // already malloc-ed block

    size_t new_size = _mulle__buffer_get_new_allocation_length(buffer, growth);
    size_t len      = buffer->_curr - buffer->_storage;      // current data length

    void *p = mulle_allocator_realloc(allocator, malloc_block, new_size);

    if( !malloc_block)                                      // first allocation
        memcpy(p, buffer->_initial_storage, len);           // copy from static/stack storage

    buffer->_storage  = p;
    buffer->_curr     = &buffer->_storage[len];
    buffer->_sentinel = &buffer->_storage[new_size];
    return 0;
}

```

If the buffer previously used static or stack storage (`_storage == _initial_storage`), the function allocates a fresh heap block and copies the existing payload. For already-heap-allocated buffers, it uses `mulle_allocator_realloc` to expand the existing block.

## Step-by-Step Growth Execution Flow

Understanding the exact sequence of operations clarifies how mulle-buffer provides transparent expansion.

### Triggering Growth During Byte Appending

The simplest growth trigger occurs when adding a single byte via `_mulle__buffer_add_byte` ([`src/mulle--buffer.h`](https://github.com/mulle-c/mulle-buffer/blob/main/src/mulle--buffer.h), lines 445-449):

```c
/* src/mulle--buffer.h – lines 445-449 */
static inline void _mulle__buffer_add_byte( struct mulle__buffer *buffer,
                                            uint8_t c,
                                            struct mulle_allocator *allocator)
{
    if( _mulle__buffer_is_full( buffer))
        if( _mulle__buffer_grow( buffer, 1, allocator))
            return;                     // growth failed → byte is dropped
    *buffer->_curr++ = c;
}

```

When `_curr` reaches `_sentinel` (detected by `_mulle__buffer_is_full`), the code calls `_mulle__buffer_grow` requesting exactly one byte. If growth succeeds (returns 0), the write proceeds; if it fails, the byte is silently dropped.

### Handling Large Write Operations

For bulk operations like `mulle_buffer_add_string`, the buffer calculates the total required space upfront. The `_mulle__buffer_advance` function requests the full length needed, causing `_mulle__buffer_grow` to compute a new size that accommodates the entire request in a single reallocation rather than multiple incremental growths.

## Special Cases and Buffer Types

### Inflexible Buffer Constraints

Buffers initialized with static memory (using `mulle_buffer_init_with_static_bytes`) carry the `MULLE_BUFFER_IS_INFLEXIBLE` flag. When these reach capacity, `_mulle__buffer_grow` does not reallocate. Instead, it either flushes flushable buffers or marks them as overflown using `_mulle__buffer_set_overflown`, causing subsequent writes to be ignored.

### First Allocation vs. Subsequent Growth

The growth behavior differs between initial heap allocation and subsequent expansions:
- **First allocation**: Uses the buffer's initial capacity (default 64 bytes) as the baseline if larger than `MULLE_BUFFER_MIN_GROW_SIZE`
- **Subsequent growth**: Always doubles the current allocation (or adds the minimum grow size, whichever is larger)

## Practical Code Examples

### Automatic Growth with Default Capacity

The following demonstration shows a buffer growing from its default 64-byte capacity to 128 bytes when writing 70 bytes:

```c
#include <mulle-buffer/mulle-buffer.h>
#include <stdio.h>

int main(void)
{
    // Create a flexible buffer with default capacity (64 bytes)
    struct mulle_buffer *buf = mulle_buffer_create_default();

    // Fill with 70 bytes, forcing one automatic growth step
    for (size_t i = 0; i < 70; ++i)
        mulle_buffer_add_byte(buf, (uint8_t)i);

    printf("length: %zu, capacity: %zu, overflown: %s\n",
           mulle_buffer_get_length(buf),
           _mulle__buffer_get_allocation_length((struct mulle__buffer *)buf),
           mulle_buffer_has_overflown(buf) ? "yes" : "no");

    mulle_buffer_destroy(buf);
    return 0;
}

```

**Output:**

```

length: 70, capacity: 128, overflown: no

```

The buffer started at 64 bytes, detected insufficient space at the 65th byte, and triggered `_mulle__buffer_grow`, which calculated 128 bytes as the new capacity (doubling the previous size while satisfying the minimum growth requirement).

### Growth Stress Testing

The repository's [`test/buffer/growstress.c`](https://github.com/mulle-c/mulle-buffer/blob/main/test/buffer/growstress.c) validates the growth algorithm by repeatedly requesting various lengths (0-199 bytes) and verifying that capacities follow the expected sequence: 64, 128, 256, 512, and so on, confirming the double-or-minimum growth strategy under real-world conditions.

## Summary

- **Automatic heap growth** in mulle-buffer triggers when `_mulle__buffer_is_full` detects that `_curr` has reached `_sentinel` during write operations.
- **Growth calculation** uses `_mulle__buffer_get_new_allocation_length` to ensure the new capacity is at least double the current size and never less than `MULLE_BUFFER_MIN_GROW_SIZE` (64 bytes).
- **Reallocation** occurs through `mulle_allocator_realloc` in `_mulle__buffer_grow`, which handles both first-time heap allocation (copying from static storage) and heap block expansion.
- **Inflexible buffers** cannot grow; they transition to an overflown state when capacity is exceeded.
- **Internal pointer management** updates `_storage`, `_curr`, and `_sentinel` atomically after successful reallocation, ensuring thread-safe visibility of the new memory region.

## Frequently Asked Questions

### What triggers automatic heap growth in mulle-buffer?

Automatic heap growth triggers when any write operation—such as `mulle_buffer_add_byte` or `mulle_buffer_add_string`—detects that the current cursor (`_curr`) has reached the sentinel boundary (`_sentinel`). The inline function `_mulle__buffer_is_full` performs this check, and when true, the write routine calls `_mulle__buffer_grow` with the number of additional bytes required before completing the write operation.

### How does mulle-buffer calculate the new capacity during growth?

The library calculates new capacity using the `_mulle__buffer_get_new_allocation_length` function, which returns the larger of three values: the requested growth size, `MULLE_BUFFER_MIN_GROW_SIZE` (64 bytes in release builds), or double the current allocation length. This ensures buffers grow exponentially to amortize reallocation costs while never allocating less than the immediate need.

### What happens when an inflexible buffer reaches capacity?

Inflexible buffers—those initialized with user-provided static memory—cannot undergo heap growth. When `_mulle__buffer_grow` detects the `MULLE_BUFFER_IS_INFLEXIBLE` flag, it either flushes the buffer if it is flushable, or calls `_mulle__buffer_set_overflown` to mark the buffer as overflown. Once overflown, subsequent write operations are ignored and data is dropped rather than buffered.

### Can I control the minimum growth size in mulle-buffer?

Yes, by modifying the `MULLE_BUFFER_MIN_GROW_SIZE` macro defined in [`src/mulle--buffer.c`](https://github.com/mulle-c/mulle-buffer/blob/main/src/mulle--buffer.c). The default is 64 bytes for release builds and 4 bytes for debug builds. Reducing this value decreases memory overhead for applications with many small buffers, while increasing it reduces the frequency of reallocations for workloads with sustained high-volume writes.