How mulle_allocator's Fail Handler Works and How to Customize It

The mulle_allocator fail handler is a function pointer stored in the fail field of every struct mulle_allocator that gets invoked when memory allocation fails; you can customize it globally using mulle_allocator_set_fail() or per-instance by directly assigning to the fail field.

The mulle-c/mulle-allocator library provides deterministic memory management by ensuring allocation routines never return NULL on failure. Instead, every allocator stores a fail handler callback that executes whenever mulle_allocator_malloc, mulle_allocator_calloc, or mulle_allocator_realloc cannot satisfy a request, allowing applications to define custom out-of-memory behavior.

How the Fail Handler Works

The fail Field in struct mulle_allocator

The fail handler is stored as a function pointer inside struct mulle_allocator (defined in src/mulle-allocator-struct.h). The field has the following signature:

void (*fail)(struct mulle_allocator *, void *, size_t) _MULLE_C_NO_RETURN;

When any allocation routine cannot obtain memory, it invokes this handler with three arguments: the allocator instance, the original block pointer (if reallocating), and the requested size. As implemented in src/mulle-allocator.c, the check follows this pattern:

if (MULLE_C_UNLIKELY(!q))
    (*p->fail)(p, block, size);

Default Failure Behavior: mulle_allocation_fail

By default, every allocator points to mulle_allocation_fail, implemented in src/mulle-allocator.c at lines 45-51. This handler prints the system error and immediately terminates the program:

MULLE_C_NO_RETURN
void mulle_allocation_fail(struct mulle_allocator *p,
                           void *block,
                           size_t size)
{
    perror("memory allocation:");
    abort();

    MULLE_C_UNUSED(p);
    MULLE_C_UNUSED(block);
    MULLE_C_UNUSED(size);
}

Because the function is annotated with _MULLE_C_NO_RETURN, the compiler assumes the allocation helpers never need to handle execution after the fail call returns.

How to Customize the Fail Handler

Using mulle_allocator_set_fail for Safe Updates

The public API provides mulle_allocator_set_fail (declared in src/mulle-allocator.h, lines 80-88) to safely install a custom handler. This inline helper accepts NULL to mean "use default" and automatically targets the global default allocator when passed NULL:

static inline void mulle_allocator_set_fail(struct mulle_allocator *p,
                                            mulle_allocator_fail_t *f _MULLE_C_NO_RETURN)
{
    if (!p)
        p = &mulle_allocator_default;
    p->fail = f ? f : mulle_allocation_fail;
}

To install a global handler that logs the failure size before aborting:

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

static void log_and_abort(struct mulle_allocator *a, void *b, size_t n)
    _MULLE_C_NO_RETURN
{
    fprintf(stderr, "[FAIL] Unable to allocate %zu bytes\n", n);
    abort();
}

int main(void)
{
    mulle_allocator_set_fail(&mulle_allocator_default, log_and_abort);
    void *p = mulle_malloc(SIZE_MAX);   // triggers handler
    (void)p;
    return 0;
}

Direct Field Assignment for Per-Allocator Control

Since the fail field is public, you can modify it directly on specific allocator instances without affecting the global default. This technique is demonstrated in test/fails/fail-malloc.c:

static void fail(struct mulle_allocator *allocator, void *unused, size_t ignored)
{
    printf("custom failure – terminating cleanly\n");
    exit(0);
}

int main(void)
{
    mulle_default_allocator.fail = fail;   // replaces default handler globally
    mulle_malloc(-1);                      // provokes failure
    return -1;
}

For isolated error handling, create a stack-based allocator copy with its own handler:

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

static void my_fail(struct mulle_allocator *a, void *b, size_t n)
    _MULLE_C_NO_RETURN
{
    fprintf(stderr, "my allocator failed for %zu bytes – cleaning up\n", n);
    exit(EXIT_FAILURE);
}

int main(void)
{
    struct mulle_allocator myalloc = mulle_allocator_default;
    myalloc.fail = my_fail;                                 // override only for this instance

    void *p = _mulle_allocator_malloc(&myalloc, 0);       // force failure
    (void)p;
}

Practical Implementation Examples

Global Logging Handler

Install a custom fail handler across your entire application to capture allocation statistics before termination:

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

static void log_and_abort(struct mulle_allocator *a, void *b, size_t n)
    _MULLE_C_NO_RETURN
{
    fprintf(stderr, "[FAIL] Unable to allocate %zu bytes\n", n);
    abort();               // terminates the program
}

int main(void)
{
    mulle_allocator_set_fail(&mulle_allocator_default, log_and_abort);
    /* This will trigger the custom handler */
    void *p = mulle_malloc(SIZE_MAX);   // unrealistic huge request
    (void)p;
    return 0;
}

Per-Allocator Handler for Isolated Error Handling

When you need specific error behavior for a subsystem without affecting global allocations, copy the default allocator and override its fail field:

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

static void my_fail(struct mulle_allocator *a, void *b, size_t n)
    _MULLE_C_NO_RETURN
{
    fprintf(stderr, "my allocator failed for %zu bytes – cleaning up\n", n);
    // perform any cleanup needed, then exit
    exit(EXIT_FAILURE);
}

int main(void)
{
    struct mulle_allocator myalloc = mulle_allocator_default; // copy defaults
    myalloc.fail = my_fail;                                 // override only for this instance

    void *p = _mulle_allocator_malloc(&myalloc, 0);        // force failure
    (void)p;
}

Test-Style Minimal Replacement

For testing failure paths, use a minimal handler that exits successfully rather than aborting:

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

static void quiet_success(struct mulle_allocator *a, void *b, size_t n)
    _MULLE_C_NO_RETURN
{
    printf("Allocation failed – but we consider it ok\n");
    exit(0);
}

int main(void)
{
    mulle_default_allocator.fail = quiet_success;   // replace globally
    mulle_malloc(-1);                              // triggers fail handler
}

Summary

  • Storage: Every struct mulle_allocator contains a fail function pointer field that stores the current failure handler.
  • Default: The built-in mulle_allocation_fail handler in src/mulle-allocator.c prints an error via perror() and calls abort().
  • Customization: Use mulle_allocator_set_fail() (from src/mulle-allocator.h) for safe global updates, or assign directly to allocator->fail for per-instance control.
  • Parameters: Handlers receive the allocator instance, the original block pointer (NULL for fresh allocations), and the requested size.
  • Contract: Fail handlers must be annotated with _MULLE_C_NO_RETURN, indicating they terminate the process via abort(), exit(), longjmp(), or similar.

Frequently Asked Questions

What parameters does the mulle_allocator fail handler receive?

The fail handler receives three parameters: a pointer to the struct mulle_allocator that experienced the failure, the original block pointer being reallocated (or NULL for malloc/calloc), and the size in bytes that could not be allocated. These allow the handler to log context-specific error details or attempt recovery based on the allocator's state.

Can I use different fail handlers for different allocators?

Yes. Because the fail field is part of struct mulle_allocator, you can create separate allocator instances—either on the stack or dynamically—and assign unique handlers to each. This allows subsystems to handle out-of-memory conditions independently without affecting the global default allocator used by the rest of the application.

How do I restore the default fail handler?

Pass NULL to mulle_allocator_set_fail(), or explicitly assign mulle_allocation_fail to the fail field. The mulle_allocator_set_fail helper automatically reverts to the default implementation when passed a NULL function pointer, ensuring you do not leave the allocator in an undefined state.

What happens if the fail handler actually returns?

The fail handler is annotated with _MULLE_C_NO_RETURN, which tells the compiler the function never returns. If you violate this contract and allow the handler to return normally, the behavior is undefined; typically, the allocation routine will fall through to subsequent code that assumes memory was allocated, likely causing immediate crashes or memory corruption. Always terminate execution or perform a non-local exit such as longjmp().

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 →