Using mulle_allocator with Memory Pools or Region-Based Allocation

Yes, mulle_allocator fully supports memory pools and region-based allocation through its pluggable callback interface, allowing you to substitute custom allocators or use the built-in stdlib_nofree variant for arena-style memory management.

The mulle-c/mulle-allocator library decouples memory management from data structures by requiring an explicit struct mulle_allocator * argument for every allocation operation. This design enables you to redirect all heap operations into a contiguous memory region or pool allocator without modifying existing code, as implemented in src/mulle-allocator-struct.h and src/mulle-allocator.h.

How the Pluggable Allocator Interface Works

At the core of the library is the callback table defined in src/mulle-allocator-struct.h (lines 66-68). The struct mulle_allocator contains function pointers for calloc, realloc, free, and failure handling, allowing you to replace the standard heap with any allocation strategy.

The library provides three built-in global instances declared in src/mulle-allocator.h (lines 75-88):

  • mulle_allocator_default – Uses the standard C library malloc family
  • mulle_allocator_stdlib – Explicit stdlib-based allocator with standard free behavior
  • mulle_allocator_stdlib_nofree – Allocates from the heap but ignores free calls, enabling simple pool-like behavior

Because functions like mulle_allocator_malloc, mulle_calloc, mulle_realloc, and mulle_strdup accept a struct mulle_allocator * parameter, you can pass a custom pool allocator to any data structure. The allocator propagates through the object graph, ensuring all nested allocations draw from the same region.

Implementing a Bump-Pointer Arena Allocator

For high-performance scenarios requiring bulk deallocation, you can implement a bump-pointer arena. This pattern allocates a large contiguous block once, then hands out smaller pieces via pointer arithmetic. The free callback becomes a no-op, and the entire arena releases with a single system call.

The following example demonstrates a custom arena that plugs into the mulle-allocator interface:

#include <mulle-allocator/mulle-allocator.h>
#include <stdlib.h>
#include <string.h>

static void *arena_base;
static size_t arena_offset;
static size_t arena_capacity;

static void arena_init(size_t reserve)
{
    arena_base = malloc(reserve);
    arena_offset = 0;
    arena_capacity = reserve;
}

static void *arena_malloc(struct mulle_allocator *alloc, size_t size)
{
    if (arena_offset + size > arena_capacity)
        return NULL;
    void *ptr = (char *)arena_base + arena_offset;
    arena_offset += size;
    return ptr;
}

static void *arena_realloc(struct mulle_allocator *alloc,
                           void *block, size_t size)
{
    if (block != (char *)arena_base + arena_offset - size)
        return NULL;
    if (arena_offset - size + size > arena_capacity)
        return NULL;
    arena_offset = arena_offset - size + size;
    return block;
}

static void arena_free(struct mulle_allocator *alloc, void *block)
{
    (void)alloc;
    (void)block;
}

static struct mulle_allocator arena_allocator = {
    .calloc = (void *(*)(size_t, size_t, struct mulle_allocator *))calloc,
    .realloc = arena_realloc,
    .free = arena_free,
    .fail = mulle_allocation_fail,
    .abafree = NULL,
    .aba = NULL
};

int main(void)
{
    arena_init(1 << 20);
    arena_allocator.calloc = (void *(*)(size_t,size_t,struct mulle_allocator *))
                              arena_malloc;

    char *s = mulle_allocator_strdup(&arena_allocator, "hello arena");
    printf("%s\n", s);

    free(arena_base);
    return 0;
}

Key implementation details:

  • The arena_allocator satisfies the struct mulle_allocator contract defined in src/mulle-allocator-struct.h
  • mulle_allocator_strdup (declared in src/mulle-allocator.h) draws from the arena instead of the heap
  • Individual frees are no-ops; the entire region releases via free(arena_base)

Using the Built-In stdlib_nofree Allocator

For simpler use cases where you need a static pool without implementing custom callbacks, the library provides mulle_allocator_stdlib_nofree. This allocator delegates allocation to malloc but substitutes a no-op for free, effectively creating a grow-only pool that persists for the process lifetime.

This approach works well for lookup tables or configuration data that lives for the entire execution:

#include <mulle-allocator/mulle-allocator.h>

static void *static_pool;
static size_t pool_used;
static size_t pool_size;

static void static_pool_init(size_t size)
{
    static_pool = malloc(size);
    pool_used = 0;
    pool_size = size;
}

static void *pool_malloc(struct mulle_allocator *alloc, size_t size)
{
    if (pool_used + size > pool_size)
        return NULL;
    void *ptr = (char *)static_pool + pool_used;
    pool_used += size;
    return ptr;
}

static struct mulle_allocator pool_allocator = {
    .calloc = (void *(*)(size_t,size_t,struct mulle_allocator *))pool_malloc,
    .realloc = (void *(*)(void *,size_t,struct mulle_allocator *))realloc,
    .free = (void (*)(void *,struct mulle_allocator *))free,
    .fail = mulle_allocation_fail,
    .abafree = NULL,
    .aba = NULL
};

int main(void)
{
    static_pool_init(64 * 1024);

    int *vec = mulle_allocator_malloc(&pool_allocator, 10 * sizeof(int));
    for (int i = 0; i < 10; ++i)
        vec[i] = i * i;
    
    return 0;
}

While this example implements a custom wrapper, you can alternatively use &mulle_allocator_stdlib_nofree directly for heap-backed, never-free allocation behavior as documented in assets/dox/TOC.md.

Embedding Pool Allocators in Data Structures

To ensure an entire object graph allocates from the same pool, embed the allocator pointer directly in your data structures. This pattern, described in the documentation at assets/dox/TOC.md (line 24), allows objects to move between different memory schemes by swapping the allocator reference.

typedef struct {
    struct mulle_allocator *alloc;
    int *data;
    size_t count;
    size_t capacity;
} int_vector;

static int_vector *int_vector_create(struct mulle_allocator *a)
{
    a = a ? a : &mulle_allocator_default;
    int_vector *v = mulle_allocator_malloc(a, sizeof(*v));
    v->alloc = a;
    v->capacity = 16;
    v->data = mulle_allocator_malloc(a, v->capacity * sizeof(int));
    v->count = 0;
    return v;
}

static void int_vector_push(int_vector *v, int value)
{
    if (v->count == v->capacity)
    {
        v->capacity *= 2;
        v->data = mulle_allocator_realloc(v->alloc, v->data,
                                           v->capacity * sizeof(int));
    }
    v->data[v->count++] = value;
}

static void int_vector_destroy(int_vector *v)
{
    mulle_allocator_free(v->alloc, v->data);
    mulle_allocator_free(v->alloc, v);
}

This approach guarantees that int_vector instances created with a pool allocator will return memory to that same pool during destruction, maintaining consistency across the object lifecycle.

Summary

  • The pluggable allocator interface in struct mulle_allocator enables substitution of custom memory management strategies without code modification
  • Bump-pointer arenas implement the allocator callbacks to serve memory from a pre-allocated region with bulk deallocation
  • The built-in mulle_allocator_stdlib_nofree provides ready-made, heap-backed pool behavior where individual frees are no-ops
  • Embedding allocator pointers in data structures ensures the entire object graph uses consistent pool allocation
  • All public API functions accept an explicit allocator argument, making region-based allocation transparent to library consumers

Frequently Asked Questions

Can I use mulle_allocator with existing memory pools?

Yes. By implementing the callback functions in struct mulle_allocator to wrap your existing pool's allocation and deallocation routines, you can pass the custom allocator to any mulle-allocator function. The library will route all malloc, realloc, and free operations through your pool's interface.

What is the difference between stdlib and stdlib_nofree allocators?

The mulle_allocator_stdlib allocator uses standard malloc, realloc, and free behavior, while mulle_allocator_stdlib_nofree uses malloc for allocation but implements free as a no-op. The no-free variant is useful for arena-style allocation where you intend to release the entire memory region at once rather than individual blocks.

How do I implement a bump-pointer allocator with mulle_allocator?

Implement custom calloc and realloc callbacks that return memory from a pre-allocated buffer using pointer arithmetic, and set the free callback to an empty function. Pass this custom struct mulle_allocator to functions like mulle_allocator_malloc or mulle_allocator_strdup to allocate from your arena.

Can I switch an existing object to use a different memory pool?

Yes. If your data structure embeds a struct mulle_allocator * pointer, you can change the allocator reference at any time to redirect future allocations (such as realloc operations) to a different pool. Existing allocated blocks remain valid in their original pool until freed.

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 →