C Allocator Overview
Memory allocation in C typically involves direct use of malloc(),
calloc(), and free(). While flexible, these can incur performance
overhead, lead to fragmentation, and increase the risk of memory leaks or
dangling pointers in large applications.
The C Allocator module in this library provides lightweight and efficient
allocation utilities implemented in pure C and declared in
c_allocator.h. The only dpendency this module has within the CSalt
library is the c_error.h file, making it suitable ifor integration
into larger systems.
Build-time configuration flags modify behavior depending on needs:
STATIC_ONLY— Disables all heap allocation. Only stack or static memory supplied by the application is permitted.NO_FUNCTION_MACROS— Removes convenience macros that wrap allocation behavior. This improves transparency and supports MISRA C compliance.
These features make the allocator suitable for environments requiring strict determinism and safety analysis, such as embedded and real-time systems or projects that require compliance to MISRA C standards.
Arena Overview
The arena_t allocator implements a bump-pointer memory allocation model.
Memory is acquired in large blocks, and each allocation advances an internal
write cursor. All allocated memory may be reclaimed simultaneously, rather
than individually.
This model is ideal when:
a group of allocations share the same lifetime
memory may be cleared in a single operation
peak memory needs are known in advance or bounded
Examples include:
parsing and temporary string generation
construction of intermediate data structures
frame-based game engine or graphical workloads
scratch space for iterative scientific computations
It is also appropriate to use a single arena for program lifecycle memory managent if sufficient memory exists for the application.
Benefits
Fast allocation performance (constant time)
No fragmentation
High data locality
Complete reset in constant time using
reset_arena()
Limitations
Individual objects cannot be freed
Memory usage may exceed minimal requirements (i.e. dynamic allocations are in powers of 2)
Best used in controlled phases of execution
Static vs. Dynamic Arenas
There are two Arena types provided:
Static arena_t — fixed-size buffer supplied by the caller; no heap usage.
Dynamic arena_t — optionally grows through additional memory chunks.
When the build is configured with STATIC_ONLY, dynamic features are
excluded entirely at compile time, ensuring the resulting codebase contains
no hidden heap allocation paths.
This allows the same API to serve both safety-critical systems and high-performance workloads, simply by selecting different build flags.
Data Types
The following are data structures and derived data types used in the c_allocator.h
and c_allocator.c files to support the arena_t data type.
Chunk
Chunk is an opaque data structure that is not visibile to the user. This struct
contains metadata on a chunk of allocated memory. If the allocator is initialized
for dynamic memory allocation and it allows resizing, it will also point to the
next Chunk data structure.
struct Chunk{
uint8_t *chunk; // Pointer to beginning of memory
size_t len; // Populated length of memory within struct in bytes
size_t alloc; // Allocated length of memory within struct in bytes
struct Chunk* next; // Pointer to next memory chunk
};
arena_t
arena_t is an opaque data structure that can not be directly accessed by a user.
This structure contains all of the metadata associated with a bump allocator, in
additon, this structure also contains pointers to the head and tail Chunk structures
which contain all memory allocations. Metadata within this struct can be accessed
through getter functions.
typedef struct {
uint8_t* cur; // A pointer to the next available memory slot
Chunk* head; // Pointer to head of memory chunk linked list
Chunk* tail; // Pointer to the tail of memory chunks for the linked list
size_t alignment; // alignment in bytes
size_t len; // Total memory used in bytes between all memory chunks
size_t alloc; // Total memory allocated in bytes between all memory chunks
size_t tot_alloc; // Total memory allocated to include containers
size_t min_chunk; // The minimum chunk size in bytes
uint8_t mem_type; // type of memory used
uint8_t resize; // allows resizing if true with mem_type == DYNAMIC
uint8_t owns_memory; // 0 if structure does not own memory, 1 otherwise
uint8_t _pad[5]; // keep 8 byte passing
} arena_t;
ArenaCheckPoint
ArenaCheckPoint is an opaque data structure that is used to store data
related to a bump allocator. The restore_arena() function can extract the data from
this structure to reconstitute a bump allocator. This struct is defined as
ArenaCheckPoint in the .c file and ArenaCheckPointRep in the .h file.
typedef struct {
Chunk* chunk; // Pointer to saved memory chunk
uint8_t* cur; // Current pointer position in saved chunk
size_t len; // Length of saved chunk
} ArenaCheckPointRep;
arena_expect_t
arena_expect_t is an error handling struct to be used in the creation
of arena_t data types to catch and convey errors to a user.
typedef struct {
bool has_value;
union {
arena_t* value;
error_code_t error;
} u;
} arena_expect_t;
Initialization and Memory Management
The functions in this section can be used to initialize memory for a bump allocator, parse that memory to variables and to deallocate the memory. The csalt library restricts the user to a maximum heap allocation of 16 MB per chunk, which is sufficient for most applications. Larger memory allocations for engineering calculations (i.e. 1400 x 1400 matrices) will require a different custom allocator.
init_dynamic_arena
-
arena_expect_t init_dynamic_arena(size_t bytes, bool resize, size_t min_chunk_in, size_t base_align_in)
Initialize a dynamically growing arena allocator.
Allocates an arena that obtains its initial storage via
malloc()and, if permitted byresize, may grow by allocating new chunks on demand. The arena header, first chunk header, and initial data region are all placed contiguously in the first allocation. Additional chunks (if growth is enabled) are allocated separately and linked into the arena. If the user compiles the code with theSTATIC_ONLYflag, this function will not be compiled as part of the code.This function uses an “expected” style return type. On success, the returned
arena_expect_thashas_valueset totrueandu.valuepoints to a fully initializedarena_t. On failure,has_valueisfalseandu.errorcontains an appropriate error_code_t value describing the cause.On success:
result.has_value== trueresult.u.valueis a pointer to the newly createdarena_t.
On failure:
result.has_value== falseresult.u.errorcontains one of the following error_code_t values:
See also
init_static_arena(), free_arena(), alloc_arena(), alloc_arena_aligned(), reset_arena(), arena_remaining()
- Example
// Create a dynamic arena with 4 KiB initial storage, // allow future growth, minimum chunk of 4 KiB, default alignment. arena_expect_t res = init_dynamic_arena(4096, // initial bytes true, // resize enabled 4096, // minimum chunk size for growth alignof(max_align_t)); if (!res.has_avlue) { // Initialization failed; inspect the error code error_code_t ec = res.u.error; // handle error (log, abort, fall back, etc.) return; } arena_t* a = res.u.value; void_ptr_expect_t expect = alloc_arena(a, 128, true) // allocate 128 zeroed bytes if (!expect.has_value) { // Handle error } void *p = expect.u.value; // ... use arena allocations ... free_arena(a); // releases all chunks and the arena header itself
Note
On success, the returned arena’s first chunk is fully initialized and ready for allocation via
alloc_arena()oralloc_arena_aligned().Note
The initial data region begins at an address aligned to
max(base_align_in, alignof(max_align_t))after normalization.Warning
On success, the arena returned in
result.u.valuemust be released withfree_arena(). Individual allocations from the arena must not be freed withfree().- Parameters:
bytes – Requested initial total size in bytes. This is the minimum storage footprint to allocate. If
bytesis smaller thanmin_chunk_in(when non-zero), the larger value is used instead.resize – Whether the arena may grow when out of space. If
false, the arena behaves like a static arena that simply lives in heap memory.min_chunk_in – Minimum data chunk size to allocate on growth (0 allowed). If non-zero and not a power-of-two, it is rounded up to the next power-of-two. When growing, the arena will allocate at least this many bytes.
base_align_in – Required minimum base alignment for all allocations within the arena (0 => use
alignof(max_align_t)). Rounded up to the next power-of-two if needed and never less thanalignof(max_align_t).
- Returns:
An arena_expect_t result describing either success or failure.
- Return values:
INVALID_ARG – If
bytesis too small to contain the arena header and first chunk header, or ifbase_align_inormin_chunk_inhas an invalid value that cannot be normalized (e.g., overflow while rounding up to a power-of-two).ALIGNMENT_ERROR – If alignment normalization fails or the computed aligned data region cannot be placed within the allocated buffer.
LENGTH_OVERFLOW – If arithmetic overflow is detected during pointer or size computations for the header, chunk, or data regions.
OUT_OF_MEMORY – If the computed usable data region size is zero after layout, indicating that no space remains for allocations.
BAD_ALLOC – If the underlying
malloc()call for the initial arena buffer fails.FEATURE_DISABLED – If dynamic arenas are disabled at compile time (
ARENA_ENABLE_DYNAMIC== 0).
- Pre:
ARENA_ENABLE_DYNAMICmust be enabled (non-zero) at compile time.- Post:
On success (
result.has_value== true), the arenaa=result.u.valuesatisfies:a->mem_type== DYNAMICa->resize== (resize ? 1 : 0)a->head== a->taila->curbegins at the start of the aligned initial data region.
init_static_arena
-
arena_expect_t init_static_arena(void *buffer, size_t bytes, size_t passing_in)
Initialize a STATIC (non-growing) arena inside a caller-supplied buffer.
This function constructs an arena in place within the memory range
[buffer, buffer + bytes). No heap allocations occur. The memory layout is:The data region for the head chunk is aligned to[ arena_t header | padding | struct Chunk | padding | data... ]
alignment_in(rounded up to a power of two and never less thanalignof(max_align_t)). Any alignment padding reduces the usable capacity available for allocations.On success:
result.has_value== trueresult.u.valueis a pointer to the in-placearena_t
On failure:
result.has_value== falseresult.u.errorcontains one of the following error_code_t values:
- Example: Using a fixed-size stack buffer
uint8_t buf[4096]; arena_expect_t r = init_static_arena(buf, sizeof(buf), alignof(max_align_t)); if (!r.has_value) { error_code_t ec = r.u.error; // handle error... } arena_t *a = r.u.value; void_ptr_expect_t expect = alloc_arena(a, 128, false) // ok if (!expect.has_value) { // Handle error } void *p1 = except.u.value; expect = alloc_arena(a, 9000, false); if (!expect.has_value) { // This will fail since capacity has been exceeded printf(error_to_string(expect.error)); } reset_arena(a, false); // discards allocations, capacity unchanged // No need to free buf; STATIC arenas do not own their backing store.
- Example: Caller buffer with forced misalignment
enum { RAW = 16384 }; uint8_t *raw = malloc(RAW); uint8_t *unaligned = raw + 1; // deliberately unaligned start arena_expect_t r = init_static_arena(unaligned, RAW - 1, 64); if (!r.has_value) { // handle error... } arena_t *a = r.u.value; void_ptr_expect_t expect = alloc_arena(a, 256, true); if (!expect.has_value) { // Handle error } void *p = expect.u.value; // Guaranteed 64-byte alignment: assert(((uintptr_t)p % 64u) == 0u); free(raw); // frees original allocation, not the arena
Note
The arena is created in STATIC mode:
mem_type= STATICresize= 0 (growth is not permitted) Attempts to grow viaalloc_arena()beyond remaining space should return an appropriate error_code_t (oftenOUT_OF_MEMORYorOPERATION_UNAVAILABLE).
Note
tot_allocis set to the full caller-supplied buffer footprint (i.e.,bytes).allocis the usable data capacity after all required alignment and header placement.Warning
Do not free
bufferwhile the arena is in use.
The arena header lives
insidebuffer, so freeingbufferinstantly invalidates the arena.
The public
free_arena()implementation must not attempt to free the backing store in STATIC mode.- Parameters:
buffer – Caller-supplied backing store for the arena. Must remain valid for the entire arena lifetime. The arena header and the first chunk header are written directly into
buffer.bytes – Total size in bytes of
buffer. This must be large enough to hold:one
arena_t,one
Chunk,at least one byte of data after alignment.
alignment_in – Requested base alignment for the chunk’s data region. If zero, defaults to
alignof(max_align_t). If not a power of two, it is rounded up to the next power of two. The final alignment is always at leastalignof(max_align_t).
- Returns:
An arena_expect_t describing success or failure.
- Return values:
NULL_POINTER – If
bufferis NULL.INVALID_ARG – If
bytesis too small to containarena_t+Chunkafter alignment, or if the aligned Arena header would lie beyond the buffer.ALIGNMENT_ERROR – If
alignment_incannot be normalized to a valid power-of-two, or if the aligned data region cannot fit inside the provided buffer.LENGTH_OVERFLOW – If arithmetic overflow is detected while computing pointer offsets or buffer boundaries.
OUT_OF_MEMORY – If no usable bytes remain for the data region after alignment.
- Pre:
bufferpoints to at leastbytesof valid writable memory.- Pre:
The lifetime of
bufferstrictly dominates the lifetime of the arena.- Post:
On success:
a->headanda->tailreference the single in-place head chunka->curbegins at the start of the aligned data regiona->len== 0a->alloc== usable aligned data bytesa->tot_alloc== bytes
init_darena
-
arena_expect_t init_darena(size_t bytes, bool resize)
Convenience initializer for a dynamic arena with common defaults.
This helper constructs a dynamic (heap-backed, optionally growing) arena using typical defaults by forwarding to init_dynamic_arena(): If the code is compiled with the
STATIC_ONLYflag, this function will not be compiled as part of the code base.init_dynamic_arena(bytes, resize, 4096u, alignof(max_align_t));
Use it when you want a dynamic arena with:
a reasonable minimum chunk size (4 KiB), and
a base alignment of at least
alignof(max_align_t), without manually specifying growth chunk size or base alignment.
If you need custom growth tuning or a different base alignment, call init_dynamic_arena() directly instead.
On success:
result.has_value== trueresult.u.valuepoints to the newly createdarena_t.
On failure:
result.has_value== falseresult.u.erroris the error_code_t propagated from init_dynamic_arena(), typically one of:INVALID_ARG— invalid size or layout (e.g., too small for headers)ALIGNMENT_ERROR— base alignment normalization or placement failedLENGTH_OVERFLOW— arithmetic overflow in size/layout calculationsOUT_OF_MEMORY— no usable data space after layoutBAD_ALLOC— underlyingmalloc()for the initial buffer failedFEATURE_DISABLED— dynamic arenas disabled at compile time
See also
- Example
arena_expect_t r = init_darena(4096u, true); if (!r.has_value) { error_code_t ec = r.u.error; // handle error (log, abort, fallback, etc.) return; } arena_t *a = r.u.value; void_ptr_expect_t expect = alloc_arena(a, 128u, true); if (!expect.has_value) { // Handle error } void* p = expect.u.value; // ... use arena allocations ... free_arena(a);
Note
Minimum chunk size is fixed at 4096 bytes (4 KiB) in this wrapper. This is a conventional page-like default, not a guarantee of OS page size. For workloads that benefit from different growth steps, call init_dynamic_arena() directly.
Note
The arena’s base alignment is
alignof(max_align_t)in this wrapper. Per-allocation alignment follows this base unless you use the aligned allocation variant.Warning
Dynamic arenas allocate from the heap. Release resources via free_arena() when done. Individual allocations from the arena must not be freed with
free().- Parameters:
bytes – Initial allocation footprint requested for the arena’s first region. The implementation may increase this internally to satisfy header placement and the minimum chunk rule.
resize – If
true, the arena may grow by allocating additional chunks as needed. Iffalse, allocations that exceed the remaining capacity will fail with an appropriate error_code_t.
- Returns:
An arena_expect_t describing success or failure.
init_sarena
-
arena_expect_t init_sarena(void *buffer, size_t bytes)
Convenience initializer for a STATIC (non-growing) arena with default alignment.
This helper constructs a STATIC (non-growing, in-place) arena inside a caller-supplied buffer by forwarding to init_static_arena():
init_static_arena(buffer, bytes, alignof(max_align_t));
Use it when you want an in-place arena built inside a caller-managed buffer with the data region aligned to at least
alignof(max_align_t)and do not need a custom base alignment. For different alignment requirements, call init_static_arena() directly.On success:
result.has_value== trueresult.u.valuepoints to the in-placearena_tinsidebuffer.
On failure:
result.has_value== falseresult.u.erroris the error_code_t propagated from init_static_arena(), typically one of:NULL_POINTER—bufferisNULLINVALID_ARG—bytestoo small for header + chunk + dataALIGNMENT_ERROR— base alignment normalization or data placement failedLENGTH_OVERFLOW— arithmetic overflow during layout computationOUT_OF_MEMORY— no usable data region after layout
- Example: Caller-managed stack buffer
enum { BUF = 16u * 1024u }; uint8_t buf[BUF]; // stack- or static-allocated backing store arena_expect_t r = init_sarena(buf, BUF); if (!r.has_value) { error_code_t ec = r.u.error; // handle error... return; } arena_t *a = r.u.value; void_ptr_expect_t expect = alloc_arena(a, 1024u, false); if (!expect.has_value) { // Hanlde error } void *p = expect.u.value; // Reset discards allocations but keeps capacity. reset_arena(a, false); // No explicit free for 'buf' is needed if it is stack or static storage. // ::free_arena(a) should perform only logical cleanup in STATIC mode.
Note
The arena is created in STATIC mode (no growth):
mem_type= STATICresize= 0 (fixed capacity) Calls to alloc_arena() that exceed remaining capacity will fail and return an appropriate error_code_t.
Note
Ownership: the arena does not own
buffer. free_arena() for a STATIC arena must not attempt to freebuffer. Typically, the caller uses stack or statically allocated storage forbuffer, which is released when its scope or program lifetime ends.- Parameters:
buffer – Caller-supplied backing store where the arena header, first chunk header, and data region will be constructed in place. Must remain valid and writable for the entire arena lifetime.
bytes – Total size in bytes of
buffer. Must be large enough to contain onearena_t, oneChunk, and at least one byte of usable data after alignment and padding.
- Returns:
An arena_expect_t describing success or failure.
init_arena_with_arena
-
arena_expect_t init_arena_with_arena(arena_t *parent, size_t bytes, size_t alignment_in)
Create a fixed-capacity sub-arena carved from a parent arena.
This function allocates a contiguous memory region from
parentusing a single call toalloc_arena(), and constructs an entire arena in place within that region:The resulting sub-arena:[ arena_t header | padding | Chunk | padding | usable data ]
does not own its memory (owned by the parent)
cannot grow (fixed capacity)
**inherits
mem_type**from the parent arena
On success:
result.has_value== trueresult.u.valuepoints to the newly constructed sub-arena
On failure:
result.has_value== falseresult.u.errorcontains one of the following error_code_t values:
- Example: Scoped sub-arena for temporary allocations
arena_expect_t main_r = init_dynamic_arena(1024 * 1024, true, 4096, 0); assert(main_r.has_value); arena_t *main = main_r.u.value; // Carve an 8 KiB sub-arena (headers included) arena_expect_t sub_r = init_arena_with_arena(main, 8192, alignof(max_align_t)); assert(sub_r.has_value); arena_t *temp = sub_r.u.value; void_ptr_expect_t expect = alloc_arena(temp, 1024, false); if (!expect.has_value) { // Hanlde error } void *data = expect.u.value; // Sub-arena teardown: no memory is freed; parent still owns everything. free_arena(temp); // Parent teardown releases the entire carved region. free_arena(main);
Note
The sub-arena has:
mem_type= parent->mem_typeresize= 0 (cannot grow)owns_memory= 0 (parent owns the region)
Note
free_arena()on a sub-arena performs only shallow cleanup (no memory is freed). The carved region is released only when the parent arena is reset or freed.Warning
Resetting or freeing
parentimmediately invalidates all sub-arenas carved from it.- Parameters:
parent – [inout] Pointer to a valid parent arena. Must not be NULL.
bytes – [in] Total number of bytes to carve from the parent, including headers, padding, and data region.
alignment_in – [in] Requested base alignment for the sub-arena’s data region (0 =>
alignof(max_align_t)). Rounded up to the next power of two if needed.
- Returns:
An arena_expect_t describing success or failure.
- Return values:
NULL_POINTER – If
parentis NULL.INVALID_ARG – If
bytesis zero or too small to containarena_t+Chunk+ at least one data byte, or if alignment normalization fails.LENGTH_OVERFLOW – If pointer arithmetic overflows when computing internal layout.
ALIGNMENT_ERROR – If the aligned data region cannot fit within the carved block.
OUT_OF_MEMORY – If the parent arena cannot satisfy the
bytesrequest.
- Post:
On success:
The sub-arena’s header and first chunk reside entirely within the carved parent region.
a->curbegins at the aligned start of the data region.a->alloc== usable aligned data capacity.a->tot_alloc== bytes.
init_arena_with_buddy
-
arena_expect_t init_arena_with_buddy(buddy_t *buddy, size_t bytes, size_t base_align_in)
Initialize an arena_t backed by a region allocated from a buddy allocator.
This function creates an arena whose entire lifetime and storage are contained inside a single allocation obtained from the provided
buddyallocator. The allocated region is used to store:the
arena_theader,exactly one
Chunkheader, andthe arena’s data region aligned to
base_align_in.
The returned arena pointer is:
the address of the arena_t object itself, and
exactly equal to the pointer returned by
alloc_buddy().
This is important because the backing memory must later be released via return_arena_with_buddy(), which internally calls
return_buddy_element()using the arena pointer. The buddy allocator exists on the heap. If the code is compiled with theSTATIC_OBLYflag, this function will not be compiled with the code base.The initialized arena has:
mem_type= DYNAMIC (memory originates from a dynamic source)resize= 0 (fixed-capacity sub-arena, no growth)owns_memory= 0 (buddy allocator owns the backing memory)alloc= usable aligned data bytestot_alloc=bytes
- Successful Result
On success:
result.has_value== trueresult.u.valuepoints to a fully constructedarena_tlocated inside the buddy-allocated region.
Possible error codes:
NULL_POINTER—buddyis NULL.INVALID_ARG—bytesis zero, or region too small to holdarena_t+Chunk+ at least one byte of data.ALIGNMENT_ERROR—base_align_incannot be normalized to a valid power-of-two alignment ≥alignof(max_align_t).LENGTH_OVERFLOW— arithmetic overflow detected when computing layout.OUT_OF_MEMORY—alloc_buddy()could not satisfy the request.
- Failure Result
On failure:
result.has_value== falseresult.u.errorcontains an appropriate error_code_t.
If an allocation was obtained from the buddy allocator but a subsequent layout/alignment check fails, the region is returned automatically to
buddyviareturn_buddy_element()before the error is returned.See also
alloc_buddy(), return_buddy_element(), return_arena_with_buddy(), init_dynamic_arena(), init_static_arena()
- Example
buddy_t *b = init_buddy_allocator(4096u, 64u, alignof(max_align_t)); if (!b) { // handle error } arena_expect_t r = init_arena_with_buddy(b, 1024u, alignof(max_align_t)); if (!r.has_value) { error_code_t ec = r.u.error; // handle error } arena_t *a = r.u.value; void *p = alloc_arena(a, 128u, true).u.value; assert(p != NULL); // When finished with this arena: (void)return_arena_with_buddy(a, b); free_buddy(b);
Warning
Do not call
free_arena()on an arena created by this function. Release it only through return_arena_with_buddy(), which returns the entire sub-region to the buddy allocator.- Parameters:
buddy – Valid buddy allocator instance that provides the backing memory.
bytes – Total size of the sub-region to reserve from
buddy. This includes the arena header, the Chunk header, and all aligned/usable data space.base_align_in – Desired per-arena base alignment for allocations made via alloc_arena(). If zero, defaults to
alignof(max_align_t).
- Returns:
An arena_expect_t describing success or failure.
return_arena_with_buddy
-
bool return_arena_with_buddy(arena_t *arena, buddy_t *buddy)
Return a buddy-backed arena region to its buddy allocator.
This function returns the entire memory region associated with an arena created by init_arena_with_buddy() back to the underlying buddy allocator.
The
arenapointer must be the same pointer that was originally returned by init_arena_with_buddy(), which is also the user pointer obtained from alloc_buddy(). The function treatsarenaas a single buddy allocation and returns it via return_buddy_element(). This function will not compile with the code base if theSTATIC_ONLYflag is invoked.After a successful call:
the memory containing
arena, its Chunk header, and the arena’s data region is returned tobuddy, andthe
arenapointer becomes invalid and must not be dereferenced.
This function is intended only for arenas that do not own their memory independently.
Failure is only apprant based in false value return. Possible failure reasons are the following
NULL pointers for arena or buddy passed to function
The arena does not own its memory
The arena structure is not a pointer within the buddy memory allocation
On success, return_buddy_element() is called with
arena, and its return value is propagated to the caller. In that case, the memory region is considered freed from the arena’s perspective.buddy_t *b = init_buddy_allocator(4096u, 64u, alignof(max_align_t)); if (!b) { // handle error } arena_t *a = init_arena_with_buddy(b, 1024u, alignof(max_align_t)); if (!a) { // handle error } // Use 'a' for various arena allocations... if (!return_arena_with_buddy(a, b)) { // handle error; memory not returned to buddy } free_buddy(b);
- Parameters:
arena – Pointer to an arena_t instance that was created by init_arena_with_buddy().
buddy – Pointer to the buddy_t allocator that originally provided the backing memory.
- Return values:
true – On success; the region has been returned to
buddyandarenais no longer valid.false – On failure.
free_arena
-
void free_arena(arena_t *arena)
Destroy a dynamically allocated arena and frees all of its heap memory.
This function releases the entire allocation associated with a dynamic arena created by
init_dynamic_arena()/init_darena(). It walks and frees any growth chunks after the head, then frees the base block that contains thearena_theader (and the head chunk). After this call, all pointers previously returned byalloc_arena()become invalid.- Ownership model
Dynamic: a single
free(arena)releases the base block (arena header + head chunk). Each growth chunk is a single-malloc block freed withfree(chunk_header). Internal pointers such aschunk->chunkmust never be freed directly.Static:
free_arena()must not free the caller’s buffer.
- Example (dynamic arena)
arena_expect_t expect = init_darena(4096, true); if (!expect.has_value) { // Handle error } arena_t *a = expect.u.value; void_ptr_expect_t aexpect = alloc_arena(a, 1024, false); if (!aexpect.has_value) { // Handle error } void *p1 = aexpect.u.value; // ... use p1 ... free_arena(a); // releases all chunks and 'a' itself a = NULL; // prevent accidental reuse
- Example (static arena)
enum { BUF = 8192 }; void *buf = malloc(alignof(max_align_t)); arena_expect_t expect = init_sarena(buf, BUF); if (!expect.has_value) { // Handle error } arena_t *a = expect.u.value; // Later: free_arena(a); // Correct teardown: // (Optionally) reset_arena(a, false); free(buf); // caller frees its own buffer
Note
For STATIC arenas (from
init_static_arena()/), the arena header lives inside the caller’s buffer; freeing it here would be invalid. In STATIC mode the caller manages the buffer’s lifetime separately and may optionally provide a no-op teardown.Warning
This function is not idempotent. Calling
free_arena()twice on the same pointer is undefined behavior (typically a double-free). After calling, set your variable toNULLor use a helper likedispose_arena(&arena)to null the caller’s pointer.- Parameters:
arena – Pointer to a dynamic arena instance to destroy.
- Pre:
arenawas allocated byinit_dynamic_arena()/init_darena().- Pre:
arena->mem_type==DYNAMIC.- Post:
All heap memory owned by the arena is released;
arenaitself is freed.
alloc_arena
-
inline void_ptr_expect_t alloc_arena(arena_t *arena, size_t bytes, bool zeroed)
Allocate a block from an arena with its base alignment, returning an explicit success-or-error result.
Performs a bump allocation from the arena’s current tail chunk. The returned block is aligned to the arena’s base alignment (see arena_t::alignment).
This function does not signal errors through
errno. Instead it returns a void_ptr_expect_t structure containing either:.has_value = trueand.u.value = void*on success, or.has_value = falseand.u.error = error_code_tdescribing the failure.
If enough space exists in the current tail chunk, the block is carved out immediately (with any required leading padding).
If not enough space exists:
STATIC arenas fail immediately.
DYNAMIC arenas grow by allocating a new chunk (if dynamic support is compiled in and growth is enabled).
If growth is not permitted or the new chunk cannot be allocated, the call fails.
The function returns an error with one of the following codes:
Condition
Returned error_code_t
arena == NULLNULL_POINTERbytes == 0INVALID_ARGarena->alignmentis zero or not a power-of-twoALIGNMENT_ERRORarena->tail == NULL(invalid or corrupted arena state)ILLEGAL_STATEPadding + size overflows
size_tLENGTH_OVERFLOWInsufficient space & arena is STATIC
OPERATION_UNAVAILABLEInsufficient space & growth disabled (
resize == 0)OPERATION_UNAVAILABLEGrowth required but dynamic support compiled out
UNSUPPORTED_next_chunk_size()reports an invalid/overflow sizeLENGTH_OVERFLOW_chunk_new_ex()fails to allocate a new chunkBAD_ALLOCThe arena charges both the payload and any leading padding needed to reach alignment. Internal usage counters (
tail->len,arena->len) increase by:wherepad + bytes
pad ∈ [0, alignment-1].For the first allocation from a newly grown chunk, no leading padding is required because the chunk’s data base is already aligned.
The returned pointer must not be freed with
free(). Memory is released only when the arena is reset with reset_arena() or destroyed via free_arena().
See also
init_static_arena(), init_dynamic_arena(), alloc_arena_aligned(), reset_arena(), free_arena()
- Example: STATIC arena (no growth)
uint8_t buf[8192]; arena_expect_t ar = init_static_arena(buf, sizeof(buf), alignof(max_align_t)); assert(ar.has_value); arena_t *a = ar.u.value; void_ptr_expect_t r1 = alloc_arena(a, 256, true); assert(r1.has_value); void *p1 = r1.u.value; // Oversized request -> error (OPERATION_UNAVAILABLE) void_ptr_expect_t r2 = alloc_arena(a, 9000, false); if (!r2.has_value) { printf("alloc failed: %d\n", r2.u.error); }
- Example: DYNAMIC arena (growth allowed)
arena_expect_t ar = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); assert(ar.has_value); arena_t *a = ar.u.value; // Fill current chunk (void)alloc_arena(a, 4095, false); // This allocation will grow a new chunk void_ptr_expect_t r = alloc_arena(a, 2, false); if (!r.has_value) { printf("alloc failed: %d\n", r.u.error); } free_arena(a);
- Parameters:
arena – Pointer to an initialized arena_t. Must not be NULL.
bytes – Requested payload size in bytes. Must be > 0.
zeroed – If true, the returned payload is zero-initialized. Otherwise it is left uninitialized.
- Returns:
A void_ptr_expect_t struct:
On success:
.has_value = true,.u.value = pointer.On failure:
.has_value = false,.u.errorcontaining:
- Pre:
arenamust be initialized.arena->alignmentmust be a non-zero power-of-two.
- Post:
On success:
The returned pointer is aligned to
arena->alignment.arena->curadvances bypad + bytes.Usage counters (
tail->len,arena->len) reflect the allocation.
realloc_arena
-
inline void_ptr_expect_t realloc_arena(arena_t *arena, void *variable, size_t var_size, size_t realloc_size, bool zeroed)
Reallocate an object within an arena, returning a void_ptr_expect_t.
Arenas cannot grow an allocation in place. This function provides a
realloc-like interface that implements reallocation as:If
realloc_size<=var_size:→ No-op. Return the original pointer in a success expect.Otherwise:
Allocate a new block of
realloc_sizebytes using alloc_arena().Copy the first
var_sizebytes fromvariableinto the new block.If
zeroedis true, zero-fill bytes [var_size,realloc_size).Return the new pointer in a success expect.
The original memory is not freed and remains part of the arena until a reset. This function never reclaims memory on failure.
1. INVALID_ARG
arenais NULLvariableis NULL
- Failure conditions and error_code_t meanings
A failure expect (has_value = false) may contain any of the following:
2. LENGTH_OVERFLOW
Internal size arithmetic (e.g., var_size + (realloc_size - var_size)) overflows size_t
3. BAD_ALLOC (propagated from alloc_arena)
Arena lacks remaining capacity AND:
arena is STATIC, or
arena->resize == 0 (growth disabled), or
dynamic support is compiled out
Or dynamic growth attempted but chunk allocation failed
4. ALIGNMENT_ERROR (propagated from alloc_arena)
Arena’s base alignment invalid or corrupted
5. ILLEGAL_STATE (propagated from alloc_arena)
Arena internal pointers (e.g., tail) are invalid
// Example: growing an integer array inside an arena arena_expect_t r = init_dynamic_arena(4096u, true, 4096u, alignof(max_align_t)); if (!r.has_value) { fprintf(stderr, "Arena init failed: %d\n", r.u.error); return; } arena_t *arena = r.u.value; size_t n = 4u; void_ptr_expect_t e0 = alloc_arena(arena, n * sizeof(int), true); if (!e0.has_value) { fprintf(stderr, "Initial alloc failed: %d\n", e0.u.error); return; } int *arr = e0.u.value; for (size_t i = 0; i < n; ++i) arr[i] = (int)i; // Grow from 4 to 8 ints size_t new_n = 8u; void_ptr_expect_t er = realloc_arena(arena, arr, n * sizeof(int), new_n * sizeof(int), true); // zero-fill new region if (!er.has_value) { fprintf(stderr, "Realloc failed: %d\n", er.u.error); return; } int *new_arr = er.u.value; // new_arr now holds: [0, 1, 2, 3, 0, 0, 0, 0] reset_arena(arena); free_arena(arena);
See also
See also
Note
This function never frees memory; old blocks accumulate until the arena is reset via reset_arena().
Alignment of the new block is determined by alloc_arena() rules.
Passing a pointer not allocated by the arena results in undefined behavior (this function cannot detect foreign pointers).
Not thread-safe unless externally synchronized.
- Parameters:
arena – Pointer to an initialized arena_t instance. Must not be NULL.
variable – Pointer to an existing block previously allocated from
arena. Must not be NULL. Ownership is not validated; passing a foreign pointer causes undefined behavior.var_size – Size in bytes of the existing object. Must be exactly the size originally allocated; incorrect sizes result in undefined behavior (e.g., buffer overrun during memcpy).
realloc_size – Requested new size. If <=
var_size, no new allocation occurs. If >var_size, a new block is allocated and old contents copied.zeroed – If true and a new block is allocated, only the tail region (bytes [
var_size,realloc_size)) is zero-filled. The leadingvar_sizebytes are preserved.
- Returns:
void_ptr_expect_t
On success:
expect.has_value = true
expect.u.value = original pointer (no-op case) OR the newly allocated pointer (growth case)
On failure:
expect.has_value = false
expect.u.error = one of the error_code_t values described below
alloc_arena_aligned
-
inline void_ptr_expect_t alloc_arena_aligned(arena_t *arena, size_t bytes, size_t alignment, bool zeroed)
Allocate a block from an arena with a caller-specified alignment, returning a void_ptr_expect_t for structured error reporting.
Performs a bump allocation from the arena’s current tail chunk. The returned pointer is aligned to the effective alignment, defined as:
Ifmax(requested_alignment, arena->alignment)
alignmentis zero, the arena’s base alignment is used. Any non-zero alignment must be a power-of-two.If there is insufficient space in the current tail chunk:
In STATIC arenas or when growth is disabled, the call fails.
In DYNAMIC arenas with growth enabled, a new chunk is allocated whose data region is naturally aligned to the effective alignment, and the block is carved from that fresh chunk with no leading pad.
Unlike earlier versions, this function no longer returns NULL. Instead it returns a void_ptr_expect_t:
1. INVALID_ARG- expect.has_value = true → allocation succeeded - expect.has_value = false → allocation failed; expect.u.error contains an ::error_code_t describing whyarenais NULLbytes== 0alignmentis non-zero and not a power-of-two
- Failure conditions and error_code_t values
An expect with has_value = false may contain any of the following errors:
2. ALIGNMENT_ERROR
Arena base alignment is zero
Arena alignment is not a power-of-two
3. ILLEGAL_STATE
arena->tailis NULL (corrupted or uninitialized arena)
4. OPERATION_UNAVAILABLE
Insufficient space in the current tail AND:
arena is STATIC, or
arena->resize == 0 (growth disabled), or
dynamic growth support was compiled out
5. BAD_ALLOC
Dynamic growth is allowed but allocating a new chunk fails
6. LENGTH_OVERFLOW
Internal size arithmetic (pad + bytes) overflows size_t
- Example: Requesting a stricter alignment than the arena’s default
arena_expect_t r = init_dynamic_arena(4096u, true, 4096u, alignof(max_align_t)); if (!r.has_value) { fprintf(stderr, "arena init failed: %d\n", r.u.error); return; } arena_t *a = r.u.value; void_ptr_expect_t e = alloc_arena_aligned(a, 128, 64, false); if (!e.has_value) { fprintf(stderr, "alloc failed: %d\n", e.u.error); return; } void *p = e.u.value; assert(((uintptr_t)p % 64) == 0); free_arena(a);
- Example: Falling back to arena base alignment
arena_expect_t r = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); if (!r.has_value) { fprintf(stderr, "arena init failed: %d\n", r.u.error); return; } arena_t *a = r.u.value; void_ptr_expect_t e2 = alloc_arena_aligned(a, 64, 0, true); if (!e2.has_value) { fprintf(stderr, "alloc failed: %d\n", e2.u.error); return; } void *p = e2.u.value; assert(((uintptr_t)p % alignof(max_align_t)) == 0); free_arena(a);
Note
On success, the arena charges both the payload and the required padding. That is,
tail->lenandarena->lenincrease by (pad + bytes).Note
On first allocation in a freshly grown chunk, no leading pad is added because the chunk’s base is already aligned.
Warning
The returned pointer must not be freed with
free(). Memory is reclaimed only through reset_arena() or free_arena().- Parameters:
arena – Arena to allocate from (must not be NULL).
bytes – Requested payload size in bytes (must be > 0).
alignment – Desired alignment (0 → use arena default). Must be a non-zero power-of-two if non-zero.
zeroed – If true, the returned memory is zero-initialized.
- Returns:
void_ptr_expect_t
On success: expect.has_value = true, expect.u.value = pointer
On failure: expect.has_value = false, expect.u.error = error_code_t
- Pre:
arenais a valid, initialized arena.- Pre:
alignmentis zero or a non-zero power-of-two.- Post:
On success:
The returned pointer is aligned to max(alignment, arena->alignment).
arena->curadvances bybytes(plus pad if allocated from tail).Chunk and arena usage counters are updated accordingly.
realloc_arena_aligned
-
void_ptr_expect_t realloc_arena_aligned(arena_t *arena, void *variable, size_t var_size, size_t realloc_size, bool zeroed, size_t alignment)
Reallocate an object within an arena using a specified alignment.
This function provides a
realloc-like interface adapted to arena semantics. Because arenas cannot grow an allocation in place, reallocation is performed using the following steps:Allocate a new block of
realloc_sizebytes with the requested alignmentalignedusing alloc_arena_aligned().Copy the first
var_sizebytes fromvariableinto the new block.If
zeroedis true, zero-fill the newly added region (bytes [var_size, realloc_size)).Return the new block.
The original memory remains owned by the arena until the arena is reset. It is never individually freed.
If
realloc_sizeis less than or equal tovar_size, the function performs a no-op and returnsvariableunchanged. This avoids unnecessary arena consumption and is consistent with arena-based allocation semantics.Invalid inputs
arenais NULLvariableis NULL
Invalid alignment (checked by alloc_arena_aligned)
alignedis non-zero and not a power-of-twoThe arena’s base alignment is invalid
Arena internal state invalid
arena->tailis NULLCorrupted or uninitialized arena state
Insufficient space and no ability to grow
STATIC arena
Or DYNAMIC arena with growth disabled (
arena->resize== 0)And the requested block does not fit in the current chunk
Dynamic growth attempted but chunk allocation failed
Growth is allowed but alloc_arena_aligned() cannot allocate a sufficiently large chunk (e.g., underlying allocation failure)
- Reasons this function may return NULL
A NULL return indicates that the attempt to allocate a new, larger block failed. Common causes include:
In all cases, only the NULL return value signals failure; no diagnostic information is stored or returned.
// Example: aligned growth of a struct inside an arena typedef struct { float x, y, z; } vec3; arena_expect_t r = init_dynamic_arena(4096u, true, 4096u, alignof(max_align_t)); if (!r.has_value) { // handle arena initialization error via r.u.error return; } arena_t *arena = r.u.value;* // Allocate with 16-byte alignment vec3* v = alloc_arena_aligned(arena, sizeof(vec3), 16, true); size_t old_size = sizeof(vec3); size_t new_size = sizeof(vec3) + 32; // Grow the block with stricter alignment and zero tail vec3* v2 = realloc_arena_aligned(arena, v, old_size, new_size, true, // zero tail 32); // stricter alignment // v2 is 32-byte aligned and contains a zeroed extension region. // v still exists in the arena and is not freed. reset_arena(arena); // Reclaims all arena allocations at once free_arena(arena);
See also
See also
See also
Note
The function never frees memory. Old blocks accumulate until the arena is reset.
Alignment of the new block may differ from the original.
Passing a pointer not allocated from
arenais undefined behavior.Not thread-safe unless externally synchronized.
- Parameters:
arena – Pointer to a valid arena_t. Must not be NULL.
variable – Pointer to an object previously allocated from
arena. Must not be NULL. Ownership is not validated; passing foreign pointers results in undefined behavior.var_size – Size in bytes of the existing object. Must match the size originally allocated for
variable. Incorrect sizes lead to undefined behavior.realloc_size – New size in bytes. If greater than
var_size, a new allocation is made; otherwise, the original pointer is returned unchanged.zeroed – If true and a new block is allocated, only the newly added tail region is zero-filled. The first
var_sizebytes preserve their original values.aligned – Required alignment for the new block. Must be zero (use arena default) or a non-zero power-of-two supported by alloc_arena_aligned(). The returned pointer will satisfy:
(uintptr_t)ptr % aligned == 0
- Returns:
variableitself ifrealloc_size<=var_size.A newly allocated, properly aligned pointer if growth succeeds.
NULLif allocation of the new block fails.
Utility Funcitons
is_arena_ptr
-
bool is_arena_ptr(const arena_t *arena, const void *ptr)
Check whether a pointer falls inside the used region of any chunk in an arena.
This helper answers: “Does
ptrpoint into bytes that have been allocated from the arena (i.e., within the currently used portion of some chunk)?” It first checks the tail chunk (fast path) and then walks the remaining chunks. For each chunk it treats the valid range as:so only memory that has been handed out (including any leading pad charged to the chunk) is considered “in-arena.” Memory in the unused tail of a chunk is not.[ chunk->chunk , chunk->chunk + chunk->len )
- Complexity
Average O(1) for the tail fast-path, O(N) worst-case across N chunks.
- Example
arena_t *a = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); void *p = alloc_arena(a, 128, false); assert_true(is_arena_ptr(a, p)); // inside used region uint8_t *q = (uint8_t*)p + 127; assert_true(is_arena_ptr(a, q)); // still inside the same used span uint8_t *r = (uint8_t*)p + 128; assert_false(is_arena_ptr(a, r)); // exactly 1 past -> outside used span free_arena(a);
Note
This is a geometric test against the arena’s current accounting (len/alloc). It cannot tell whether
ptractually points to the start of a specific allocation—only that it falls somewhere inside the used span of a chunk.Note
Defensive clamping is applied when a chunk appears corrupted (e.g.,
len>alloc). In that case the check usesmin(len, alloc).Note
Overflow guards are used on address arithmetic (e.g., end >= start) and the function returns
falseif a suspicious range is detected.Warning
Not thread-safe against concurrent mutation of the arena without external synchronization. If another thread grows/resets the arena while this function runs, results are undefined.
- Parameters:
arena – Arena to query (must be a valid, initialized arena).
ptr – Pointer to test (may be any address).
- Returns:
trueifptrlies within the used region of any chunk, otherwisefalse.
is_arena_ptr_sized
-
bool is_arena_ptr_sized(const arena_t *arena, const void *ptr, size_t size)
Check whether a [ptr, ptr+size) range lies fully inside the used region of the arena.
This is a stricter variant of
is_arena_ptr:it verifies that the entire half-open interval[ptr, ptr+size) is contained within the used portion of exactly one chunk. It returnsfalseif the span crosses a chunk boundary or extends beyond the used length of a chunk.- Complexity
Average O(1) for the tail fast-path, O(N) worst-case across N chunks.
- Examples
arena_t *a = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); uint8_t *p = (uint8_t*)alloc_arena(a, 128, false); // Fully contained span: assert_true(is_arena_ptr_sized(a, p + 64, 32)); // [p+64, p+96) is within p’s block // One-past-the-end is not contained: assert_false(is_arena_ptr_sized(a, p + 120, 9)); // would extend beyond used region // Force a new chunk, then ask for a cross-chunk span: size_t rem = arena_remaining(a); (void)alloc_arena(a, rem, false); // exhaust tail -> next alloc grows uint8_t *q = (uint8_t*)alloc_arena(a, 64, false); // q is in the new chunk // Span starting near end of first block that would cross into the gap/new chunk: assert_false(is_arena_ptr_sized(a, p + 127, 2)); free_arena(a);
Note
The test uses per-chunk used lengths (like
is_arena_ptr) and clampsusedtomin(len, alloc)for robustness when corruption is suspected.Note
Cross-chunk spans return
falseeven if each byte is “in-arena” individually. The function requires containment within a single chunk’s used interval.Warning
Not thread-safe against concurrent mutation (growth/reset) without external synchronization.
- Parameters:
arena – Arena to query (must be a valid, initialized arena).
ptr – Start of the span to test.
size – Length in bytes of the span (must be > 0).
- Return values:
false – if
arenais NULL,ptris NULL,size== 0, or an overflow inptr+ size is detected.- Returns:
trueif the full span[ptr, ptr+size) is inside a single chunk’s used region, otherwisefalse.
reset_arena
-
bool reset_arena(arena_t *arena, bool trim_extra_chunks)
Reset an arena to an empty state, optionally trimming dynamic chunks.
This function rewinds an arena to a fresh “empty” state. All chunk usage counters are cleared,
arena->lenbecomes 0, and the cursorarena->curis repositioned appropriately.Two reset modes are supported:
Non-trimming reset (
trim_extra_chunks == false)
All chunks remain allocated.
Only usage counters are cleared.
Total capacity (
arena->alloc) and total allocation footprint (arena->tot_alloc) are preserved.The cursor is moved to the start of the current tail chunk (or the head if no tail exists).
Trimming reset (
trim_extra_chunks == true) Applies only to DYNAMIC arenas:
Frees all growth chunks after the head.
Resets the chunk list to a single head chunk.
The cursor is reset to the head’s data region.
arena->allocbecomes exactly the head chunk’s usable size.arena->tot_allocis reduced to reflect the removal of those extra chunks.
STATIC arenas always behave as if
trim_extra_chunks == false; they never free additional chunks because their storage is caller-owned.No other failure conditions exist. A NULL arena is the only error state.
- Reasons this function may return false
arenais NULL.
- Complexity
O(N) over the number of chunks.
When trimming, freeing the extra chunks is also O(N_tail).
- Examples
// Dynamic arena created via arena_expect_t: arena_expect_t exp = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); if (!exp.has_value) { / handle error } arena_t *a = exp.u.value; // Non-trimming reset preserves all allocated chunks: (void)alloc_arena(a, 1024, false); reset_arena(a, false); // usage cleared, capacity preserved // Force the arena to grow: size_t rem = arena_remaining(a); (void)alloc_arena(a, rem, false); // exhaust head (void)alloc_arena(a, 64, false); // causes a new tail chunk // Trimming reset collapses back to a single head chunk: reset_arena(a, true); free_arena(a); // dynamic arena teardown
Note
STATIC arenas never trim: their chunk chain is caller-supplied storage.
When trimming dynamic arenas, each chunk’s footprint was originally added as:
This same quantity is subtracted when the chunk is removed.align_up(sizeof(Chunk), arena->alignment) + chunk->alloc
Note
This function is safe to call on arenas created via init_dynamic_arena() or init_static_arena(). When using init_dynamic_arena(), remember that the initializer returns an
arena_expect_t. Always check.has_valuebefore using the returned arena.Warning
Not thread-safe unless externally synchronized.
- Parameters:
arena – Pointer to an initialized arena_t. Must not be NULL.
trim_extra_chunks – If true and the arena is dynamic, all chunks after the head are freed. If false, all chunks are retained and only usage is cleared.
- Returns:
trueon success.falseifarenais NULL.
save_arena
-
ArenaCheckPoint save_arena(const arena_t *arena)
Capture a lightweight checkpoint of the arena’s current position.
Creates an opaque ArenaCheckPoint value that encodes the arena’s current cursor position, the active tail chunk, and the arena’s global used-byte count at the moment of capture. No memory is copied; only structural addresses and counters are saved.
The returned checkpoint may later be passed to restore_arena() to rewind the arena to this exact allocation point. Checkpoints provide a fast, allocation-free mechanism for implementing temporary “scoped” arena usage.
If
arenais NULL, this function returns a zeroed ArenaCheckPoint.
This value is treated as a
no-op checkpoint by restore_arena() and indicates that no valid state was captured.Apart from a NULL arena, this function has no other failure modes.
Internally (in a private representation), the checkpoint stores:
the pointer to the current tail chunk at the time of capture,
the cursor position within that chunk, and
the arena’s total used length (
arena->len) for diagnostics.
A checkpoint is valid only as long as the tail chunk it references remains part of the arena’s chunk list. Operations that remove or free the recorded chunk invalidate the checkpoint. In particular:
A trimming reset (see reset_arena with
trim_extra_chunks == true) may free tail chunks, invalidating older checkpoints.A dynamic growth does not invalidate a checkpoint—the tail grows, but the checkpoint remains valid unless that tail is later removed.
STATIC arenas never remove chunks, so checkpoints remain valid until an arena-wide reset that overwrites usage information.
Note
Checkpoints do not “pin” or protect memory. They merely store pointers and counters. Removing or freeing the underlying chunk chain invalidates them.
Warning
Not thread-safe unless externally synchronized. Concurrent mutation of the arena while saving or restoring invalidates checkpoints.
- Parameters:
arena – Pointer to an initialized arena_t. If NULL, a zeroed checkpoint is returned.
- Returns:
A nonzero ArenaCheckPoint that can be safely passed to restore_arena() for rewinding.
A zeroed ArenaCheckPoint if
arenais NULL.
restore_arena
-
bool restore_arena(arena_t *arena, ArenaCheckPoint cp)
Rewind an arena to a previously saved checkpoint.
Restores the arena’s allocation state to the point captured by save_arena(). The function rewinds the cursor, tail chunk, and internal usage counters, optionally freeing dynamic growth chunks depending on the arena type.
If
cprepresents an “empty” checkpoint (as returned when save_arena() is called with a NULL arena), this function performs a no-op and returnstrue.A checkpoint is valid only if:
the referenced chunk still exists in the arena’s chunk chain,
the saved cursor lies within that chunk’s data region,
the arena has a valid power-of-two alignment,
the checkpoint was produced from this arena (caller responsibility).
If any of these validations fail, the function returns
falseand the arena remains unchanged.Dynamic arenas
All chunks after the checkpoint’s chunk are freed (each corresponds to a single-growth allocation).
The chunk list is truncated at the checkpoint chunk, which becomes the new tail.
Usage counters (
len,alloc) and footprint counters (tot_alloc) are recomputed or adjusted based on the surviving chain.
Static arenas
No chunks are freed; storage is caller-owned.
The cursor and tail chunk are rewound to the checkpoint.
Usage/footprint accounting is recomputed.
The function returns
falseif any of the following occur:arenaisNULL.The checkpoint’s chunk does not appear in the arena’s current chunk chain (e.g., checkpoint is stale due to trimming or external modifications).
The checkpoint’s saved cursor lies outside the referenced chunk’s data bounds.
The arena’s base alignment is zero or not a power-of-two.
Internal invariants fail (e.g., chunk missing its data pointer).
In all failure cases, the arena is left unchanged.
The checkpoint is empty (treated as a no-op).
The checkpoint is valid and the arena is rewound successfully.
Note
After a successful restore, any pointers obtained from the arena after the saved checkpoint become invalid:
In dynamic arenas, their chunks may have been freed.
In static arenas, their memory is still present but considered “unused” again.
Warning
Checkpoints do not pin memory. If the arena shrinks, trims, or mutates after saving a checkpoint, the checkpoint may become invalid.
Warning
Not thread-safe. External synchronization is required if other threads could interact with the arena concurrently.
- Parameters:
arena – Pointer to a valid arena_t instance. Must not be NULL.
cp – A checkpoint previously returned by save_arena().
- Returns:
trueif the checkpoint is valid and the arena is successfully rewound (or the checkpoint is empty).falseif validation fails or the operation cannot be performed.
arena_stats
-
bool arena_stats(const arena_t *arena, char *buffer, size_t buffer_size)
Render a human-readable snapshot of arena state into a caller buffer.
Writes a multi-line textual report describing the arena’s configuration and current usage. The format is stable enough for logs and diagnostics, e.g.:
Arena Statistics: Type: STATIC Used: 1024 bytes Capacity: 4096 bytes Total (with overhead): 8192 bytes Utilization: 25.0% Chunk 1: 1024/4096 bytes Chunk 2: 0/2048 bytes
If
arenais NULL, the function writes: “Arena: NULL\n” and returnstrue.The function guarantees no truncation: it uses a bounded formatter and returns
falseif the output would exceedbuffer_size. On success, the buffer is NUL-terminated.- Example
char buf[512]; if (!arena_stats(a, buf, sizeof buf)) { perror("arena_stats"); } else { fputs(buf, stdout); }
Note
The report includes:
Type: “STATIC” or “DYNAMIC” (derived from
arena->mem_type).Used:
arena->len(bytes consumed, including per-allocation padding).Capacity:
arena->alloc(sum of usable data bytes across chunks).Total (with overhead):
arena->tot_alloc(implementation accounting of headers + alignment + data).Utilization: 100*Used/Capacity, or “N/A” if capacity is zero.
One line per chunk: “Chunk i: len/alloc bytes”.
Note
Thread-safety: the function does not lock the arena; if other threads mutate the arena concurrently, the snapshot may be transient or inconsistent. Coordinate externally if needed.
- Parameters:
arena – Arena to describe. May be NULL (prints “Arena: NULL\n”).
buffer – Destination character buffer for the report. Must not be NULL.
buffer_size – Size in bytes of
buffer, including space for the terminating NUL. Must be > 0.
- Returns:
trueon success (fully written, NUL-terminated);falseon error.
Getter and Setter Functions
arena_remaining
-
size_t arena_remaining(const arena_t *arena)
Return the number of immediately usable bytes remaining in the arena’s current tail chunk.
Computes the free space available in the tail chunk only, using:
This reflects how many bytes can be allocated without triggering arena growth (in dynamic arenas) and without traversing earlier chunks.remaining = tail->alloc - min(tail->len, tail->alloc)
If
arenais NULL, has no tail chunk, or the tail chunk lacks a valid data region, the function returns 0.- Example
size_t rem = arena_remaining(a); if (rem == SIZE_MAX) { // ascertain if result is real or a NULL pointer was passed to function }
Note
This is not the total free space across all chunks. It reports only what the current tail can satisfy directly.
Note
The reported value does not include any per-allocation alignment padding that may be required by alloc_arena() or alloc_arena_aligned()
.
An allocation of
nbytes succeeds from the tail iff:wherearena_remaining(a) >= pad + n
padis the alignment padding required to bring the cursor up to the arena’s base or requested alignment.Note
For dynamic arenas, if the remaining space is insufficient, a subsequent allocation may still succeed by causing the arena to grow a new chunk.
- Parameters:
arena – Pointer to an initialized arena_t, or NULL.
- Returns:
The number of bytes available in the current tail chunk. Returns 0 on invalid input or if no space remains. Returns SIZE_MAX if a NULL pointer is passed to the function
arena_chunk_count
-
size_t arena_chunk_count(const arena_t *arena)
Count the number of chunks currently linked in the arena.
Walks the chunk list from
headtoNULLand returns the number of chunk headers encountered.- Complexity
O(N) where N is the number of chunks.
Note
For a freshly initialized arena there is always at least one chunk (the head). After
reset_arena(…, trim=true) on a dynamic arena, the count returns to 1.- Parameters:
arena – Arena to query (may be NULL).
- Returns:
The number of chunks in the arena; SIZE_MAX if
arenais NULL.
arena_mtype
-
inline alloc_t arena_mtype(const arena_t *arena)
Return the arena’s memory type as an
alloc_tenum.Converts the arena’s internal 8-bit storage to the public
alloc_t(e.g.,STATICorDYNAMIC).Note
The return value reflects configuration time. Whether a dynamic arena will actually grow also depends on
arena->resize(seetoggle_arena_resizewhen available).- Parameters:
arena – Arena to query.
- Return values:
STATIC – The arena uses caller-supplied storage and cannot grow.
DYNAMIC – The arena was dynamically allocated and may grow (subject to
resizeflag).ALLOC_INVALID – On NULL input or corrupted state.
- Returns:
The
alloc_tvalue for this arena.
arena_size
-
inline size_t arena_size(const arena_t *arena)
Return the total bytes currently consumed from the arena.
This is the logical usage counter aggregated across all chunks. It includes any per-allocation padding that was charged to satisfy alignment, so it may be larger than the sum of the user-requested sizes.
Note
This value never exceeds
arena_alloc(arena).- Parameters:
arena – Arena to query (may be NULL).
- Returns:
Total used bytes (including pad) across the arena; SIZE_MAX if
arenais NULL.
arena_alloc
-
inline size_t arena_alloc(const arena_t *arena)
Return the total usable capacity (in bytes) across all chunks.
This is the sum of
allocfields for every chunk currently linked in the arena. It does not include header or padding bytes and may increase when the arena grows (dynamic) or decrease when trimmed.See also
total_arena_alloc() for capacity including metadata overhead.
- Parameters:
arena – Arena to query (may be NULL).
- Returns:
Total usable capacity; SIZE_MAX if
arenais NULL.
total_arena_alloc
-
inline size_t total_arena_alloc(const arena_t *arena)
Return the arena’s total footprint, including metadata overhead.
For dynamic arenas, this approximates:
align_up(sizeof(arena_t), alignment)
Σ over chunks { align_up(sizeof(Chunk), alignment) + chunk->alloc }
For static arenas initialized over a caller buffer, this typically equals the full buffer size provided at initialization.
Note
This is an accounting value; it is not necessarily the exact number of bytes returned by underlying allocation calls in all configurations.
- Parameters:
arena – Arena to query (may be NULL).
- Returns:
Total bytes attributed to the arena (data + headers + alignment padding as accounted by the implementation); SIZE_MAX if
arenais NULL.
arena_alignment
-
inline size_t arena_alignment(const arena_t *arena)
Return the arena’s base alignment policy in bytes.
The base alignment is applied to the start of the data region in each chunk, and to each allocation’s placement within a chunk (for the default allocator).
Note
A nonzero return is guaranteed to be a power-of-two by construction.
- Parameters:
arena – Arena to query.
- Returns:
The power-of-two alignment (≥ alignof(max_align_t)) used by this arena. Returns SIZE_MAX arena is NULL or uninitialized.
arena_min_chunk_size
-
inline size_t arena_min_chunk_size(const arena_t *arena)
Return the arena’s minimum growth chunk size (bytes).
For dynamic arenas, this value (if nonzero) is used as a floor when computing the size of new growth chunks. For static arenas, this is 0.
- Parameters:
arena – Arena to query.
- Returns:
The minimum growth size in bytes; 0 for static arenas or on error. Returns SIZE_MAX if
arenais NULL or uninitialized.
arena_owns_memory
-
inline bool arena_owns_memory(const arena_t *arena)
Check whether an arena owns its backing memory.
Determines the ownership model of the arena’s memory allocation:
true: Arena owns its memory and free_arena() will release it
false: Arena borrows memory from another source (parent arena or caller buffer)
This is useful for understanding memory lifetime and determining safe operations on an arena.
- Example: Checking ownership before operations
arena_t* parent = init_darena(64*1024, true); arena_t* sub = init_arena_with_arena(parent, 8192, 8); if (arena_owns_memory(parent)) { printf("Parent owns its memory\n"); // Prints } if (!arena_owns_memory(sub)) { printf("Sub-arena borrows from parent\n"); // Prints } // Only owned arenas can have resize toggled if (arena_owns_memory(parent)) { toggle_arena_resize(parent, false); // OK } if (!arena_owns_memory(sub)) { // toggle_arena_resize(sub, true); // Would fail with EPERM } free_arena(parent);
- Example: Ownership by arena type
// Dynamic arena: owns memory arena_t* dyn = init_darena(4096, true); assert_true(arena_owns_memory(dyn)); free_arena(dyn); // Frees all memory // Static arena: owns header, caller owns buffer uint8_t buf[8192]; arena_t* sta = init_sarena(buf, sizeof(buf)); assert_true(arena_owns_memory(sta)); free_arena(sta); // Returns EPERM (caller must manage buf) // Sub-arena: owns nothing arena_t* parent = init_darena(64*1024, true); arena_t* sub = init_arena_with_arena(parent, 8192, 8); assert_false(arena_owns_memory(sub)); free_arena(sub); // Just nulls it out free_arena(parent); // Frees everything including sub's region
Note
For static arenas, owns_memory=true means the arena owns its header structure, but the caller still owns the buffer itself.
Note
This function is useful for:
Determining if toggle_arena_resize() is allowed (requires owns_memory=true)
Understanding cleanup responsibilities
Validating arena relationships before operations
- Parameters:
arena – [in] Pointer to the arena to query. May be NULL.
- Return values:
true – Arena created with init_darena() or init_dynamic_arena() (owns its dynamically allocated memory)
true – Arena created with init_sarena() or init_static_arena() (owns the arena header but NOT the caller’s buffer)
false – Arena created with init_arena_with_arena() (sub-arena borrowing from parent, owns nothing)
- Returns:
true if the arena owns its backing memory; false otherwise.
toggle_arena_resize
-
inline void toggle_arena_resize(arena_t *arena, bool toggle)
Enable or disable geometric growth for a dynamic arena at runtime.
Controls whether
alloc_arena()is allowed to allocate new “growth” chunks when the current tail chunk has insufficient free space.When
toggleistrue, subsequent allocations that do not fit in the current tail may grow the arena (subject to other limits such asmin_chunk).
- Example
arena_t *a = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); // Temporarily freeze growth for a phase that must not allocate extra memory: toggle_arena_resize(a, false); void *p = alloc_arena(a, 8192, false); // will fail with EPERM if it doesn't fit // Re-enable growth: toggle_arena_resize(a, true);
Note
This function does not shrink or free existing chunks. To drop extra chunks and return to a single head chunk, use
reset_arena(arena, true).Note
Has no effect on STATIC arenas.
Warning
Not thread-safe; coordinate with other threads that allocate from or modify the same arena.
- Parameters:
arena – Arena to modify. Must be a valid pointer to a dynamic arena.
toggle –
trueto allow growth;falseto forbid growth.
Arena Context Functions
The following functions provide the arena-backed implementation of the
allocator vtable interface. They adapt an arena_t instance to the generic
allocator API by exposing allocation, reallocation, and bulk deallocation
operations in a consistent form.
Each function corresponds to one of the prototypes defined in the
allocator context system (see Freelist Context Functions), and is used
internally by the arena_allocator() helper to construct a fully
configured allocator_vtable_t for arena-based allocation.
These functions are not intended to be called directly by user code; instead, they are used through the generic allocator interface.
arena_v_alloc
-
static inline void_ptr_expect_t arena_v_alloc(void *ctx, size_t size, bool zeroed)
Vtable adapter for arena allocation.
Allocates a block of memory of size
sizefrom the arena referenced byctx. Ifzeroedis true, the returned memory will be zero-initialized according to the semantics of alloc_arena().This function implements the alloc_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
size – Number of bytes to allocate.
zeroed – Whether the memory should be zero-initialized.
- Return values:
void* – Pointer to a block of at least
sizebytes.NULL – On failure.
arena_v_alloc_aligned
-
static inline void_ptr_expect_t arena_v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Vtable adapter for aligned arena allocation.
Allocates a block of memory of size
sizefrom the arena referenced byctx, with a minimum alignment ofalign. The alignment must be a power of two. Ifzeroedis true, the returned memory will be zero-initialized.This function implements the alloc_aligned_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
size – Number of bytes to allocate.
align – Required alignment (power of two).
zeroed – Whether the memory should be zero-initialized.
- Return values:
void* – Pointer to an aligned block of at least
sizebytes.NULL – On failure.
arena_v_realloc
-
static inline void_ptr_expect_t arena_v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Vtable adapter for arena reallocation.
Resizes a block of memory previously allocated from the arena referenced by
ctx. The block is identified byold_ptrand its previous sizeold_size. The new requested size isnew_size.The contents of the allocation are preserved up to min(
old_size,new_size). Ifzeroedis true and the allocation grows, any newly added region must be zero-initialized according to the semantics of realloc_arena().This function implements the realloc_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
old_ptr – Pointer to the existing allocation (may be NULL).
old_size – Size of the existing allocation.
new_size – Requested new size.
zeroed – Whether any expanded portion must be zero-initialized.
- Return values:
void* – New pointer to the resized allocation on success.
NULL – On failure.
arena_v_realloc_aligned
-
static inline void_ptr_expect_t arena_v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Vtable adapter for aligned arena reallocation.
Resizes a block of memory previously allocated from the arena referenced by
ctx, enforcing a minimum alignment ofalignfor the resulting block. The previous allocation is described byold_ptrandold_size; the new requested size isnew_size.The contents of the allocation are preserved up to min(
old_size,new_size). Ifzeroedis true and the allocation grows, any newly added region must be zero-initialized.This function implements the realloc_aligned_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
old_ptr – Pointer to the existing allocation.
old_size – Size of the existing allocation.
new_size – Requested new size.
zeroed – Whether any expanded portion must be zero-initialized.
align – Required alignment (power of two).
- Return values:
void* – Pointer to the resized, aligned allocation on success.
NULL – On failure.
arena_v_return
-
static inline void arena_v_return(void *ctx, void *ptr)
Vtable adapter for returning an element to an arena allocator.
Arena allocators do not support returning individual blocks to the allocator; all memory is typically released in bulk via a reset or destroy operation. As a result, this function is effectively a no-op.
This function implements the return_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
ptr – Pointer to an allocated block (ignored).
arena_v_free
-
static inline void arena_v_free(void *ctx)
Vtable adapter for freeing an arena allocator.
Releases all memory owned by the arena referenced by
ctx. After this call, the arena must not be used again.This function implements the free_prototype interface for arena-backed allocators.
- Parameters:
ctx – Pointer to an arena_t instance.
Pool Allocator Overview
The pool_t allocator implements a fixed-size block allocation model.
Each pool manages memory divided into uniformly sized blocks, enabling
extremely fast allocation and deallocation of objects of the same size.
Pools may be backed by either static or dynamic arenas, and may optionally
grow by acquiring additional slices of memory from a dynamic arena.
This model is ideal when:
many objects of identical size are frequently allocated and freed
allocation and deallocation must be deterministic and extremely fast
fragmentation must be minimized or eliminated
objects may be recycled using an intrusive free-list
Examples include:
ECS (entity–component–system) component storage
job-system nodes, task descriptors, or scheduler queues
physics or graphics engine object caches
network packet or frame metadata
high-frequency scientific workloads with uniform object shapes
Benefits
Constant-time allocation and free via an intrusive LIFO free-list
No fragmentation, since all blocks share the same size and alignment
High locality, as blocks are carved contiguously from arena slices
Optional reuse of memory via
return_pool_element()Optional growth when backed by a dynamic arena
No external malloc/free — all pool metadata resides inside the arena
Limitations
Block size is fixed at initialization; allocations must fit within
block_sizePools do not support resizing of individual allocations
Dynamically growing pools require a dynamic arena with adequate space
Static pools have fixed capacity and cannot grow
Individual blocks cannot be freed to the system; reclamation occurs only when the entire pool is destroyed via
free_pool()
Static vs. Dynamic Pools
Pools may be constructed in one of two modes:
Static pool — backed by an
arena_tcreated withinit_static_arena(). Capacity is determined entirely by the caller-provided buffer. Growth is permanently disabled, and the pool always prewarms a single contiguous slice during initialization.Dynamic pool — backed by an
arena_tcreated withinit_dynamic_arena(). Additional slices may be allocated on demand as long as the arena’s growth policy allows it. Growth can be toggled at runtime usingtoggle_pool_growth().
Both pool types share the same opaque pool_t structure and the same API.
When the build is configured with STATIC_ONLY, dynamic features are omitted
completely, ensuring compliance with safety-critical environments that forbid
hidden heap allocation paths.
pool_t provides deterministic, high-performance allocation patterns well
suited to real-time and cache-sensitive applications, especially when paired
with arena_t for higher-level memory partitioning.
Data Types
The following data types are used in the implementation of the pool_t
data type.
pool_t
pool_t is an opaque data structure that can not be directly accessed by a user.
This structure contains all of the metadata associated with a pool allocator in
additon, this structure also contains a pointer to the arena_t structures
which contain all memory allocations. Metadata within this struct can be accessed
through getter functions.
struct pool_t {
arena_t* arena; // Backing arena supplying memory (owned or borrowed)
bool owns_arena; // true if this pool allocated and must destroy the arena
size_t block_size; // User-requested block payload size (bytes)
size_t stride; // Actual block size = block_size rounded up to required alignment
size_t blocks_per_chunk; // Number of blocks to allocate in each arena slice (growth granularity)
uint8_t* cur; // Pointer to next free byte in current arena slice (bump pointer)
uint8_t* end; // End of current arena slice (cur == end means next grow needed)
void* free_list; // Head of intrusive free list (pointer stored in first word of freed blocks)
size_t total_blocks; // Total number of blocks ever made available to this pool (including freed)
size_t free_blocks; // Number of blocks currently in free_list (available to reuse)
bool grow_enabled; // If false, pool cannot request new slices from arena (fixed-size mode)
#ifdef DEBUG
pool_slice_t* slices; // Linked list of all memory slices obtained from arena (for debug verification)
#endif
};
PoolCheckPoint
PoolCheckPoint is an opaque data structure that is used to store data
related to a bump allocator. The restore_pool function can extract the data from
this structure to reconstitute a bump allocator. This struct is defined as
PoolCheckPoint in the .c file and PoolCheckPointRep in the .h file.
typedef struct {
void* free_list; // Head of free list at checkpoint time
size_t free_blocks; // Number of blocks in free list
uint8_t* cur; // Bump pointer position in current slice
uint8_t* end; // End of current slice at checkpoint time
size_t total_blocks; // Total blocks available at checkpoint time
#ifdef DEBUG
pool_slice_t* slices; // First slice at checkpoint time (for validation)
#endif
} PoolCheckpointRep;
pool_expect_t
pool_expect_t is an error handling struct to be used in the creation
of pool_t data types to catch and convey errors to a user.
typedef struct {
bool has_value;
union {
pool_t* value;
error_code_t error;
} u;
} pool_expect_t;
Initialization and Memory Management
The following functions can be used to initialize a pool allocator
init_pool_with_arena
-
pool_expect_t init_pool_with_arena(arena_t *arena, size_t block_size, size_t alignment, size_t blocks_per_chunk, bool prewarm_one_chunk, bool grow_enabled)
Initialize a fixed-size memory pool backed by an existing arena.
Creates a pool allocator that dispenses fixed-size blocks from
arena. The pool acquires memory in “chunks” (slices) carved from the arena, optionally maintains an intrusive free list for recycled blocks, and may operate in either fixed-capacity or grow-on-demand mode.The pool header itself is allocated from
arenausing alloc_arena_aligned(), so no externalmallocis performed.This function uses the pool_expect_t result type:
result.has_value== true → success; the pool pointer is available inresult.u.valueresult.has_value== false → failure; the reason is encoded inresult.u.error(an error_code_t)
INVALID_ARG If
arenais NULL,block_size== 0,blocks_per_chunk== 0, oralignmentis non-zero and not a power-of-two.ALIGNMENT_ERROR If the requested alignment exceeds the allocator’s supported limits or violates power-of-two requirements.
BAD_ALLOC If the pool header could not be allocated from
arena.OUT_OF_MEMORY If
prewarm_one_chunkis true and the pool is unable to acquire its initial slice (e.g., arena cannot satisfy the request).
- error_code_t meanings
The function may return the following error_code_t values:
Additional errors may be returned via propagated error_code_t values from alloc_arena_aligned() or the underlying grow path.
See also
alloc_pool(), return_pool_element(), reset_pool(), free_arena()
- Example: Fixed-capacity pool inside a static arena
enum { BUF = 64 * 1024 }; void *buf = aligned_alloc(alignof(max_align_t), BUF); arena_expect_t ar = init_static_arena(buf, BUF, alignof(max_align_t)); assert(ar.has_value); pool_expect_t pr = init_pool_with_arena(ar.u.value, 64, // block size 0, // arena default alignment 128, // blocks per slice true, // prewarm slice false); // fixed capacity if (!pr.has_value) { // handle pr.u.error } pool_t *p = pr.u.value; void_ptr_expect_t expect = alloc_pool(p); if (!expect.has_value) { // Handle error } void *x = expect.u.value; return_pool_element(p, x); // After all 128 blocks are consumed, further allocations fail: for (int i = 0; i < 128; ++i) assert(alloc_pool(p)); assert(!alloc_pool(p)); // no errno; inspect return code only reset_pool(p); free(buf); // static arena storage was caller-owned
- Example: Grow-on-demand pool inside a dynamic arena
arena_expect_t ar = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); assert(ar.has_value); pool_expect_t pr = init_pool_with_arena(ar.u.value, 32, 16, 64, false, // no prewarm true); // allow growth if (!pr.has_value) { // handle pr.u.error } pool_t *p = pr.u.value; void_ptr_expect_t expect = alloc_pool(p); if (!expect.has_value) { // Handle error } void *b = expect.u.value; return_pool_element(p, b); // returned to free list free_arena(ar.u.value); // destroys pool header and all slices
Note
Freed blocks return to an intrusive free list and are reused in LIFO order. Only blocks allocated from this pool may be returned to it.
Note
Memory owned by the pool is not individually freed; all storage is reclaimed only when the underlying arena is reset or destroyed.
- Parameters:
arena – Existing arena to supply memory. Must not be NULL.
block_size – User payload size (in bytes) per block. Must be > 0.
alignment –
Desired alignment for each block (0 → use arena’s default alignment). If non-zero, it must be a power-of-two.
The effective alignment is:
ensuring each freed block can store a next-pointer for the free list.max(alignment, alignof(void*))
blocks_per_chunk – Number of blocks the pool allocates per arena slice. Must be > 0.
prewarm_one_chunk – If true, the pool immediately acquires one slice during initialization so that the first allocation is O(1). If false, the first allocation may invoke a grow operation.
grow_enabled – Whether the pool may request additional slices from the arena when capacity is exhausted. If false, the pool is fixed-capacity and allocations fail once all blocks are consumed.
- Returns:
pool_expect_t
On success:
result.has_value == true pool_t *p = result.u.value;
On failure:
result.has_value == false error_code_t e = result.u.error;
- Pre:
arenawas created via init_static_arena or init_dynamic_arena.alignmentis zero or a power-of-two.
- Post:
On success:
The pool header resides inside
arena.If
prewarm_one_chunkis true:pool_total_blocks(p)==blocks_per_chunk.
init_dynamic_pool
-
pool_expect_t init_dynamic_pool(size_t block_size, size_t alignment, size_t blocks_per_chunk, size_t arena_seed_bytes, size_t min_chunk_bytes, bool grow_enabled, bool prewarm_one_chunk)
Create a grow-capable fixed-size memory pool backed by an internally owned dynamic arena.
This constructor creates a pool allocator whose storage is obtained from a freshly created dynamic arena. The arena is fully owned by the pool and destroyed automatically when the pool is destroyed via
free_pool(). This function will not compile with the code base if theSTATIC_ONLYcompile time flag is invoked.The pool allocates fixed-size blocks (“elements”) of
block_sizebytes, using an effective alignment computed fromalignmentand internal constraints. Blocks are dispensed sequentially from arena slices; freed blocks are returned to an intrusive free list for fast O(1) reuse.The pool may operate in:
Fixed-capacity mode (
grow_enabled == false):
Only the initial slice is available, and it must be prewarmed (i.e.,
prewarm_one_chunkmust be true).Grow-on-demand mode (
grow_enabled == true):
Additional slices are allocated from the arena automatically when the pool becomes exhausted.
The pool header itself is allocated inside the owned arena, so no external
malloc()is used.INVALID_ARG
block_size== 0blocks_per_chunk== 0arena_seed_bytes== 0alignmentis non-zero and not a power-of-twofixed-capacity pool requested (
grow_enabled == false) butprewarm_one_chunk== false
LENGTH_OVERFLOW
Computation of slice size overflows:
stride * blocks_per_chunk
BAD_ALLOC
Dynamic arena creation failed
Allocation of the pool header failed
Prewarm slice allocation failed
FEATURE_DISABLED
Dynamic arenas are disabled at compile time (
ARENA_ENABLE_DYNAMIC == 0).
STATE_CORRUPT
init_dynamic_arena() reported success but returned a NULL arena pointer (should not normally occur; defensive condition).
- Failure conditions (returned as error_code_t)
This function may fail with:
See also
init_pool_with_arena(), alloc_pool(), return_pool_element(), free_pool(), init_dynamic_arena()
- Example (grow-enabled pool)
pool_expect_t r = init_dynamic_pool(32, 16, 64, 4096, 4096, true, false); if (!r.has_value) { printf("Pool creation failed: %d\n", r.u.error); return; } pool_t *p = r.u.value; void_ptr_expect_t expect = alloc_pool(p); if (!expect.has_value) { // Handle error } void *x = expect.u.value; return_pool_element(p, x); // returns block to free list free_pool(p); // destroys pool and its arena
Note
The pool owns its internal arena. Destroying the pool destroys the arena and all memory allocated within it.
Individual blocks returned by alloc_pool() must not be freed with
free().Freed blocks are recycled through an intrusive free list; memory is not returned to the arena until pool destruction.
- Parameters:
block_size – Size in bytes of each user allocation. Must be > 0.
alignment – Desired block alignment.
If 0, defaults to
alignof(max_align_t)The effective alignment is clamped to at least
alignof(void*)to allow storing a next-pointer inside freed blocks.Must be a power-of-two if nonzero.
blocks_per_chunk – Number of blocks allocated in each arena slice. Must be > 0.
arena_seed_bytes – Initial size in bytes for the internal dynamic arena. Must be > 0.
min_chunk_bytes – Minimum slice size requested from the dynamic arena when growth occurs. Passed directly to init_dynamic_arena().
grow_enabled –
If true, the pool may grow by acquiring additional slices.
If false, the pool is fixed-capacity and
requiresprewarm_one_chunk== true.prewarm_one_chunk – If true, the pool eagerly allocates its first slice so the first call to alloc_pool()
is O(1).
If false and the pool is fixed-capacity, initialization fails.
- Returns:
pool_expect_tStructured success/failure result:On success:
(pool_expect_t){ .has_value = true, .u.value = pool_pointer }
On failure:
(pool_expect_t){ .has_value = false, .u.error = <error_code_t> }
- Pre:
ARENA_ENABLE_DYNAMICmust be enabled at build time.
- Post:
On success:
The pool header resides inside a newly created arena.
If
prewarm_one_chunkis true,total_blocks== blocks_per_chunk.The pool is ready for use with alloc_pool() and return_pool_element().
init_static_pool
-
pool_expect_t init_static_pool(void *buffer, size_t buffer_bytes, size_t block_size, size_t alignment)
Initialize a fixed-capacity memory pool backed by a caller-supplied static buffer.
This constructor builds a pool allocator whose storage is carved from a caller-provided memory region
buffer. Internally, it creates a STATIC arena in-place insidebuffervia init_static_arena(), then allocates the pool header and a single slice of pool blocks from that arena.The resulting pool:
Has a fixed capacity determined by the remaining space in
bufferafter arena and pool headers are placed.Never grows: when all blocks are consumed and the free list is empty, further allocations fail.
Reuses freed blocks via an intrusive free list stored inside the blocks themselves.
The caller remains the ultimate owner of
buffer. Destroying the pool (e.g., viafree_pool()) destroys the internal arena and pool metadata, but must not freebufferitself; that remains the caller’s responsibility.NULL_POINTER
bufferis NULL.
INVALID_ARG
buffer_bytes== 0block_size== 0
ALIGNMENT_ERROR
alignmentis non-zero and not a power-of-two.
Errors propagated from init_static_arena()
For example, the caller buffer is too small to hold the arena header, chunk header, and at least one data byte, or normalization of the static arena layout fails. These appear as whatever init_static_arena() encodes in
arena_expect_t.u.error.
Errors from alloc_arena_aligned() when allocating:
the pool header, or
the single pool slice:
Typically OUT_OF_MEMORY if there is insufficient usable space remaining in the static arena to satisfy the request.
OUT_OF_MEMORY
There is enough space for the arena and pool header, but the computed capacity allows zero blocks (i.e., the slice cannot contain even one block at the chosen stride).
STATE_CORRUPT
Defensive: init_static_arena() reports success but returns a NULL arena pointer.
- Failure conditions (error_code_t)
See also
init_dynamic_pool(), init_pool_with_arena(), alloc_pool(), return_pool_element(), free_pool(), init_static_arena()
- Example
enum { BUF = 64 * 1024 }; // 64 KiB caller-owned buffer static uint8_t buf[BUF]; pool_expect_t r = init_static_pool(buf, BUF, 64, 0); // use default if (!r.has_value) { printf("static pool init failed: %d\n", r.u.error); return; } pool_t *p = r.u.value; void_ptr_expect_t expect = alloc_pool(p); if (!expect.has_value) { // Handle error } void *x = expect.u.value; expect = alloc_pool(p); if (!expect.has_value) { // Handle error } void *y = expect.u.value; // ok return_pool_element(p, x); // block reclaimed for reuse // When done: free_pool(p); // destroys pool and its internal arena // 'buf' remains caller-owned and is not freed here.
Note
This pool is strictly fixed-capacity: no additional slices will ever be allocated. alloc_pool() will eventually fail once all blocks are allocated and the free list is empty.
Individual blocks must not be passed to
free(). They are reclaimed only via return_pool_element() or when the pool (and its arena) are torn down.The underlying buffer
bufferis not freed by this API; its lifetime is entirely managed by the caller.
- Parameters:
buffer – Caller-supplied backing storage for the static arena and pool. Must point to at least
buffer_bytesbytes of writable memory and remain valid for the entire lifetime of the pool.buffer_bytes – Total size in bytes of
buffer. Must be large enough to contain:the arena header,
the pool header,
and at least one usable block at the computed stride.
block_size – Size of the user payload for each block (in bytes). Must be > 0.
alignment – Desired alignment for pool blocks.
If 0, defaults to
alignof(max_align_t).The effective alignment is clamped to at least
alignof(void*)so that freed blocks can store a next-pointer.Must be a power-of-two if non-zero; otherwise the function fails with ALIGNMENT_ERROR.
- Returns:
pool_expect_tStructured success/failure result:On success:
(pool_expect_t){ .has_value = true, .u.value = pool_pointer }
On failure:
(pool_expect_t){ .has_value = false, .u.error = <error_code_t> }
- Pre:
bufferpoints to at leastbuffer_bytesbytes of writable memory.The lifetime of
bufferstrictly dominates the lifetime of the pool.
- Post:
On success:
A STATIC arena is constructed in-place inside
buffer.The pool header resides inside that arena.
A single slice covering
total_blocks* stride bytes is carved out and ready to serve allocations.
alloc_pool
-
inline void_ptr_expect_t alloc_pool(pool_t *pool, bool zeroed)
Allocate a fixed-size block from a memory pool.
Retrieves a block from the pool using the following strategy:
Free-list reuse (fast path): If the pool contains previously freed blocks, the most recently released block is popped and returned immediately.
Carve from current slice: If the active slice still has unused space (
cur < end), the next block is carved from the slice and returned.Grow-on-demand (dynamic pools only): If the active slice is full and
grow_enabledis true, a new slice is requested from the pool’s backing arena, and the block is allocated from that fresh slice.
If
zeroedis true, the block’s user-visible region (block_sizebytes) is zero-filled. Any stride padding is left uninitialized.This function does not use errno. Errors are reported through a void_ptr_expect_t return value.
Success:
expect.has_value == true expect.u.value -> pointer to allocated block
Failure:
expect.has_value == false expect.u.error == one of: NULL_POINTER — @p pool is NULL CAPACITY_OVERFLOW — pool exhausted and growth disabled BAD_ALLOC — arena failed to grow a new slice STATE_CORRUPT — inconsistent state after growth
See also
init_pool_with_arena(), init_static_pool(), pool_grow(), return_pool_element()
- Failure Conditions
Failure occurs when:
poolis NULL → NULL_POINTERNo blocks remain and growth is disabled → CAPACITY_OVERFLOW
Growth is enabled but arena allocation fails → BAD_ALLOC
Growth succeeded but internal state invalid → STATE_CORRUPT
- Example
// Create a dynamic arena and pool arena_expect_t ar = init_dynamic_arena(4096u, true, 4096u, alignof(max_align_t)); if (!ar.has_value) { fprintf(stderr, "Arena init failed: %d\n", ar.u.error); return; } arena_t *arena = ar.u.value; // Initialize a pool where each block holds 32 bytes pool_expect_t pr = init_pool_with_arena(arena, 32, // block size 0, // alignment = arena default 64, // blocks per chunk true, // prewarm first slice true); // allow dynamic growth if (!pr.has_value) { fprintf(stderr, "Pool init failed: %d\n", pr.u.error); free_arena(arena); return; } pool_t *pool = pr.u.value; // Allocate a zeroed block void_ptr_expect_t b1 = alloc_pool(pool, true); if (!b1.has_value) { fprintf(stderr, "alloc_pool failed: %d\n", b1.u.error); return; } // Allocate a second block void_ptr_expect_t b2 = alloc_pool(pool, false); // Return first block to the free list return_pool_element(pool, b1.u.value); // Next allocation will reuse the returned block (LIFO behavior) void_ptr_expect_t reused = alloc_pool(pool, false); assert(reused.has_value && reused.u.value == b1.u.value); // Cleanup free_arena(arena); // destroys the pool and arena
Note
The returned block must be released using return_pool_element(), not
free().Memory is reclaimed only when the underlying arena is reset or destroyed.
Blocks are reused in LIFO order (stack behavior).
- Parameters:
pool – Pointer to a valid pool_t instance. Must not be NULL.
zeroed – If true, the returned block’s payload is zero-initialized.
- Returns:
void_ptr_expect_t describing success or failure:
return_pool_element
-
inline void return_pool_element(pool_t *pool, void *ptr)
Return a previously allocated block to the pool’s free list.
Places
ptronto the pool’s intrusive LIFO free list in O(1) time. The block must have been obtained from the samepoolviaalloc_pool()and must not already be on the free list (no double free).In debug builds, the internal helper validates that
ptr:lies within the backing arena’s used region,
is aligned to the pool’s
stride,belongs to one of this pool’s recorded slices. Failing any of these conditions triggers an assertion.
See also
- Example
arena_t* a = init_dynamic_arena(4096, true, 4096, alignof(max_align_t)); pool_t* p = init_pool_with_arena(a, 64, 0, 32, true, true); void* x = alloc_pool(p, false); void* y = alloc_pool(p, false); return_pool_element(p, x); // x goes to the head of the free list void* z = alloc_pool(p, false); // z == x (LIFO reuse) free_arena(a); // destroys all pool memory
Note
This function does not set
errnoand never allocates.Note
The free list is intrusive: the first
sizeof(void*)bytes of a freed block are repurposed to store the next pointer until reallocated.Warning
Passing a pointer not obtained from this pool, a pointer belonging to another pool, or double-freeing a block is undefined behavior in release builds and will corrupt the free list. Debug builds attempt to catch these errors via assertions.
- Parameters:
pool – Target pool (may be NULL; treated as a no-op).
ptr – Pointer previously returned by
alloc_pool()(may be NULL; no-op).
- Returns:
void
- Pre:
poolwas initialized viainit_pool_with_arena().- Pre:
ptris either NULL or a live block frompool.- Post:
The block is available for immediate reuse by
alloc_pool()and will typically be the next block returned (LIFO behavior).
reset_pool
-
void reset_pool(pool_t *pool)
Reset a pool to its initial empty state without releasing arena memory.
Clears the pool’s internal bookkeeping — including its free list, bump pointer state, and block counters — effectively returning the pool to a freshly-initialized state without reclaiming any memory from the underlying arena. The arena-owned slices previously used by the pool remain reserved until the arena is reset or destroyed.
After
reset_pool(), subsequent calls toalloc_pool()behave as if no blocks have been allocated yet: if the pool was prewarmed or previously grew, the first allocation after reset will trigger growth again unless the initial slice is still present andcur!=end.In debug builds, per-slice debug metadata is cleared; the slice headers themselves remain in arena memory and will be reclaimed if and when the arena is reset or freed.
- Example
arena_t* a = init_dynamic_arena(16 * 1024, true, 4096, alignof(max_align_t)); pool_t* p = init_pool_with_arena(a, 64, 0, 64, true, true); void* x = alloc_pool(p, false); void* y = alloc_pool(p, false); return_pool_element(p, x); assert(pool_free_blocks(p) == 1); reset_pool(p); // invalidate x, y, and all other outstanding blocks assert(pool_free_blocks(p) == 0); assert(pool_total_blocks(p) == 0); void* z = alloc_pool(p); // triggers a fresh slice allocation if needed free_arena(a); // pool header + slices reclaimed
Note
This does not free or reuse arena storage. It only resets the pool’s bookkeeping. Call
reset_arena()orfree_arena()to reclaim memory globally.Warning
Any outstanding blocks obtained from
alloc_pool()become invalid afterreset_pool(). Using them after reset is undefined behavior.- Parameters:
pool – Pool to reset (may be
NULL; treated as a no-op).
- Post:
pool_free_blocks(pool)== 0pool_total_blocks(pool)== 0Free list is empty
Bump state is cleared (
cur==end== NULL)
free_pool
-
void free_pool(pool_t *pool)
Destroy a pool and release its resources.
Frees the memory associated with a pool allocator. If the pool owns its backing arena (created via a convenience constructor that allocates the arena), then this function will call
free_arena()and all memory for the pool and its slices is reclaimed.If the pool does not own the arena (i.e., the arena was supplied by the caller), the pool object is invalidated but the arena is left untouched. This allows a pool to be used as a transient allocator layered on top of a longer-lived arena.
After calling
free_pool(), thepoolpointer must not be used again.See also
init_pool_with_arena(), alloc_pool(), return_pool_element(), reset_pool(), reset_arena(), free_arena()
- Example: Borrowed arena
arena_t* a = init_dynamic_arena(8192, true, 4096, alignof(max_align_t)); pool_t* p = init_pool_with_arena(a, 64, 0, 32, false, false); void* x = alloc_pool(p, false); return_pool_element(p, x); free_pool(p); // pool invalid, arena 'a' still valid void* y = alloc_arena(a, 128, false); // still works free_arena(a);
- Example: Pool owns arena
pool_t* p = init_pool_dynamic(8192, 64, 0, 32, true); void* x = alloc_pool(p, true); free_pool(p); // frees arena and pool header // p is now invalid
Note
This function never sets
errno.Warning
Outstanding allocations obtained from
alloc_pool()become invalid afterfree_pool(). Accessing them is undefined behavior.Warning
When a pool does not own its arena, this function does not return arena memory to the system. Use
reset_pool()to reuse the arena space, orreset_arena()/free_arena()when the arena itself should be reclaimed.- Parameters:
pool – Pool to destroy (may be
NULL, treated as no-op).
- Pre:
poolwas initialized viainit_pool_with_arena()or a pool creation helper.- Post:
If
pool->owns_arena== true, its arena and slices are freed.- Post:
If
pool->owns_arena== false, pool metadata is invalidated but the arena remains usable for other allocators.
save_pool
-
PoolCheckPoint save_pool(const pool_t *pool)
Capture the current state of a pool for later restoration.
Creates a lightweight checkpoint of the pool’s allocation state. The checkpoint can later be passed to restore_pool() to rewind allocations to this point in time.
See also
- Example: Transactional allocation
pool_t* pool = init_dynamic_pool(64, 8, 128, 4096, 4096, true, true); PoolCheckpoint cp = save_pool(pool); // Allocate some objects for a transaction void* obj1 = alloc_pool(pool, false); void* obj2 = alloc_pool(pool, false); if (!try_operation(obj1, obj2)) { // Operation failed - restore to checkpoint restore_pool(pool, cp); // obj1 and obj2 are now invalid } else { // Success - keep the allocations } free_pool(pool);
Note
Checkpoints do not pin memory. They store only metadata (pointers and counters) for later validation and restoration.
Note
After restoring to a checkpoint, any pointers returned by alloc_pool() after the save point become invalid and must not be used.
Warning
Not thread-safe. If multiple threads access the pool while a checkpoint is being saved or restored, behavior is undefined.
- Parameters:
pool – [in] Pointer to the pool to checkpoint. May be NULL.
- Returns:
Opaque PoolCheckpoint value. If
poolis NULL, returns an empty checkpoint that restore_pool() will treat as a no-op.
restore_pool
-
bool restore_pool(pool_t *pool, PoolCheckPoint cp)
Restore a pool to a previously saved checkpoint state.
Rewinds the pool’s allocation state to the point captured by save_pool(). Any blocks allocated after the checkpoint become available for reuse (returned to the bump pointer region or free list, depending on implementation).
See also
- Example: Nested checkpoints
pool_t* pool = init_dynamic_pool(32, 4, 64, 4096, 4096, true, true); PoolCheckpoint cp1 = save_pool(pool); void* a = alloc_pool(pool, false); PoolCheckpoint cp2 = save_pool(pool); void* b = alloc_pool(pool, false); void* c = alloc_pool(pool, false); // Restore to cp2: only 'a' remains allocated restore_pool(pool, cp2); // Restore to cp1: all allocations reclaimed restore_pool(pool, cp1); free_pool(pool);
Note
After successful restoration:
All blocks allocated after the checkpoint are reclaimed
Pointers obtained after save_pool() must not be used
The free list may contain blocks that existed at save time
Statistics (total_blocks, free_blocks) are updated
Note
Empty checkpoints (from save_pool(NULL)) are treated as successful no-ops.
Warning
This function does NOT free memory back to the arena in dynamic pools. Slices allocated after the checkpoint remain allocated but become available for bump allocation.
Warning
Not thread-safe. External synchronization required if multiple threads access the pool.
- Parameters:
pool – [inout] Pointer to the pool to restore. Must not be NULL.
cp – [in] Checkpoint previously returned by save_pool().
- Return values:
true – Pool successfully restored to checkpoint state.
false – Restoration failed.
false, for – the following reasons
poolis NULLCheckpoint is invalid or corrupted
Checkpoint’s bump pointer is out of bounds
Pool has been reset or freed since checkpoint was saved
- Returns:
true on success, false on failure.
pool_stats
-
bool pool_stats(const pool_t *pool, char *buffer, size_t buffer_size)
Format a human-readable summary of a pool into
buffer.The report includes:
Kind (STATIC/DYNAMIC) from underlying arena
Ownership (whether pool owns the arena object)
Growth policy (enabled/disabled)
Block size, stride, and effective alignment
Totals: total/free/in-use/bump-remaining blocks
Derived utilization (in-use / total)
Optional: slice list with [start,end) when DEBUG and slices are tracked
Gett and Setter Functions
The following functions can be used to access attributes of the
pool_t data structure.
pool_block_size
-
inline size_t pool_block_size(const pool_t *pool)
Return the user-visible block size for a pool.
Reports the size in bytes of the usable payload of each block returned by
alloc_pool(). This is the value originally passed asblock_sizetoinit_pool_with_arena().Note that the actual internal footprint of each block may be larger than
block_sizedue to alignment and intrusive free-list requirements; seepool_stride()for the full internal size.See also
pool_stride(), alloc_pool(), return_pool_element(), init_pool_with_arena(), pool_total_blocks(), pool_free_blocks()
- Example
size_t user_size = pool_block_size(p); printf("Pool block payload size: %zu bytes\n", user_size);
- Parameters:
pool – Pool to query (must be non-NULL).
- Returns:
The payload size in bytes, or
0ifpoolisNULL
pool_stride
-
inline size_t pool_stride(const pool_t *pool)
Obtain the stride (internal block size) of a pool.
Returns the number of bytes consumed by each allocation made from the pool, including any padding required to satisfy the pool’s alignment guarantee and to hold the intrusive next-pointer when blocks are returned to the free list. This value may be larger than the user-requested
block_size.See also
init_pool_with_arena(), alloc_pool(), return_pool_element(), pool_total_blocks(), pool_free_blocks()
- Example
size_t stride = pool_stride(p); printf("Each pool block consumes %zu bytes\n", stride);
Note
The stride is
max(block_size, sizeof(void*)), rounded up to the pool’s effective alignment.- Parameters:
pool – Pool to query (must be non-NULL).
- Returns:
The stride in bytes, or
0ifpoolisNULL
pool_total_blocks
-
inline size_t pool_total_blocks(const pool_t *pool)
Return the total number of blocks ever made available by the pool.
Counts all blocks that have been provisioned from arena slices, regardless of whether they are currently allocated or returned to the free list. This value increases when the pool prewarms or grows, and is reset to zero by
reset_pool().- Example
printf("Blocks created so far: %zu\n", pool_total_blocks(p));
Note
This does not necessarily equal the number of currently usable blocks if
reset_pool()was called without resetting the backing arena.- Parameters:
pool – Pool to query (must be non-NULL).
- Returns:
Total number of blocks the pool has made available, or
0ifpoolisNULL
pool_free_blocks
-
inline size_t pool_free_blocks(const pool_t *pool)
Return the number of blocks currently available for reuse.
Counts the number of blocks on the pool’s intrusive free list. Each call to
return_pool_element()increments this value, and each call toalloc_pool()that reuses a freed block decrements it. This counter is reset to zero byreset_pool().- Example
printf("Currently free blocks: %zu\n", pool_free_blocks(p));
- Parameters:
pool – Pool to query (must be non-NULL).
- Returns:
Number of free blocks, or
0ifpoolisNULL
pool_alignment
-
inline size_t pool_alignment(const pool_t *pool)
Return the effective alignment used for blocks in this pool.
Returns the alignment value applied when allocating blocks from the pool. This is derived from the pool’s requested alignment and the minimum required alignment for storing an intrusive free-list pointer.
See also
Note
The returned alignment is always >= alignof(void*).
Note
Does not modify errno on success.
- Parameters:
pool – Valid pool handle.
- Returns:
The pool’s alignment in bytes on success.
pool_bump_remaining_blocks
-
size_t pool_bump_remaining_blocks(const pool_t *pool)
Return the number of blocks still available in the active bump slice.
Computes how many blocks remain before the pool must grow (dynamic pools), or before further allocation fails (static pools).
See also
Note
This value counts only unused bump-region blocks. Additional free blocks may exist on the free list; see
pool_free_blocks().Note
Does not trigger any growth; this is a pure query.
- Parameters:
pool – Valid pool handle.
- Returns:
Remaining blocks available in the current slice.
pool_in_use_blocks
-
inline size_t pool_in_use_blocks(const pool_t *pool)
Return the number of blocks currently in use (not on the free list).
See also
Note
Computed as:
pool_in_use = pool_total_blocks(pool) - pool_free_blocks(pool)
- Parameters:
pool – Valid pool handle.
- Returns:
Number of allocated (live) blocks owned by the caller.
pool_owns_arena
-
inline bool pool_owns_arena(const pool_t *pool)
Return whether this pool owns the underlying arena.
See also
Warning
When
false, destroying the pool must not deallocate the arena. The caller remains responsible for arena lifetime management.- Parameters:
pool – Valid pool handle.
- Returns:
trueif the pool owns its arena (created via init_dynamic_pool() or init_static_pool()),falseotherwise.
pool_grow_enabled
-
inline bool pool_grow_enabled(const pool_t *pool)
Query whether the pool is currently allowed to grow.
See also
toggle_pool_growth(), alloc_pool()
Note
Growth may still fail at runtime if the arena is exhausted or if its internal resize policy disallows expansion.
- Parameters:
pool – Valid pool handle.
- Returns:
trueif the pool may allocate new slices from the arena.- Returns:
falseif the pool is fixed-capacity.
pool_mtype
-
inline alloc_t pool_mtype(const pool_t *pool)
Return the memory type of the underlying arena.
Indicates whether the arena backing the pool uses STATIC or DYNAMIC allocation semantics, matching
alloc_tfrom the arena API.See also
Note
Does not set errno. If
poolis NULL, behavior is implementation-defined; you may choose to returnSTATICas a safe default or assert in debug builds.- Parameters:
pool – Valid pool handle.
- Return values:
a – valid
alloc_tvalue.- Returns:
STATICorDYNAMICdepending on the arena’s memory type.
pool_size
-
inline size_t pool_size(const pool_t *pool)
Get the total number of bytes currently in use.
Returns the sum of all block payload bytes that are allocated and not on the free list. This does NOT include:
Alignment padding between blocks (stride - block_size)
Blocks available in the bump region
Blocks on the free list
- Parameters:
pool – [in] Pointer to the pool to query.
- Returns:
Number of payload bytes in use, or 0 on error.
pool_alloc
-
inline size_t pool_alloc(const pool_t *pool)
Get the total pool capacity in bytes.
Returns the sum of all block payload bytes that have been made available to this pool (including in-use, free, and bump regions). This represents the usable capacity, NOT the total memory footprint.
To get the actual memory footprint including alignment padding:
size_t footprint = pool_total_blocks(pool) * pool_stride(pool);
- Parameters:
pool – [in] Pointer to the pool to query.
- Returns:
Total capacity in payload bytes, or 0 on error.
pool_footprint
-
inline size_t pool_footprint(const pool_t *pool)
Get the total memory footprint of the pool in bytes.
Returns the total bytes consumed by all pool blocks, including alignment padding. This is the actual memory taken from the arena.
Formula: total_blocks × stride
Compare with pool_alloc() which returns only usable payload bytes.
- Parameters:
pool – [in] Pointer to the pool to query.
- Returns:
Total memory footprint in bytes, or 0 on error.
is_pool_ptr
-
bool is_pool_ptr(const pool_t *pool, const void *ptr)
Verify whether a pointer belongs to a given pool.
Determines if
ptrfalls within memory currently owned bypool. In debug builds, this check validates against the pool’s tracked slices. In release builds, it falls back to an arena-level containment check.Note
In release builds, this function cannot distinguish between two pools sharing the same arena; it only verifies arena-level containment.
Warning
This does not validate alignment or active allocation state; it only checks spatial containment.
- Parameters:
pool – Pointer to a valid pool_t.
ptr – Pointer to check.
- Return values:
true – If
ptrlies within the pool’s managed memory.false – If
ptris outside the pool or invalid.
pool_owns_memory
-
inline bool pool_owns_memory(const pool_t *pool)
Query whether a pool owns the underlying arena memory.
A pool may either:
Own its arena — as in the case of
init_dynamic_pool()orinit_static_pool()where the pool internally creates or embeds anarena_t, orBorrow an arena — as in
init_pool_with_arena(), where the caller provides an existing arena to carve the pool header and slices from.
This function distinguishes those cases:
If the pool owns its arena, calling
free_pool(pool)will free the arena and all memory associated with the pool.If the pool borrows its arena, calling
free_pool(pool)will not free the arena; the pool header is invalidated, but the caller remains responsible for destroying the arena.
// Example: pool_t *p1 = init_dynamic_pool(64, 0, 16, 8192, 4096, true, true); assert(pool_owns_memory(p1) == true); // owns arena arena_t *a = init_dynamic_arena(8192, true, 4096, 0); pool_t *p2 = init_pool_with_arena(a, 64, 0, 16, true, true); assert(pool_owns_memory(p2) == false); // borrows arena free_pool(p1); free_pool(p2); // does NOT free 'a' free_arena(a);
- Parameters:
pool – Pointer to a valid
pool_tinstance.
- Returns:
trueif the pool owns its arena;falseif it borrows an external arena or ifpool == NULL.
Function Like Macros
alloc_pool_type
-
alloc_pool_type(T, pool)
Allocate one object of type
Tfrom a memory pool.This macro wraps alloc_pool and casts the result to
T*, improving readability and reducing repetitive casting in caller code.Obtains one block from
poolviaalloc_pool(pool).Casts the result to
T*.Returns
NULLon failure.
typedef struct { float x, y, z; } vec3; pool_t* p = init_dynamic_pool(sizeof(vec3), 0, 64, 8192, 4096, true, true); vec3* v = alloc_pool_type(vec3, p); if (!v) { perror("alloc_pool_type"); exit(EXIT_FAILURE); } v->x = 1; v->y = 2; v->z = 3; return_pool_element(p, v); free_pool(p);
Note
Caller must initialize the returned object.
Memory must be returned with return_pool_element.
Only valid when the pool’s block size ≥
sizeof(T).
- Parameters:
T – The C type to allocate (e.g.,
MyStruct)pool – Pointer to an initialized
pool_tallocator
- Returns:
T*on success, orNULLon failure.
Pool Context Functions
The following functions provide the pool-backed implementation of the
allocator vtable interface. They adapt a pool_t instance to the generic
allocator API by exposing allocation, deallocation, and element-return
operations in a consistent form.
Each function corresponds to one of the prototypes defined in the
allocator context system (see Freelist Context Functions), and is used
internally by the pool_allocator() helper to construct a fully
configured allocator_vtable_t for pool-based allocation.
These functions are not intended to be called directly by user code; instead, they are used through the generic allocator interface.
pool_v_alloc
-
static inline void_ptr_expect_t pool_v_alloc(void *ctx, size_t size, bool zeroed)
Allocate a block from a pool via the generic allocator interface.
This function adapts alloc_pool() to the allocator vtable model. The caller supplies a pool via
ctx, and requests a block of sizesize.Pool allocators dispense fixed-size blocks. Therefore, the request is valid only if
sizeis less than or equal to the pool’s configured block size.On success, this returns a void_ptr_expect_t with:
has_value= true,u.value= pointer to a valid pool block.
On failure,
has_valueis false andu.errordescribes the reason:NULL_POINTER —
ctxwas NULL.INVALID_ARG — requested
sizeexceeds pool block capacity.BAD_ALLOC — pool is exhausted and cannot grow.
- Parameters:
ctx – Pointer to a pool_t instance.
size – Number of bytes requested (must not exceed pool block size).
zeroed – If true, the returned block is zero-initialized.
- Returns:
A void_ptr_expect_t containing either an allocated pointer or an error code.
pool_v_alloc_aligned
-
static inline void_ptr_expect_t pool_v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Allocate a block from a pool with an alignment request.
Pool allocators have a fixed alignment determined at initialization. Therefore
alignis ignored, and alignment errors cannot occur here as long assizefits in a single pool block.Behavior is identical to pool_v_alloc(), except that
alignis ignored.- Parameters:
ctx – Pointer to a pool_t instance.
size – Number of bytes requested (must not exceed pool block size).
align – Ignored; pool alignment is fixed.
zeroed – If true, the returned block is zero-initialized.
- Returns:
A void_ptr_expect_t with either:
valid block pointer, or
error: NULL_POINTER, INVALID_ARG, or BAD_ALLOC.
pool_v_realloc
-
static inline void_ptr_expect_t pool_v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Reallocate memory from a pool via the vtable interface.
Memory pools cannot resize blocks. Therefore, reallocation follows strict rules:
If
old_ptris NULL → returns an error (INVALID_ARG).If
new_sizeexceeds the pool’s block size → returns CAPACITY_OVERFLOW.Otherwise, the reallocation is a no-op: the original pointer is returned unchanged.
No copying occurs, and no new block is allocated.
- Parameters:
ctx – Pointer to a pool_t instance.
old_ptr – Existing block allocated from the pool (must not be NULL).
old_size – Ignored (block size is fixed).
new_size – Requested new size (must not exceed block size).
zeroed – Ignored (no new memory is created).
- Returns:
A void_ptr_expect_t with:
success:
old_ptrunchanged,failure: NULL_POINTER, INVALID_ARG, or CAPACITY_OVERFLOW.
pool_v_realloc_aligned
-
static inline void_ptr_expect_t pool_v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Reallocate memory from a pool with an alignment request.
Alignment does not affect reallocation since pools cannot resize blocks and have fixed alignment. The semantics mirror pool_v_realloc():
old_ptrmust not be NULL,new_sizemust not exceed pool block size,otherwise, return
old_ptrunchanged.
alignis ignored.- Parameters:
ctx – Pointer to a pool_t instance.
old_ptr – Existing block allocated from the pool.
old_size – Ignored.
new_size – New size (must not exceed pool block size).
zeroed – Ignored.
align – Ignored.
- Returns:
A void_ptr_expect_t containing either
old_ptror an error code.
pool_v_return
-
static inline void pool_v_return(void *ctx, void *ptr)
Return a previously allocated block to the pool.
This inserts
ptrinto the pool’s intrusive free list for later reuse.Invalid inputs result in a silent no-op (consistent with MISRA and allocator-vtable expectations). No error is returned.
Note
This function never frees memory; it only recycles blocks.
- Parameters:
ctx – Pointer to the pool_t instance.
ptr – Block previously returned by alloc_pool(). Must belong to this pool; the function does not validate ownership.
pool_v_free
-
static inline void pool_v_free(void *ctx)
Destroy a pool via the vtable interface.
This invokes free_pool() on the underlying pool instance. The pool must have been allocated using a pool initializer such as init_pool_with_arena() or init_dynamic_pool().
Invalid input is ignored by design (no error reporting).
Note
If the pool owns its arena, that arena is destroyed as well.
- Parameters:
ctx – Pointer to the pool_t instance to destroy.
Freelist Overview
The freelist_t allocator implements a returnable-block allocation model.
Unlike the arena_t bump allocator—where memory is
only reclaimed via a full reset—the freelist supports individual frees
while still maintaining high performance and predictable behavior.
Memory is managed as a contiguous region divided into variable-sized blocks. Each block is either:
allocated, containing a header that records its total size and internal alignment offset, or
free, tracked in a singly linked free list ordered by address.
This layout allows the allocator to return memory to the free list and efficiently coalesce adjacent free blocks, reducing external fragmentation.
When to Use a Freelist
The freelist allocator is ideal when:
allocations and deallocations are intermixed
memory reuse is essential
predictable fragmentation behavior is required
you want deterministic, bounded allocation times without relying on
malloc
Common use cases:
transient or reusable object pools
high-frequency allocation patterns in game or simulation engines
embedded systems needing predictable memory footprints
dynamic buffers inside larger memory arenas
Benefits
Individual deallocation — Unlike an arena, objects can be returned and reused.
Predictable performance — First-fit allocation runs in bounded linear time.
Coalescing — Adjacent free blocks automatically merge, limiting external fragmentation.
Contiguous memory region — No per-allocation heap calls, no hidden system overhead.
Compatible with arenas — Freelist can be layered atop an
arena_tfor full stack control.
Limitations
Potential fragmentation The freelist updates internal block boundaries dynamically. Fragmentation may occur, though automatic coalescing keeps it bounded.
Internal fragmentation Every allocation includes a small header that stores:
total block size
user pointer offset (for alignment)
This means each allocation consumes slightly more memory than requested. Alignments > struct alignment may increase padding.
External fragmentation When free blocks cannot be merged—because allocated blocks lie between them—the available space becomes split across multiple regions. This is inherent to any free-list allocator.
More overhead than an arena Allocation is slower than bump allocation, though significantly faster than
malloc.
Static vs. Dynamic Freelist
Two initialization paths exist:
Static freelist — wraps a user-provided buffer
Dynamic freelist — allocates its own backing
arena_t
Dynamic freelists are available only when the build is compiled with:
ARENA_ENABLE_DYNAMIC = 1
This ensures MISRA-compliant builds (STATIC_ONLY) completely exclude heap usage.
Reset Behavior
Calling reset_freelist(fl) restores the allocator to its initial state:
all blocks become free
the free list is replaced by one large free block
accounting returns to zero
This mirrors the arena’s reset_arena(), but the freelist preserves its
ability to perform individual allocations afterward.
Data Types
The following are data structures and derived data types used in the c_allocator.h
and c_allocator.c files to support the freelist_t data type.
freelist_t
The freelist_t data type is the fundamental structure that enables the
freelist allocator.
struct freelist_t {
free_block_t* head; // Head of free list - accessed first in alloc
uint8_t* cur; // High-water mark - updated on alloc
size_t len; // Current usage - updated on alloc/free
size_t alignment;// Checked on every alloc
void* memory; // Start of memory region (for reset/bounds checking)
size_t alloc; // Total usable memory
size_t tot_alloc;// Total including overhead
arena_t* parent_arena; // Parent arena reference
bool owns_memory; // Ownership flag
uint8_t _pad[7]; // Explicit padding to 8-byte boundary
};
freelist_header_t
The freelist_header_t structure provides data at the beginning of an
allocated memory block that enables variable alignment per allocation.
typedef struct freelist_header {
size_t block_size; // total size of the allocated block (from block_start)
size_t offset; // (uint8_t*)user_ptr - (uint8_t*)block_start
} freelist_header_t;
free_block_t
The free_block_t data structure is the basis for a non-intrusive pointer
that enables a link list to tracked returned memory.
typedef struct free_block {
size_t size;
struct free_block* next;
} free_block_t;
freelist_expect_t
freelist_expect_t is an error handling struct to be used in the creation
of freelist_t data types to catch and convey errors to a user.
typedef struct {
bool has_value;
union {
freelist_t* value;
error_code_t error;
} u;
} freelist_expect_t;
Initialization and Memory Management
The functions in this section can be used to initialize memory for a free list allocator, parse that memory to variables and to deallocate the memory.
init_freelist_with_arena
-
freelist_expect_t init_freelist_with_arena(arena_t *arena, size_t size, size_t alignment)
Initialize a freelist allocator using memory obtained from an existing arena.
Creates a freelist_t object entirely within the storage provided by an existing arena_t. The freelist does not own the underlying memory; the arena remains responsible for its lifetime. All memory managed by the freelist is carved from the arena via a single allocation.
The internal memory layout is:
Within the usable region, the freelist manages variable-sized blocks using a linked free-block structure. Returned blocks are reinserted into the free list and coalesced when possible to reduce external fragmentation.[ freelist_t | aligned usable memory region ]
has_value== trueu.valuepoints to a fully initialized freelist_t.has_value== falseu.errorcontains an error_code_t describing the failure.
arenais NULL.sizeis smaller than freelist_min_request.The requested alignment cannot be normalized to a valid power-of-two.
Arithmetic overflow occurs while computing internal layout sizes.
The arena cannot supply a contiguous block large enough to hold the freelist header and usable memory region.
- Failure conditions
Initialization fails if any of the following occur:
#include "c_allocator.h" int main(void) { uint8_t buffer[2048]; arena_expect_t a = init_static_arena(buffer, sizeof buffer, alignof(max_align_t)); if (!a.has_value) { return 1; } freelist_expect_t f = init_freelist_with_arena(a.u.value, 1024, 0); if (!f.has_value) { // handle initialization error via f.u.error return 1; } freelist_t* fl = f.u.value; void_ptr_expect_t p = alloc_freelist(fl, 64, true); if (!p.has_value) { // allocation failed return 1; } // use p.u.value return_freelist_element(fl, p.u.value); reset_arena(a.u.value); return 0; }
- Example
Creating and using a freelist inside a static arena:
Note
The freelist has fixed capacity. It does not grow after initialization. All capacity is determined by the initial
sizerequest and the available space in the arena.Note
The freelist shares the lifetime of the parent arena. When the arena is reset or destroyed, all allocations obtained from the freelist become invalid.
Note
Allocation functions operating on this freelist (e.g., alloc_freelist()) return a void_ptr_expect_t to explicitly report allocation success or failure.
Warning
The freelist does not perform ownership validation. Passing pointers not allocated by this freelist to return_freelist_element() results in undefined behavior.
- Parameters:
arena – Pointer to an initialized arena_t. Must not be NULL.
size – Requested number of usable bytes for the freelist (excluding internal metadata). Must be at least freelist_min_request. The actual usable capacity may be rounded up to satisfy alignment constraints.
alignment – Desired alignment for both the freelist structure and its internal memory region. If zero, defaults to
alignof(max_align_t). Non–power-of-two values are rounded up. The effective alignment is always at leastalignof(max_align_t).
- Returns:
A freelist_expect_t describing the result:
init_static_freelist
-
freelist_expect_t init_static_freelist(void *buffer, size_t bytes, size_t alignment)
Initialize a freelist allocator over a user-supplied static buffer.
Constructs a
freelist_tentirely within a caller-provided memory buffer. No heap allocation is performed. The freelist does not take ownership of the buffer; the caller is responsible for ensuring that the buffer remains valid for the full lifetime of the freelist.A static freelist is well suited for embedded systems, scratch allocators, fixed-capacity workspaces, or environments where dynamic allocation is restricted or forbidden.
Internally, this function creates a non-owning STATIC arena over the supplied buffer and then carves a freelist allocator out of that arena.
The in-buffer memory layout is:
The free region begins as a single large free block and is managed using variable-sized blocks with coalescing on free.[ aligned freelist_t | aligned free region for allocations ]
has_value == trueu.valuepoints to a fully initializedfreelist_t
has_value == falseu.errorindicates the reason initialization failed
Invalid arguments
bufferis NULLbytesis smaller than the minimum required control structures
Alignment errors
Requested alignment cannot be normalized to a valid power-of-two
Insufficient buffer capacity
The buffer cannot accommodate the freelist header and at least one free block after alignment
Arena initialization failure
Internal static arena setup fails (propagated from init_static_arena)
Arena allocation failure
Internal call to alloc_arena fails due to layout constraints
- Failure reasons
Initialization fails with
has_value == falsefor any of the following:
#include "c_allocator.h" int main(void) { uint8_t buffer[2048]; freelist_expect_t r = init_static_freelist(buffer, sizeof buffer, 0); if (!r.has_value) { // Handle initialization error via r.u.error return 1; } freelist_t* fl = r.u.value; void_ptr_expect_t a = alloc_freelist(fl, 128, true); if (!a.has_value) { // Allocation failed (capacity exhausted, etc.) return 1; } void* p = a.u.value; // Use memory... return_freelist_element(fl, p); // No destruction required; caller owns the buffer. return 0; }
- Example
Creating a freelist over a static buffer:
See also
See also
See also
See also
Note
The freelist does not own the underlying memory. Destroying or resetting the freelist does not free the buffer.
Note
The internal arena created by this function is also non-owning and exists solely to support allocator composition.
Note
All memory returned by
alloc_freelist()becomes invalid if the underlying arena is reset or destroyed.- Parameters:
buffer – Pointer to the start of the user-supplied memory region. Must not be NULL.
bytes – Total size of the buffer in bytes. Must be large enough to contain at least:
and preferably more to provide usable allocation capacity. If insufficient, initialization fails.sizeof(freelist_t) + sizeof(free_block_t)
alignment – Required alignment for the freelist header and all internal allocations. If zero,
alignof(max_align_t)is used. Non–power-of-two values are rounded up. The effective alignment always satisfies:alignment >= alignof(max_align_t)
- Returns:
A
freelist_expect_tresult:
init_dynamic_freelist
-
freelist_expect_t init_dynamic_freelist(size_t bytes, size_t alignment, bool resize)
Create a dynamically backed freelist allocator.
Constructs a
freelist_tinside a newly created dynamicarena_t. The freelist owns the arena and is responsible for its lifetime. All memory used by the freelist—including the freelist structure itself and all future allocations—resides entirely within this dedicated arena. This function will not compile with the code base if theSTATIC_ONLYflag is invoked at
compile time.
The caller specifies a desired minimum usable payload size (
bytes). This function computes the minimum arena space required to accommodate:an aligned
freelist_tcontrol structure,at least one
free_block_t, andthe requested usable memory region.
The underlying dynamic arena may allocate more space than requested; any additional capacity is incorporated into the freelist automatically.
has_value == trueu.valuepoints to a fully initializedfreelist_t.has_value == falseu.errordescribes the failure condition.
Invalid arguments
bytesis smaller thanfreelist_min_requestarithmetic overflow occurs during size calculations
Invalid alignment
requested alignment cannot be normalized to a valid power-of-two
Insufficient memory
the dynamic arena cannot allocate the required space
Unsupported configuration
dynamic arenas are disabled at compile time (
ARENA_ENABLE_DYNAMIC == 0)
- Failure conditions
This function returns
has_value == falsefor any of the following reasons:
#include "c_allocator.h" int main(void) { freelist_expect_t r = init_dynamic_freelist(4096, 0, false); if (!r.has_value) { // handle error via r.u.error return 1; } freelist_t* fl = r.u.value; // Allocate memory from the freelist void_ptr_expect_t m = alloc_freelist(fl, 128, true); if (!m.has_value) { // allocation failed (out of space, etc.) free_freelist(fl); return 1; } void* p = m.u.value; // ... use p ... // Return memory to the freelist return_freelist_element(fl, p); // Destroy the freelist and its arena free_freelist(fl); return 0; }
- Example
Creating a standalone freelist with dynamic backing:
Note
The freelist owns the arena created by this function. Call
free_freelist()to release all associated memory.Note
The actual usable capacity of the freelist may exceed the requested
bytes, depending on how the underlying arena rounds allocations.Warning
All pointers obtained from this freelist become invalid once
free_freelist()is called.- Parameters:
bytes – Requested minimum usable payload size in bytes (excluding internal metadata). Must be at least
freelist_min_request.alignment – Desired alignment for the freelist header and all internal allocations. If zero,
alignof(max_align_t)is used. Non–power-of-two values are rounded up. The effective alignment is always at leastalignof(max_align_t).resize – Controls whether the underlying dynamic arena is permitted to grow. In the current design, the freelist itself remains fixed-size after construction; this flag primarily affects arena behavior and future extensibility.
- Returns:
A
freelist_expect_tresult:
free_freelist
-
void free_freelist(freelist_t *fl)
Destroy a dynamically allocated freelist and its underlying arena.
This function frees a freelist created with
init_dynamic_freelist().
Only dynamic freelists own their underlying memory; static freelists do not.
After calling this function:
All memory allocated from the freelist becomes invalid.
The
freelist_tobject itself is invalid, because it lives inside the freed arena.
Static freelists (those created with
init_freelist_with_arena()orinit_static_freelist()) must not be passed to this function.freelist_t* fl = init_dynamic_freelist(2048, 0, false); if (!fl) return 1; void* p = alloc_freelist(fl, 128, false); if (!p) return 1; free_freelist(fl);
Note
This function only applies to freelists that own their arena. Attempting to free a static freelist does nothing.
- Parameters:
fl – Pointer to a freelist. Must refer to a dynamically allocated freelist.
alloc_freelist
-
void_ptr_expect_t alloc_freelist(freelist_t *fl, size_t size, bool zero)
Allocate a block of memory from a freelist allocator.
This function allocates
bytesof user-visible memory from the freelist, returning a pointer aligned to the freelist’s effective alignment. Internally, the allocator may consume more memory than requested due to:a small allocation header (
freelist_header_t) stored immediately before the returned user pointeralignment padding between the free block and the user region
full-block consumption when the remaining fragment is too small to form another free block
These internal details are fully managed by the allocator and are invisible to the caller.
Memory returned by this function must be released back to the freelist using return_freelist_element(). The allocator performs block coalescing where possible to reduce external fragmentation.
has_value== trueu.valuepoints to an aligned user-accessible memory region
has_value== falseu.errorindicates the reason for failure
freelist_expect_t fe = init_dynamic_freelist(2048, 0, false); if (!fe.has_value) { // handle freelist initialization error via fe.u.error return; } freelist_t* fl = fe.u.value; void_ptr_expect_t r = alloc_freelist(fl, 128, true); if (!r.has_value) { // handle allocation failure via r.u.error free_freelist(fl); return; } void* p = r.u.value; // p is aligned and safe to use return_freelist_element(fl, p); free_freelist(fl);
- Example
Allocating and returning memory from a freelist:
See also
See also
See also
See also
Note
The returned pointer is always aligned to at least
freelist_alignment(fl). Alignment padding may increase the amount of freelist capacity consumed beyondbytes.Note
The freelist may split free blocks or consume them entirely to avoid leaving unusable tail fragments.
Note
This function is not thread-safe unless externally synchronized.
- Parameters:
fl – Pointer to a valid freelist_t instance. Must not be NULL.
bytes – Number of user bytes requested. Must be greater than zero.
zeroed – If
true, the returned user region is zero-initialized. Iffalse, the contents of the block are left uninitialized.
- Returns:
A void_ptr_expect_t result:
- Return values:
INVALID_ARG – The freelist pointer is NULL, the requested size is zero, or internal argument validation fails.
ALIGNMENT_ERROR – The freelist alignment is invalid (not a power-of-two).
CAPACITY_OVERFLOW – No suitable free block exists to satisfy the request, or an internal size computation overflow was detected.
realloc_freelist
-
void_ptr_expect_t realloc_freelist(freelist_t *fl, void *variable, size_t old_size, size_t new_size, bool zeroed)
Resize an allocation managed by a freelist.
This function provides a
realloc-like interface for memory obtained from a freelist_t. Because freelists do not support in-place growth, resizing follows these rules:If
ptrisNULL, the call behaves like alloc_freelist().If
new_sizeis less than or equal toold_size, the existing pointer is returned unchanged (the block is not shrunk).If the block must grow, a new block is allocated, the first
old_sizebytes are copied, the old block is returned to the freelist via return_freelist_element(), and the new pointer is returned.
The returned pointer is always aligned to at least ::freelist_alignment(fl).
has_value== trueu.valuepoints to:the original pointer if no growth was required, or
a newly allocated block if growth succeeded.
has_value== falseu.errorcontains the reason for failure, typically:INVALID_ARG — invalid arguments (e.g. NULL freelist, zero
new_size)CAPACITY_OVERFLOW — insufficient free space to grow
freelist_expect_t flr = init_dynamic_freelist(4096, 0, false); if (!flr.has_value) { // handle freelist initialization error return; } freelist_t* fl = flr.u.value; void_ptr_expect_t a = alloc_freelist(fl, 64, false); if (!a.has_value) return; // Grow from 64 bytes to 200 bytes void_ptr_expect_t r = realloc_freelist(fl, a.u.value, 64, 200, true); if (!r.has_value) { // handle resize failure via r.u.error return; } return_freelist_element(fl, r.u.value); free_freelist(fl);
See also
See also
See also
Note
The freelist never frees memory implicitly. Old blocks remain valid until explicitly returned to the freelist.
Note
In-place growth is not supported; all expansions require allocate–copy–return semantics.
Note
Passing a pointer not allocated from this freelist results in undefined behavior.
- Parameters:
fl – Pointer to a valid freelist_t. Must not be NULL.
ptr – Existing user pointer previously returned by alloc_freelist or alloc_freelist_aligned. May be NULL.
old_size – Number of user bytes originally allocated at
ptr. The freelist does not store user sizes internally; the caller must supply an accurate value.new_size – Number of user bytes requested for the resized allocation. Must be greater than zero.
zeroed – If true and the allocation grows, the newly added region (
[old_size, new_size)) is zero-initialized.
- Returns:
A void_ptr_expect_t describing the result:
alloc_freelist_aligned
-
void_ptr_expect_t alloc_freelist_aligned(freelist_t *fl, size_t bytes, size_t alignment, bool zeroed)
Allocate an aligned block of memory from a freelist.
This function behaves like alloc_freelist(), but allows the caller to request a specific alignment for the returned pointer. The effective alignment is computed as follows:
If
alignmentis zero, the freelist’s base alignment is used.If
alignmentis not a power-of-two, it is rounded up.The final alignment is always at least ::freelist_alignment(fl).
Internally, the allocator may consume more memory than the requested
bytesdue to:a freelist header stored immediately before the user pointer
padding required to satisfy the effective alignment
full-block consumption when the remaining fragment would be too small to form another free block
These details are tracked internally and do not affect the size or alignment guarantees of the pointer returned to the caller.
has_value== trueu.valuepoints to an aligned user region ofbytes.has_value== falseu.errorindicates the reason for failure:INVALID_ARG if
flis NULL orbytesis zeroALIGNMENT_ERROR if alignment normalization fails or overflows
CAPACITY_OVERFLOW if no suitable free block is available
See also
See also
- Example
freelist_expect_t fr = init_dynamic_freelist(4096, 0, false); if (!fr.has_value) { return 1; } freelist_t *fl = fr.u.value; void_ptr_expect_t r = alloc_freelist_aligned(fl, 256, 64, true); if (!r.has_value) { // handle allocation failure via r.u.error free_freelist(fl); return 1; } void *p = r.u.value; // use p ... return_freelist_element(fl, p); free_freelist(fl);
Note
The returned pointer must be released using return_freelist_element(). Individual blocks must not be freed with
free().Note
Requesting a stricter alignment may increase internal fragmentation but does not otherwise affect freelist accounting semantics.
- Parameters:
fl – Pointer to a valid freelist_t. Must not be NULL.
bytes – Number of user bytes requested. Must be greater than zero.
alignment – Desired alignment for the returned pointer. If zero, the freelist’s base alignment is used. Non–power-of-two values are rounded up. The effective alignment is guaranteed to be at least ::freelist_alignment(fl).
zeroed – If true, the user-visible region is zero-initialized before being returned.
- Returns:
A void_ptr_expect_t describing the result:
realloc_freelist_aligned
-
void_ptr_expect_t realloc_freelist_aligned(freelist_t *fl, void *ptr, size_t old_size, size_t new_size, bool zeroed, size_t alignment)
Resize an aligned allocation managed by a freelist.
This function is the alignment-aware counterpart to realloc_freelist. It provides
realloc-like behavior for memory obtained from a freelist_t, while allowing the caller to request a specific alignment for the resized allocation.Because freelists do not support in-place growth, expansion is always implemented as:
Allocate a new block of
new_sizebytes with the requested alignment using alloc_freelist_aligned().Copy the first
old_sizebytes fromptrinto the new block.Optionally zero-fill the newly added tail region.
Return the old block to the freelist via return_freelist_element().
If
new_sizeis less than or equal toold_size, the function performs a no-op and returns the original pointer unchanged.The effective alignment of the returned block is:
max(requested_alignment, freelist_alignment(fl))
has_value== trueu.valuepoints to the resized allocation (which may be the original pointer if no growth was required).has_value== falseu.errorindicates the reason for failure, typically one of:INVALID_ARG — invalid freelist, size, or alignment
ALIGNMENT_ERROR — requested alignment cannot be satisfied
CAPACITY_OVERFLOW — insufficient free space to grow
freelist_expect_t f = init_dynamic_freelist(4096, 0, false); if (!f.has_value) return 1; freelist_t* fl = f.u.value; // Initial allocation with 64-byte alignment void_ptr_expect_t a = alloc_freelist_aligned(fl, 128, 64, false); if (!a.has_value) return 1; void* p = a.u.value; // Grow to 512 bytes, preserving alignment void_ptr_expect_t r = realloc_freelist_aligned(fl, p, 128, 512, true, 64); if (!r.has_value) { // handle error via r.u.error return 1; } void* q = r.u.value; return_freelist_element(fl, q); free_freelist(fl);
See also
See also
See also
Note
This function never shrinks allocations. Requests with
new_size<=old_sizereturn the original pointer unchanged.Note
If the original block was allocated with a weaker alignment than requested here, the returned block will necessarily move.
Warning
Any pointers returned by earlier allocations that overlap the freed block become invalid after a successful grow operation.
- Parameters:
fl – Pointer to a valid freelist_t. Must not be NULL.
ptr – Existing pointer previously returned by alloc_freelist or alloc_freelist_aligned. May be NULL, in which case this function behaves like alloc_freelist_aligned.
old_size – Number of user bytes originally allocated at
ptr. The freelist does not track user sizes internally; the caller must supply this value accurately.new_size – Number of user bytes requested for the resized allocation. Must be greater than zero.
zeroed – If true and a new block is allocated, the newly added tail region (bytes [
old_size,new_size)) is zero-filled.alignment – Desired alignment for the returned pointer. If zero, the freelist’s base alignment is used. Non–power-of-two values are rounded up.
- Returns:
A void_ptr_expect_t with the following semantics:
return_freelist
-
inline void reset_freelist(freelist_t *fl)
Reset a freelist to its initial empty state.
This function clears all allocation state within a freelist_t, restoring it to a pristine, fully empty allocator. All previously allocated blocks become invalid and may not be used after the reset.
The freelist’s internal region is rebuilt as a single large free block:
No memory is freed back to an arena in this operation; it only resets the freelist’s internal bookkeeping. If the freelist was created inside an arena (via init_freelist_with_arena or init_static_freelist), the arena itself remains unchanged.[ free_block_t(size = fl->alloc) ]
freelist_t* fl = init_dynamic_freelist(4096, 0, false); void* p = alloc_freelist(fl, 128, false); // Reset the freelist — p becomes invalid reset_freelist(fl); void* q = alloc_freelist(fl, 128, false); return_freelist_element(fl, q); free_freelist(fl);
Note
This operation invalidates all outstanding freelist allocations.
- Parameters:
fl – Pointer to an initialized freelist. Must not be NULL. The freelist must have a valid
memoryregion and non-zero capacity.
return_freelist_element
-
void return_freelist_element(freelist_t *fl, void *ptr)
Return a previously allocated block to the freelist.
This function frees a block allocated by alloc_freelist or alloc_freelist_aligned and reinserts it into the freelist as a free block. If adjacent free blocks exist immediately before or after the returned region, they are automatically coalesced, reducing external fragmentation.
The function verifies:
ptris non-NULLptrlies inside the freelist’s managed memory regionA valid freelist header exists immediately before
ptrThe block’s recorded
block_sizeandoffsetare saneThe block fits entirely within the freelist memory region
On successful return, the freelist’s internal accounting (
len) is decreased by exactly the block size originally charged during allocation.freelist_t* fl = init_dynamic_freelist(4096, 0, false); void* p = alloc_freelist(fl, 128, false); // Return p to the freelist return_freelist_element(fl, p); // p must not be used after this point free_freelist(fl);
Note
Double frees, frees of foreign pointers, and corrupted metadata are all detected and rejected.
Note
This function performs in-place free-block coalescing whenever adjacent blocks are physically contiguous.
Warning
After calling this function,
ptrbecomes invalid and must not be dereferenced.- Parameters:
fl – Pointer to the freelist that originally allocated
ptr. Must not be NULL.ptr – Pointer previously returned by a freelist allocation. Must not be NULL and must refer to a currently allocated block.
Utility Funcitons
is_freelist_ptr
-
bool is_freelist_ptr(const freelist_t *fl, const void *ptr)
Validate whether a pointer was allocated by this freelist.
This function performs a strict verification that
ptrcorresponds to a currently active allocation originating from the given freelist_t.Validation includes:
Pointer lies inside the freelist’s managed memory region
There is a valid allocation header immediately preceding the pointer
The recorded block size and offset form a consistent block layout
The full block fits entirely inside the freelist region
This check detects:
Pointers from other allocators
Off-by-one or misaligned pointers
Double frees
Corrupted block metadata
freelist_t* fl = init_dynamic_freelist(2048, 0, false); void* p = alloc_freelist(fl, 64, false); bool ok1 = is_freelist_ptr(fl, p); // true bool ok2 = is_freelist_ptr(fl, p + 1); // false return_freelist_element(fl, p); free_freelist(fl);
Note
This function does not check whether the block is still allocated or already freed; it only checks if
ptrcould have been returned by the freelist.- Parameters:
fl – The freelist to validate against. Must not be NULL.
ptr – The candidate pointer. Must not be NULL.
- Returns:
true if the pointer appears valid and belongs to
flfalse otherwise
is_freelist_ptr_sized
-
bool is_freelist_ptr_sized(const freelist_t *fl, const void *ptr, size_t size)
Validate that a freelist pointer is valid and large enough for a size.
This function extends is_freelist_ptr by additionally ensuring that the memory block referenced by
ptrcontains at leastsizeuser bytes.The freelist records, for each block:
The available user payload is:block_size = total block size (header + padding + user) offset = distance from block_start to user_ptr
This function verifies that:user_data_size = block_size - offset
ptris a plausible freelist allocationsizedoes not exceeduser_data_sizeptr+ size remains inside the freelist memory region
freelist_t* fl = init_dynamic_freelist(4096, 0, false); void* p = alloc_freelist(fl, 128, false); bool ok1 = is_freelist_ptr_sized(fl, p, 64); // true bool ok2 = is_freelist_ptr_sized(fl, p, 200); // false return_freelist_element(fl, p); free_freelist(fl);
- Parameters:
fl – Freelist allocator to validate against. Must not be NULL.
ptr – Candidate pointer previously returned by a freelist allocation.
size – Number of bytes the caller intends to use starting at
ptr.
- Returns:
true if
ptris valid for the freelist and has at leastsizebytes
false otherwise
freelist_stats
-
bool freelist_stats(const freelist_t *fl, char *buffer, size_t buffer_size)
Produce a human-readable diagnostic summary of a freelist allocator.
This function writes a formatted, multi-line textual report describing the internal state of a
freelist_tallocator. It mirrors the behavior ofarena_stats()and is intended for debugging, logging, and verification during development.The report includes:
freelist type (STATIC or DYNAMIC)
whether the freelist owns the underlying arena
used bytes (total block sizes consumed)
remaining bytes
usable capacity of the freelist region
total bytes carved from the parent arena (including freelist header)
utilization percentage of the freelist region
alignment requirements
enumeration of free blocks (address and size)
The string is written into the caller-provided buffer using the internal
_buf_appendf()utility. Output is always null-terminated as long as the buffer is at least 1 byte in size.char buf[512]; freelist_t *fl = init_freelist_with_arena(arena, 1024, 0); if (!fl) { // handle error } if (freelist_stats(fl, buf, sizeof buf)) { printf("%s\n", buf); } else { perror("freelist_stats"); }
Note
This function never allocates memory. All output is performed using the caller-provided buffer.
- Parameters:
fl – Pointer to the freelist instance to query. When NULL, the function writes the string “Freelist: NULL” and returns true.
buffer – Destination character buffer. Must not be NULL.
buffer_size – Size of
bufferin bytes. Must be greater than zero.
- Return values:
true – The report was successfully generated (even if truncated to fit within the provided buffer).
false – An error occurred. In this case:
Any internal
_buf_appendf()failure (buffer too small) results infalse, but the buffer may contain partial output.
Getter and Setter Functions
freelist_remaining
-
inline size_t freelist_remaining(const freelist_t *fl)
Return the remaining capacity (in bytes) of a freelist.
This function reports how many bytes of the freelist’s usable region are still available for future allocations, according to the internal accounting
alloc - len.Note that
lencounts the full block size of allocated blocks (including header and padding), not just the user-requested payload. Therefore,freelist_remaining(fl)is a logical capacity measure, not simply “sum of free fragments”.size_t before = freelist_remaining(fl); void* p = alloc_freelist(fl, 128, false); size_t after = freelist_remaining(fl);
- Parameters:
fl – Pointer to a valid freelist. Must not be NULL.
- Returns:
Number of remaining bytes (
size_t) that can be consumed by new allocations, or 0 on error.
freelist_mtype
-
inline alloc_t freelist_mtype(const freelist_t *fl)
Query the underlying allocation type used by the freelist’s arena.
This function forwards to arena_mtype on the freelist’s parent arena. It allows callers to distinguish between static and dynamic backing stores (or other allocation modes defined by alloc_t).
Note
Typical values include STATIC and DYNAMIC, depending on how the arena was initialized.
- Parameters:
fl – Pointer to a freelist. Must not be NULL.
- Returns:
An alloc_t value describing the arena’s allocation mode, or ALLOC_INVALID if
flis NULL. On erreor.
freelist_size
-
inline size_t freelist_size(const freelist_t *fl)
Return the total number of bytes currently consumed by the freelist.
This function reports how many bytes of the freelist’s usable region are currently in use, according to the internal accounting field
len.Important:
lencounts the full block size of each allocation (header + padding + user payload), not just the user-requested bytes. As a result:holds by construction.freelist_size(fl) + freelist_remaining(fl) == freelist_alloc(fl)
- Parameters:
fl – Pointer to a freelist. Must not be NULL.
- Returns:
Number of bytes accounted as in use, or 0 on error.
freelist_alloc
-
inline size_t freelist_alloc(const freelist_t *fl)
Return the total usable capacity of the freelist region.
This function reports the size, in bytes, of the freelist’s managed memory region. It represents the maximum amount of space that can be consumed by allocations plus internal metadata and padding.
Equivalently, it is the constant part of:
freelist_size(fl) + freelist_remaining(fl) == freelist_alloc(fl)
- Parameters:
fl – Pointer to a freelist. Must not be NULL.
- Returns:
Total usable freelist capacity in bytes, or 0 on error.
total_freelist_alloc
-
inline size_t total_freelist_alloc(const freelist_t *fl)
Return the total number of bytes carved from the backing arena.
This function returns the total size of the contiguous chunk obtained from the parent arena for this freelist. It may be larger than the freelist’s usable capacity (see freelist_alloc), because it includes space consumed by the freelist header and any internal alignment padding.
Conceptually:
::total_freelist_alloc(fl) ≥ ::freelist_alloc(fl)
- Parameters:
fl – Pointer to a freelist. Must not be NULL.
- Returns:
Total bytes reserved from the arena for this freelist, or 0 on error.
freelist_alignment
-
inline size_t freelist_alignment(const freelist_t *fl)
Return the base alignment guarantee of the freelist.
This function reports the alignment, in bytes, that the freelist guarantees for allocations when using alloc_freelist. Aligned variants such as alloc_freelist_aligned may request stricter alignments, but will never return pointers with alignment less than this base value.
- Parameters:
fl – Pointer to a freelist. Must not be NULL.
- Returns:
The freelist’s base alignment in bytes, or 0 on error.
min_freelist_alloc
-
inline size_t min_freelist_alloc()
Return the minimum usable allocation size required to construct a freelist.
A freelist requires that its initial usable memory region be large enough to hold at least one
free_block_tstructure. This represents the smallest possible free block that the allocator can manage.This function exposes that minimum requirement so callers can query it when:
allocating static buffers for
init_static_freelist()determining whether a requested freelist size is valid
performing compile-time or runtime capacity checks
The value returned is constant for the lifetime of the program and is typically:
sizeof(free_block_t)
- Example
size_t min_bytes = min_freelist_alloc(); // Create a static buffer large enough for the freelist and its metadata. uint8_t buffer[1024]; // Safe: 1024 >= min_bytes freelist_t* fl = init_static_freelist(buffer, sizeof buffer, 0); if (!fl) { // Hanlde error }
- Returns:
The minimum number of usable bytes (
size_t) required to initialize a freelist.
Freelist Context Functions
The following functions provide the freelist-backed implementation of the
allocator vtable interface. They adapt an freelist_t instance to the generic
allocator API by exposing allocation, reallocation, and bulk deallocation
operations in a consistent form.
freelist_v_alloc
-
static inline void_ptr_expect_t freelist_v_alloc(void *ctx, size_t size, bool zeroed)
Allocate memory from a freelist via allocator vtable.
Requests a block of at least
sizebytes from the freelist associated withctx. On success, returns a valid pointer wrapped in void_ptr_expect_t. On failure, returns an error code.- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
size – Number of bytes requested (> 0).
zeroed – If true, the returned memory is zero-initialized.
- Returns:
void_ptr_expect_t containing a pointer on success or an error code on failure.
freelist_v_alloc_aligned
-
static inline void_ptr_expect_t freelist_v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Allocate aligned memory from a freelist via allocator vtable.
Requests a block of at least
sizebytes aligned toalign. The effective alignment is validated and normalized by the freelist allocator.- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
size – Number of bytes requested (> 0).
align – Requested alignment in bytes (0 => freelist default).
zeroed – If true, the returned memory is zero-initialized.
- Returns:
void_ptr_expect_t containing a pointer on success or an error code on failure.
freelist_v_realloc
-
static inline void_ptr_expect_t freelist_v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Reallocate a freelist allocation via allocator vtable.
Resizes a previously allocated block. If
old_ptris NULL, behaves like an allocation. Shrinking does not release memory.- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
old_ptr – Existing allocation or NULL.
old_size – Size of the existing allocation in bytes.
new_size – Requested new size in bytes (> 0).
zeroed – If true, newly added memory is zero-initialized.
- Returns:
void_ptr_expect_t containing the resized pointer on success or an error code on failure.
freelist_v_realloc_aligned
-
static inline void_ptr_expect_t freelist_v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Reallocate a freelist allocation with alignment via allocator vtable.
Resizes a previously allocated block while enforcing a requested alignment. If the block must grow, a new aligned block is allocated and the old block is returned to the freelist.
- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
old_ptr – Existing allocation or NULL.
old_size – Size of the existing allocation in bytes.
new_size – Requested new size in bytes (> 0).
zeroed – If true, newly added memory is zero-initialized.
align – Requested alignment in bytes (0 => freelist default).
- Returns:
void_ptr_expect_t containing the resized pointer on success or an error code on failure.
freelist_v_return
-
static inline void freelist_v_return(void *ctx, void *ptr)
Return a previously allocated block to a freelist.
Restores a block obtained from the freelist back into its free list. The pointer must have been returned by this allocator.
- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
ptr – Pointer previously obtained from this allocator.
freelist_v_free
-
static inline void freelist_v_free(void *ctx)
Destroy a freelist allocator via allocator vtable.
Releases all resources owned by the freelist. If the freelist owns its backing arena, the arena is destroyed as well.
- Parameters:
ctx – Allocator context; must point to a valid freelist_t.
Buddy Allocator Overview
The buddy_t allocator implements a classic binary buddy allocation scheme.
Memory is acquired as a single, power-of-two–sized pool, and all allocations are
satisfied by recursively subdividing blocks into halves (“buddies”) until the
smallest usable block size is reached. Freed blocks are efficiently coalesced
with their buddies to restore larger contiguous blocks.
This model provides a balance between fast allocation, bounded fragmentation, and the ability to free individual blocks, while retaining predictable block sizes and layout.
The buddy system is ideal when:
the allocator must handle arbitrary allocation and free patterns
fragmentation control is important
memory needs can vary dynamically within a bounded pool
allocations benefit from power-of-two block sizing and alignment guarantees
the application requires deterministic performance for alloc/free
Examples include:
custom memory managers in real-time or embedded software
scratch allocators for physics engines, audio engines, or network buffers
fixed-size arenas that must support both allocation and deallocation
game engines and simulation systems where fragmentation must remain bounded
OS kernels (many use buddy systems internally for page-frame allocation)
Benefits
Fast allocation and deallocation Typically O(log N), often near O(1) due to shallow free lists.
Automatic coalescing When a block is freed, its buddy is detected and merged, restoring larger blocks and reducing fragmentation.
Bounded fragmentation Fragmentation is limited to internal power-of-two rounding, rather than the arbitrary fragmentation of general-purpose heaps.
Predictable memory layout Block sizes and boundaries follow a binary tree structure, enabling more deterministic and cache-friendly memory usage.
Strong alignment guarantees Power-of-two block sizes combined with
base_alignensure that returned pointers meet strict alignment requirements.
Limitations
Internal fragmentation Each allocation is rounded up to the next power of two.
Fixed pool size The buddy allocator does not grow once created; the pool size is fixed.
Overhead for many small allocations Very small objects may occupy larger blocks depending on
min_block_size.More complex than an arena allocator Allocation and freeing may be logarithmic and require block splitting and coalescing.
Not ideal for large numbers of long-lived objects Internal fragmentation can accumulate if most allocations hover near power-of-two boundaries.
Data Types
The following are data structures and derived data types used in the c_allocator.h
and c_allocator.c files to support the buddy_t data type.
buddy_t
buddy_t is an opaque data structure that is not visibile to the user. This struct
contains metadata on a chunk of allocated memory.
struct buddy_t {
void *base; /* Base address of the OS-allocated pool */
buddy_block_t **free_lists; /* Array of free-list heads (one per order level) */
size_t pool_size; /* Total pool size in bytes (power of two) */
size_t len; /* Total bytes currently consumed (block sizes) */
size_t alloc; /* Total usable pool bytes (== pool_size) */
size_t total_alloc; /* Total memory including pool + metadata overhead */
size_t base_align; /* Required alignment for returned user pointers */
size_t user_offset; /* Byte offset from block start to aligned user data */
uint32_t min_order; /* log2(minimum block size) */
uint32_t max_order; /* log2(pool size) */
uint32_t num_levels; /* Number of free-list levels (block orders) */
uint8_t _pad[4]; /* Pad struct to maintain 8-byte alignment */
};
The buddy_t data type uses the following structs.
typedef struct buddy_block {
struct buddy_block *next;
} buddy_block_t;
typedef struct buddy_header {
uint32_t order; /* log2(block_size) */
size_t block_offset;
} buddy_header_t;
buddy_expect_t
buddy_expect_t is an error handling struct to be used in the creation
of buddy_t data types to catch and convey errors to a user.
typedef struct {
bool has_value;
union {
buddy_t* value;
error_code_t error;
} u;
} buddy_expect_t;
Initialization and Memory Management
The functions in this section can be used to initialize memory for a bump allocator, parse that memory to variables and to deallocate the memory.
init_buddy_allocator
-
buddy_expect_t init_buddy_allocator(size_t pool_size, size_t min_block_size, size_t base_align)
Initialize a buddy allocator with a fixed-size memory pool.
This function constructs a new buddy_t allocator backed by a power-of-two–sized memory pool obtained from the OS using
buddy_os_alloc()(typicallymmapon POSIX orVirtualAllocon Windows). If the code is compiled with aSTATIC_ONLYflag, none of the buddy allocator functions in this library will compile with the code base.The allocator divides the pool into blocks whose sizes are powers of two, ranging from
min_block_sizeup topool_size. All allocation requests are rounded upward to the nearest block size that can hold:the internal allocation header (
buddy_header_t), andthe requested user payload aligned to
base_align.
The following normalization rules apply:
If
base_alignis0, it defaults toalignof(max_align_t).If
base_alignis not a power of two, it is rounded up to the next power of two.If
min_block_sizeis too small to hold the header plus alignment padding, it is increased to the minimum size that can.Both
pool_sizeandmin_block_sizeare rounded up to powers of two.min_block_sizemust not exceed the (adjusted)pool_size.
The top-level free list is initialized with a single block representing the entire pool.
On success:
has_valueis true andu.valuepoints to a fully initialized buddy_t allocator.On failure:
has_valueis false andu.errorcontains an error_code_t describing the reason for failure.
#include "buddy.h" int main(void) { size_t pool = 4096; // 4 KiB requested size_t min_block = 64; // smallest allocation is 64 bytes size_t align = 32; // user pointers aligned to 32 bytes buddy_expect_t be = init_buddy_allocator(pool, min_block, align); if (!be.has_value) { // handle be.u.error return 1; } buddy_t *buddy = be.u.value; // Allocate 100 bytes (rounded to next block size) void_ptr_expect_t pe = alloc_buddy(buddy, 100, false); if (!pe.has_value) { // handle pe.u.error free_buddy(buddy); return 1; } void *ptr = pe.u.value; // Use memory ... // Return memory return_buddy(buddy, ptr); // Release the entire buddy allocator free_buddy(buddy); return 0; }
- Example
Basic buddy allocator usage:
Note
The resulting allocator is not resizable; its pool size is fixed for the lifetime of the object. Use free_buddy() to destroy it.
Note
Individual allocations are obtained via alloc_buddy(), which returns a void_ptr_expect_t describing success or failure.
Warning
Pointers returned by alloc_buddy() must be released only via ::return_buddy(). Using
free()on them is undefined behavior.- Parameters:
pool_size – Requested total pool size in bytes. Must be non-zero. The effective pool size is rounded upward to the next power of two.
min_block_size – Minimum block size in bytes. Must be non-zero. After accommodating the internal header and alignment padding, the value is rounded upward to the next power of two.
base_align – Required alignment for returned user pointers. If zero, natural alignment (
alignof(max_align_t)) is used. Non–power-of-two values are rounded up.
- Returns:
A buddy_expect_t describing the result:
- Return values:
INVALID_ARG – One or more arguments are invalid or unsatisfiable after normalization, including:
pool_size== 0min_block_size== 0min_block_sizeexceedspool_sizeafter normalization
ALIGNMENT_ERROR – The requested alignment cannot be normalized to a valid power-of-two.
CAPACITY_OVERFLOW – A size computation overflows (for example during power-of-two rounding or internal accounting).
BAD_ALLOC – Allocation of the control structure, free-list array, or OS-level backing pool fails.
free_buddy
-
void free_buddy(buddy_t *b)
Destroy a buddy allocator and release all associated resources.
This function frees the OS-backed memory pool created by init_buddy_allocator(), releases internal metadata such as the free-list array, clears the allocator structure, and finally frees the buddy_t object itself.
All memory allocated through this buddy allocator becomes invalid after this call. Accessing pointers returned by alloc_buddy(), alloc_buddy_aligned(), realloc_buddy(), or realloc_buddy_aligned() after invoking free_buddy() results in undefined behavior.
The function is safe to call with a NULL pointer, in which case it performs no action.
buddy_t *buddy = init_buddy_allocator(4096, 64, 32); if (!buddy) { perror("init_buddy_allocator"); return 1; } void *p = alloc_buddy(buddy, 128, false); // ... use p ... free_buddy(buddy); // releases pool + metadata + allocator itself
Note
This call implicitly frees all blocks allocated from
b, regardless of whether they were returned via ::return_buddy(). Users do not need to free individual allocations before calling this function.Warning
After calling free_buddy(), the pointer
bmust not be reused. The function clears the contents of the structure before freeing it.- Parameters:
b – Pointer to a buddy_t instance previously created by init_buddy_allocator(). May be NULL.
alloc_buddy
-
void_ptr_expect_t alloc_buddy(buddy_t *b, size_t size, bool zeroed)
Allocate a block of memory from a buddy allocator.
Allocates
sizebytes from the buddy allocatorband returns the result as a void_ptr_expect_t. The allocation request is rounded upward to accommodate the internal buddy_header_t and the allocator’s power-of-two block sizing rules.The pointer returned to the caller refers to the user region, which begins immediately after the internal allocation header. The header records the block order and pool offset required to return the block to the allocator.
Allocation proceeds as follows:
The requested user size is increased by sizeof(::buddy_header_t).
The total size is raised to at least the minimum block size (
2^min_order).The result is rounded up to the next power of two.
A free block of sufficient order is located; if necessary, larger blocks are repeatedly split into buddies until the target order is reached.
A buddy_header_t is written at the start of the block.
If
zeroedis true, the user region is zero-initialized.
The returned pointer is always offset by
sizeof(buddy_header_t)from the beginning of the allocated block. If stricter user-defined alignment is required, use alloc_buddy_aligned().has_value== true
and
u.valuepointing to the allocated user region on success.has_value== false
and
u.errordescribing the failure.
buddy_expect_t be = init_buddy_allocator(4096, 64, 32); if (!be.has_value) { // handle initialization error via be.u.error return 1; } buddy_t *b = be.u.value; // Allocate 100 bytes (rounded to power-of-two block) void_ptr_expect_t r = alloc_buddy(b, 100, true); if (!r.has_value) { // handle allocation failure via r.u.error free_buddy(b); return 1; } void *ptr = r.u.value; // Use memory... return_buddy(b, ptr); // return block to buddy system free_buddy(b); // destroy allocator
Note
Returned pointers must be released using ::return_buddy(). Calling
free()on a pointer obtained from this function results in undefined behavior.Note
This function guarantees only the alignment implied by the block size and internal header placement. Use alloc_buddy_aligned() for explicit alignment requirements.
- Parameters:
b – Pointer to a valid buddy_t allocator created by init_buddy_allocator(). Must not be NULL.
size – Requested number of user bytes. Must be greater than zero.
zeroed – If true, the user portion of the allocated block is filled with zeros. The internal allocation header is never zero-initialized.
- Returns:
A void_ptr_expect_t with:
- Return values:
INVALID_ARG –
bis NULLsizeis zero
BAD_ALLOC –
No free block of sufficient size exists
The request exceeds the allocator’s total pool capacity
CAPACITY_OVERFLOW –
Size normalization or power-of-two rounding overflows
Internal accounting would overflow
realloc_buddy
-
void_ptr_expect_t realloc_buddy(buddy_t *buddy, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Resize a buddy allocation, preserving existing data when possible.
This function resizes a block previously allocated from the buddy allocator
buddy. It follows realloc-like semantics adapted to the buddy system and the library’svoid_ptr_expect_terror-handling model.The behavior is defined as follows:
If
old_ptrisNULLandnew_size> 0, the call behaves like ::alloc_buddy(buddy, new_size, zeroed).If
old_ptris non-NULL andnew_sizeis zero, the block referenced byold_ptris returned to the allocator via return_buddy_element(), and a successful result containing aNULLpointer is returned.If
new_sizeis less than or equal to the usable capacity of the existing block, the allocation is reused in place and the original pointer is returned.If
new_sizeexceeds the usable capacity, a new block is allocated, the existing contents are copied, the old block is returned to the allocator, and the new pointer is returned.
The usable capacity of an existing block is derived from the internal buddy_header_t stored immediately before
old_ptr:whereusable_old = (2^order) - sizeof(buddy_header_t)
orderis recorded in the allocation header.If
zeroedis true:In the in-place grow case, the newly exposed tail region [old_size, new_size) is zero-initialized.
In the move case, the newly allocated block is zero-initialized by alloc_buddy().
has_value== trueu.valuecontains:a pointer to the resized allocation, or
NULLwhennew_size== 0 (successful free)
has_value== falseu.errorindicates the reason for failure.
buddy_t *b = init_buddy_allocator(4096, 64, 32); if (!b) return 1; size_t size = 128; void_ptr_expect_t r1 = alloc_buddy(b, size, false); if (!r1.has_value) return 1; void *p = r1.u.value; // Grow to 256 bytes, zero-initializing the new tail void_ptr_expect_t r2 = realloc_buddy(b, p, size, 256, true); if (!r2.has_value) { // p is still valid here return_buddy_element(b, p); free_buddy(b); return 1; } void *p2 = r2.u.value; return_buddy_element(b, p2); free_buddy(b);
Note
On success, when a new block is allocated, exactly
min(old_size, usable_old)bytes are copied from the old block into the new block before the old block is returned to the allocator.Warning
Passing a pointer not obtained from alloc_buddy(), alloc_buddy_aligned(), realloc_buddy(), or realloc_buddy_aligned() on the same allocator instance results in undefined behavior.
- Parameters:
buddy – Pointer to a valid buddy_t allocator created by init_buddy_allocator(). Must not be NULL.
old_ptr – Pointer to a block previously allocated from
buddy, orNULL.old_size – Logical size (in bytes) of the existing allocation as tracked by the caller. Must be non-zero when
old_ptris non-NULL.new_size – Requested new size in bytes. A value of zero indicates that the allocation should be freed.
zeroed – If true, any newly added bytes are zero-initialized according to the rules described above.
- Returns:
A
void_ptr_expect_twith the following semantics:- Return values:
INVALID_ARG –
buddyis NULLold_ptris non-NULL andold_sizeis zeroThe allocation header is inconsistent with the allocator state
BAD_ALLOC –
A larger block is required and alloc_buddy() fails (the original allocation remains valid)
return_buddy_element
-
bool return_buddy_element(buddy_t *b, void *ptr)
Return a previously allocated block to the buddy allocator.
This function frees a block allocated by alloc_buddy(), alloc_buddy_aligned(), realloc_buddy(), or realloc_buddy_aligned(), restoring it to the buddy system’s free lists. After freeing, the block may be coalesced with its buddy if the adjacent block of the same order is also free.
The user pointer
ptrmust be one returned by one of the allocation functions of this buddy allocator. The corresponding block header (buddy_header_t) is assumed to reside immediately before the user pointer.Freeing proceeds as follows:
If
ptris NULL, the call is treated as a no-op (likefree(NULL)).The block header is read to determine the block’s order and offset.
The allocator’s accounting value
lenis decreased by the block size.If the buddy block (same order, XOR offset) is free, the two blocks are coalesced into a larger block, repeating until no further buddy merge is possible.
The resulting block is inserted into the appropriate free list.
buddy_t *b = init_buddy_allocator(4096, 64, 32); void *p = alloc_buddy(b, 200, false); if (!p) { perror("alloc_buddy"); free_buddy(b); return 1; } // ... use p ... if (!return_buddy_element(b, p)) { perror("return_buddy_element"); } free_buddy(b);
Note
After this operation,
ptrand any derived pointers become invalid.Warning
Passing a pointer that was not allocated by this allocator, or one that was already freed, results in undefined behavior.
- Parameters:
b – Pointer to a buddy_t allocator previously created by init_buddy_allocator(). Must not be NULL.
ptr – A user pointer returned by the buddy allocator’s allocation functions. May be NULL, in which case the function succeeds without action.
- Returns:
trueon success, orfalseon failure.
alloc_buddy_aligned
-
void_ptr_expect_t alloc_buddy_aligned(buddy_t *b, size_t size, size_t align, bool zeroed)
Allocate an aligned memory block from a buddy allocator.
This function allocates
sizebytes from the buddy allocatorband guarantees that the returned user pointer satisfies the requested alignment. On success, the result is returned via a void_ptr_expect_t value.The allocation request is internally expanded to accommodate:
the internal buddy_header_t (stored immediately before the user pointer),
the requested user payload,
worst-case padding required to satisfy the requested alignment.
Allocation proceeds as follows:
If
alignis zero, it defaults toalignof(max_align_t).If
alignis not a power of two, it is rounded up to the next power of two.The total required size (header + payload + alignment padding) is rounded up to at least the minimum buddy block size, then to the next power of two.
A sufficiently large free block is selected; larger blocks are split recursively until the desired order is reached.
An aligned user pointer is chosen within the block, and the allocation header is written immediately before it.
If
zeroedis true, the entire usable payload region following the user pointer is zero-initialized.
has_value== trueu.valuepoints to an aligned user-accessible memory region.has_value== falseu.errorindicates the reason for failure.
buddy_expect_t be = init_buddy_allocator(4096, 64, 32); if (!be.has_value) { // handle initialization error return; } buddy_t *b = be.u.value; // Allocate 128 bytes aligned to 64 bytes void_ptr_expect_t r = alloc_buddy_aligned(b, 128, 64, true); if (!r.has_value) { // handle allocation error via r.u.error free_buddy(b); return; } void *ptr = r.u.value; // ptr is 64-byte aligned and zero-initialized return_buddy(b, ptr); free_buddy(b);
Note
Memory returned by this function must be released using ::return_buddy(). Calling
free()on the returned pointer results in undefined behavior.Note
Alignment guarantees apply only to the returned user pointer. The internal allocation header may be unaligned and must not be accessed by user code.
- Parameters:
b – Pointer to a valid buddy_t allocator created by init_buddy_allocator(). Must not be NULL.
size – Number of user bytes requested. Must be greater than zero.
align – Required alignment for the returned user pointer. If zero, the allocator uses
alignof(max_align_t). Non–power-of-two values are rounded upward to the next power of two.zeroed – If true, the user-accessible portion of the allocation is filled with zeros. The internal header is never zeroed.
- Returns:
A void_ptr_expect_t result:
- Return values:
INVALID_ARG –
bis NULLsizeis zero
ALIGNMENT_ERROR –
aligncannot be normalized to a valid power-of-two alignment
CAPACITY_OVERFLOW –
Arithmetic overflow occurs while computing the required allocation size
BAD_ALLOC –
No suitable block exists
The requested size (after rounding and overhead) exceeds the pool size
reset_buddy
-
bool reset_buddy(buddy_t *b)
Reset a buddy allocator to its initial empty state.
This function restores the buddy allocator
bto the same effective state it had immediately after init_buddy_allocator(), without releasing or reallocating the underlying OS-backed memory pool. All outstanding allocations are logically discarded, all free lists are cleared, and the allocator is rebuilt with a single large free block covering the entire pool.This is analogous to a “phase reset” or “arena-style clear” operation: the allocator retains its pool, metadata arrays, alignment configuration, and block-order structure, but all allocated blocks become invalid.
The reset process performs the following steps:
Validate the allocator structure (non-NULL fields, non-zero pool size, valid order range).
Clear all free lists.
Insert one free block spanning the entire pool at the highest order.
Reset
b->lento 0, indicating no bytes are currently in use.
After this call, all previously returned user pointers become invalid and must not be dereferenced or passed to return_buddy_element().
buddy_t *b = init_buddy_allocator(4096, 64, 32); void *p1 = alloc_buddy(b, 100, false); void *p2 = alloc_buddy(b, 128, false); // Use memory ... // Completely discard all state and restore allocator to fresh condition if (!reset_buddy(b)) { perror("reset_buddy"); free_buddy(b); return 1; } // Now the pool is empty and alloc_buddy() can be used again. void *p3 = alloc_buddy(b, 200, true); return_buddy_element(b, p3); free_buddy(b);
Note
This operation does not release or reallocate OS memory; the pool is preserved as-is. Use free_buddy() to fully destroy the allocator and release all associated resources.
Note
Unlike return_buddy_element(), this function frees all allocations at once, regardless of their order or lifetime.
Warning
Calling return_buddy_element() on any pointer obtained before the reset is undefined behavior.
- Parameters:
b – Pointer to a buddy_t allocator to reset. Must not be NULL.
- Returns:
trueon success, orfalseon failure.
realloc_buddy_aligned
-
void_ptr_expect_t realloc_buddy_aligned(buddy_t *b, void *old_ptr, size_t old_size, size_t new_size, size_t align, bool zeroed)
Resize an aligned buddy allocation, preserving alignment and data.
Resizes a block previously allocated from
bwhile enforcing an alignment requirement for the resulting user pointer. This is a buddy-allocator analogue ofrealloc()with explicit alignment support.Semantics:
If
old_ptrisNULL:If
new_sizeis 0, the call succeeds and returns a successful result containingNULL.Otherwise, behaves like ::alloc_buddy_aligned(b, new_size, align, zeroed).
If
old_ptris non-NULL andnew_sizeis 0:Frees
old_ptrvia return_buddy_element() and succeeds with a successful result containingNULL.
If
new_sizefits within the existing block’s usable capacity andold_ptralready satisfies the normalized alignment, the allocation is reused in place and the returned pointer equalsold_ptr.Otherwise, a new aligned block is allocated, the first
min(old_size, usable_old)bytes are copied, the old block is returned, and the new pointer is returned.
Alignment handling:
If
alignis 0, it defaults toalignof(max_align_t).If
alignis not a power of two, it is rounded up to the next power of two.In-place reuse is taken only if
old_ptrsatisfies the normalized alignment.
The usable capacity of the old block is conservatively computed as:
whereusable_old = block_size - sizeof(buddy_header_t)
block_sizeis the power-of-two size of the underlying buddy block.Zeroing:
If the reallocation is reused in place and
zeroedis true andnew_size>old_size, the newly exposed tail bytes [old_size, new_size) are set to zero.If the allocation moves, zeroing behavior follows alloc_buddy_aligned() (i.e., the newly allocated block may be zero-initialized according to that function’s rules).
buddy_expect_t be = init_buddy_allocator(8192u, 64u, 32u); assert(be.has_value); buddy_t *b = be.u.value; size_t old_n = 128u; void_ptr_expect_t a = alloc_buddy_aligned(b, old_n, 64u, false); assert(a.has_value); void *p = a.u.value; // Grow to 512 bytes, maintaining 64-byte alignment, zeroing new tail if in-place void_ptr_expect_t r = realloc_buddy_aligned(b, p, old_n, 512u, 64u, true); if (!r.has_value) { // p is still valid here return_buddy_element(b, p); free_buddy(b); return 1; } void *p2 = r.u.value; // may be NULL only if new_size was 0 (not here) return_buddy_element(b, p2); free_buddy(b);
- Error codes
On failure,
.u.error is set to one of:INVALID_ARG
bis NULLold_ptris non-NULL butold_sizeis 0
ALIGNMENT_ERROR
aligncannot be normalized to a valid power of two
Any error propagated from alloc_buddy_aligned() (e.g. BAD_ALLOC, CAPACITY_OVERFLOW) when a move is required.
Note
If a move is required and allocation fails,
old_ptrremains valid and unchanged.Warning
Passing a pointer not owned by
b, or using a pointer after it has been returned to the buddy allocator, is undefined behavior.- Parameters:
b – Buddy allocator instance. Must not be NULL.
old_ptr – Pointer previously returned by alloc_buddy(), alloc_buddy_aligned(), realloc_buddy(), or realloc_buddy_aligned() on the same allocator, or NULL.
old_size – Logical size (in bytes) of the existing allocation as tracked by the caller. Must be > 0 when
old_ptris non-NULL.new_size – Requested new logical size in bytes. A value of 0 indicates “free”.
align – Required alignment for the resulting user pointer. 0 means
alignof(max_align_t). Non power-of-two values are rounded up.zeroed – If true, performs tail zeroing for in-place growth, and requests zero-initialization from alloc_buddy_aligned() when moving.
- Returns:
A void_ptr_expect_t result:
{.has_value=true, .u.value=<ptr>} on success (ptr may be NULL){.has_value=false, .u.error=<error_code_t>} on failure
Utility Funcitons
is_buddy_ptr
-
bool is_buddy_ptr(const buddy_t *b, const void *ptr)
Validate that a pointer comes from a specific buddy allocator.
This function performs a series of structural checks to determine whether
ptris a valid user pointer for the buddy allocatorb. It does not check whether the pointer currently refers to an allocated or freed block; it only verifies that the pointer is structurally consistent with the allocator’s layout and header conventions.The validation steps are:
bandptrmust be non-NULL.The internal buddy_header_t must reside immediately before
ptr.The header’s
ordermust be within [min_order,max_order].The header’s
block_offsetplus the block size must lie within the allocator’s pool size.The block offset must be correctly aligned for the block size (i.e., multiple of
2^order).The user pointer
ptrmust point inside the block, at or after the header and strictly before the block end.
If all checks succeed, the function returns
true.buddy_t *b = init_buddy_allocator(4096, 64, 32); void *p = alloc_buddy(b, 128, false); if (!p) { perror("alloc_buddy"); free_buddy(b); return 1; } if (!is_buddy_ptr(b, p)) { fprintf(stderr, "Pointer p is not a valid buddy pointer!\n"); } int local = 42; if (!is_buddy_ptr(b, &local)) { // expected to fail: &local is not from buddy allocator } return_buddy_element(b, p); free_buddy(b);
Note
This function does not track allocation state; it cannot distinguish between allocated and freed blocks. It only verifies that
ptrlooks like a pointer produced by this allocator instance.Warning
Passing arbitrary pointers (e.g., stack addresses or pointers from another allocator) is safe but will cause the function to return
false. Misuse of the buddy allocator API (such as double frees) may still result in undefined behavior elsewhere.- Parameters:
b – Pointer to a buddy_t allocator. Must not be NULL.
ptr – Candidate user pointer to validate. Must not be NULL.
- Returns:
trueifptris structurally valid forb,falseotherwise.
is_buddy_ptr_sized
-
bool is_buddy_ptr_sized(const buddy_t *b, const void *ptr, size_t size)
Validate that a pointer and size fit within a buddy block.
This function extends is_buddy_ptr() by additionally checking that a requested size
sizecan be safely stored within the underlying buddy block corresponding toptr.The following conditions are required for success:
is_buddy_ptr(
b,ptr) must succeed (i.e.,ptrmust be a structurally valid buddy pointer forb).The user-visible size
sizemust be less than or equal to the usable capacity of the block, defined as:usable = block_size - sizeof(buddy_header_t)where
block_sizeis2^orderderived from the internal header.
If both conditions hold, the function returns
true. Otherwise, it returnsfalse.buddy_t *b = init_buddy_allocator(4096, 64, 32); void *p = alloc_buddy(b, 128, false); if (!p) { perror("alloc_buddy"); free_buddy(b); return 1; } // This should succeed: we requested 128 bytes. if (!is_buddy_ptr_sized(b, p, 128)) { perror("is_buddy_ptr_sized"); } return_buddy_element(b, p); free_buddy(b);
Note
This function is particularly useful for debug assertions and defensive checks in higher-level code built on top of the buddy allocator, such as verifying that a client-provided size is consistent with an allocated block’s capacity.
- Parameters:
b – Pointer to a buddy_t allocator. Must not be NULL.
ptr – Candidate user pointer to validate. Must not be NULL and must satisfy is_buddy_ptr(
b,ptr).size – Requested logical size in bytes. Must fit within the usable region of the block.
- Returns:
trueifptris a valid buddy pointer forbandsizefits within the underlying block;falseotherwise.
buddy_stats
-
bool buddy_stats(const buddy_t *buddy, char *buffer, size_t buffer_size)
Format human-readable statistics for a buddy allocator.
This function writes a textual summary of the state of
buddyintobuffer, up tobuffer_sizebytes. The output is suitable for logging, debugging, or interactive diagnostics and mirrors the style of arena_stats().When
buddyis non-NULL, the report includes:Total pool size in bytes.
Minimum and maximum block sizes (in bytes).
Bytes currently “used” (sum of allocated block sizes).
Remaining bytes (pool size minus used).
Total memory including allocator overhead (
total_alloc).The size of the largest free block.
Utilization percentage (
used / pool_size * 100).Per-level free list details:
level index
block order and size
number of free blocks
total free bytes at that level
A summary line of the total free bytes across all free lists.
If
buddyis NULL, the function writes the line:"Buddy: NULL\n"and returns
trueas long as there is enough space inbuffer.If _buf_appendf() fails (e.g., due to insufficient buffer capacity), this function returns
false.char buf[1024]; buddy_t *b = init_buddy_allocator(4096, 64, 32); if (!b) { perror("init_buddy_allocator"); return 1; } // Perform some allocations void *p1 = alloc_buddy(b, 128, false); void *p2 = alloc_buddy(b, 256, false); if (!buddy_stats(b, buf, sizeof(buf))) { perror("buddy_stats"); } else { fputs(buf, stdout); } return_buddy_element(b, p1); return_buddy_element(b, p2); free_buddy(b);
Note
The function uses buddy_largest_block() to compute the largest free block and iterates each free list to derive per-level statistics.
Note
The string written into
bufferis not null-terminated if the underlying _buf_appendf() helper does not ensure this or if the buffer is exactly filled. Ensurebufferhas sufficient size for typical diagnostic output.- Parameters:
buddy – Pointer to the buddy_t allocator to inspect, or NULL. If NULL, a minimal
"Buddy: NULL\n"message is written.buffer – Destination character buffer for the formatted statistics. Must not be NULL.
buffer_size – Size of
bufferin bytes. Must be greater than zero.
- Returns:
trueif all output was successfully written tobuffer,falseif an error occurred (including insufficient buffer space).
Getter and Setter Functions
buddy_remaining
-
inline size_t buddy_remaining(const buddy_t *b)
Return the total amount of free memory remaining in the buddy pool.
This function reports how many bytes of the underlying buddy allocator’s memory pool are still unconsumed, regardless of fragmentation. The value is computed as:
where:remaining = pool_size - len
pool_sizeis the total allocatable memory in the buddy pool.lenis the sum of the full block sizes of all active allocations.
Because
lentracks block sizes (powers of two) rather than user-requested sizes, the returned value may differ from the sum of user-visible free space, but accurately reflects physical pool consumption.buddy_t *b = init_buddy_allocator(4096, 64, 32); if (!b) { perror("init_buddy_allocator"); return 1; } size_t free_bytes = buddy_remaining(b); printf("Remaining free memory: %zu bytes\n", free_bytes); free_buddy(b);
Warning
This value does not guarantee that a single allocation of the same size can succeed. Fragmentation may prevent large contiguous blocks from being available. To determine the largest contiguous block currently allocatable, use buddy_largest_block().
- Parameters:
b – Pointer to a valid buddy_t allocator. Must not be NULL.
- Returns:
The number of free bytes remaining in the pool, or
0on error.
buddy_size
-
inline size_t buddy_size(const buddy_t *b)
Return the overall memory size occupied by the buddy allocator.
This function simply returns the same value as ::buddy_alloc_total(). It exists to provide a naming parallel to
arena_size()and similar conventions in other allocators.printf("Buddy allocator size: %zu\n", buddy_size(b));
Note
Equivalent to calling ::buddy_alloc_total().
- Parameters:
b – Pointer to a buddy_t allocator. Must not be NULL.
- Returns:
Total size in bytes, or
0on error.
buddy_alloc
-
inline size_t buddy_alloc(const buddy_t *b)
Return the total number of bytes currently consumed from the buddy pool.
This function returns the allocator’s internal accounting value
b->len, which represents the sum of the full block sizes (power-of-two blocks) of all active allocations. This value includes internal fragmentation and header overhead.This function does not compute logical user-requested bytes; it reflects raw pool consumption.
size_t used = buddy_alloc(b); printf("Bytes currently consumed: %zu\n", used);
Note
This is analogous to “arena used” or “pool used” metrics in other allocators.
- Parameters:
b – Pointer to a buddy_t allocator. Must not be NULL.
- Returns:
The total number of bytes consumed from the pool, or
0on error.
total_buddy_alloc
-
inline size_t total_buddy_alloc(const buddy_t *b)
Return the total memory footprint of the buddy allocator including overhead.
This function returns
b->total_alloc, which includes:the OS-backed pool (
pool_size),the free-lists array,
the buddy_t struct itself,
All of these represent the full memory cost of the allocator’s existence.
printf("Allocator footprint (bytes): %zu\n", buddy_alloc_total(b));
Note
This value does not change during normal allocation or free operations; it only changes if the allocator’s metadata layout changes.
- Parameters:
b – Pointer to a buddy_t allocator. Must not be NULL.
- Returns:
The total footprint in bytes, or
0on error.
buddy_alignment
-
size_t buddy_alignment(const buddy_t *buddy)
Return the default alignment used by the buddy allocator.
This function returns the per-allocator base alignment value that was specified (or normalized) during init_buddy_allocator(). This alignment represents the minimum guarantee for all user pointers returned by alloc_buddy() and alloc_buddy_aligned().
The returned value is always a power of two, and is never less than
alignof(max_align_t). For strictly aligned allocations beyond this default, use alloc_buddy_aligned() or realloc_buddy_aligned().buddy_t *b = init_buddy_allocator(4096, 64, 32); if (!b) { perror("init_buddy_allocator"); return 1; } size_t a = buddy_alignment(b); printf("Buddy allocator default alignment: %zu bytes\n", a); free_buddy(b);
Note
The default alignment applies to normal allocations made via alloc_buddy(). Aligned allocations may request larger alignment values.
- Parameters:
buddy – Pointer to a valid buddy_t allocator. Must not be NULL.
- Returns:
The allocator’s default alignment in bytes, or
0on error.
buddy_largest_block
-
inline size_t buddy_largest_block(const buddy_t *b)
Return the size (in bytes) of the largest contiguous free block.
This function scans the buddy allocator’s free lists from the highest order (largest blocks) down to the lowest order, and returns the size of the largest block that is currently available for allocation. The size returned is a power of two and represents the maximum contiguous memory block that can be allocated without triggering a split or coalescing operation.
Unlike buddy_remaining(), which returns total free memory regardless of fragmentation, this function measures how large a single allocation may be in the allocator’s current state. If all free memory is fragmented into small blocks, this function will return a small value even if the total free memory is large.
buddy_t *b = init_buddy_allocator(4096, 64, 32); if (!b) { perror("init_buddy_allocator"); return 1; } void *p1 = alloc_buddy(b, 2000, false); // Splits large blocks size_t largest = buddy_largest_block(b); printf("Largest free block: %zu bytes\n", largest); return_buddy_element(b, p1); free_buddy(b);
Note
A return value of
0does not always mean the allocator is out of memory—it may simply be completely fragmented into blocks smaller than any requested size.- Parameters:
b – Pointer to a valid buddy_t allocator. Must not be NULL.
- Returns:
The size in bytes of the largest free block, or
0if no free blocks exist, or on error.
Buddy Context Functions
The following functions provide the arena-backed implementation of the
allocator vtable interface. They adapt an buddy_t instance to the generic
allocator API by exposing allocation, reallocation, and bulk deallocation
operations in a consistent form.
buddy_v_alloc
-
static inline void_ptr_expect_t buddy_v_alloc(void *ctx, size_t size, bool zeroed)
Allocate memory from a buddy allocator via allocator vtable.
This is a vtable adapter for alloc_buddy(). All errors are reported through the returned void_ptr_expect_t; this function never sets errno.
- Parameters:
ctx – Allocator context, expected to be a valid buddy_t pointer.
size – Number of user bytes requested.
zeroed – If true, the returned user region is zero-initialized.
- Returns:
A void_ptr_expect_t containing the allocated pointer on success, or an error code on failure.
buddy_v_alloc_aligned
-
static inline void_ptr_expect_t buddy_v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Allocate aligned memory from a buddy allocator via allocator vtable.
Vtable adapter for alloc_buddy_aligned(). Alignment normalization and allocation failures are reported via void_ptr_expect_t. This function does not read or modify errno.
- Parameters:
ctx – Allocator context (must be a valid buddy_t).
size – Number of user bytes requested.
align – Requested alignment (power-of-two or zero for natural alignment).
zeroed – If true, the user region is zero-initialized.
- Returns:
A void_ptr_expect_t containing the aligned pointer on success, or an error code on failure.
buddy_v_realloc
-
static inline void_ptr_expect_t buddy_v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Reallocate memory from a buddy allocator via allocator vtable.
Vtable adapter for realloc_buddy(). Behaves similarly to standard realloc semantics but reports all failures through void_ptr_expect_t. This function never uses errno.
- Parameters:
ctx – Allocator context (must be a valid buddy_t).
old_ptr – Pointer previously allocated from this allocator, or NULL.
old_size – Logical size of the existing allocation.
new_size – Requested new size in bytes.
zeroed – If true, any newly added bytes are zero-initialized.
- Returns:
A void_ptr_expect_t containing the resized pointer (which may be the same as
old_ptr) on success, or an error code on failure.
buddy_v_realloc_aligned
-
static inline void_ptr_expect_t buddy_v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Reallocate aligned memory from a buddy allocator via allocator vtable.
Vtable adapter for realloc_buddy_aligned(). Preserves alignment constraints when resizing and reports errors via void_ptr_expect_t. No errno usage.
- Parameters:
ctx – Allocator context (must be a valid buddy_t).
old_ptr – Pointer previously allocated from this allocator, or NULL.
old_size – Logical size of the existing allocation.
new_size – Requested new size in bytes.
zeroed – If true, newly added bytes are zero-initialized.
align – Required alignment for the resulting pointer.
- Returns:
A void_ptr_expect_t containing the resized, aligned pointer on success, or an error code on failure.
buddy_v_return
-
static inline void buddy_v_return(void *ctx, void *ptr)
Return an allocated block to a buddy allocator via allocator vtable.
Vtable adapter for return_buddy_element(). Invalid inputs are treated as a no-op. This function does not report errors and does not use errno.
- Parameters:
ctx – Allocator context (must be a valid buddy_t).
ptr – Pointer previously returned by this allocator.
buddy_v_free
-
static inline void buddy_v_free(void *ctx)
Destroy a buddy allocator via allocator vtable.
Vtable adapter for free_buddy(). If the context is NULL, the function performs no action. This function does not report errors and does not use errno.
- Parameters:
ctx – Allocator context (must be a valid buddy_t).
Slab Allocator Overview
The slab_t allocator implements a fixed-size object memory allocation
model. Memory is acquired from an underlying allocator (typically a
buddy_t allocator) in page-sized chunks called slabs. Each slab is
subdivided into uniformly-sized slots, and allocations simply return the next
available slot.
This design provides fast, predictable memory behavior for workloads where objects have the same size and are frequently allocated and freed.
This model is ideal when:
many objects share the same fixed size
allocation and deallocation occur frequently
high throughput and predictable memory reuse are required
fragmentation must be minimized
Examples include:
kernel-style data structure allocation (inspired by the Linux slab allocator)
fixed-size request/response messaging buffers
game engine entities or component storage pools
allocator back-ends for frameworks with small, frequently-used structs
real-time or embedded systems where
malloc/freeoverhead is undesirable
Benefits
Very fast allocation and free (constant time; usually a pointer pop/push from a free-list)
No external fragmentation, since all slots are equal size
Cache-friendly layout (objects of the same type are tightly packed in memory)
Stable pointer addresses while slabs remain allocated
Deterministic memory usage for a class of fixed-size objects
Limitations
Supports fixed-size allocations only (all objects must fit within one slot)
Reallocation is not supported (slots cannot grow; callers must allocate/copy/free manually)
May exhibit internal fragmentation if objects do not use the entire slot
Memory consumption grows in units of full slabs (pages)
Data Types
The following are data structures and derived data types used in the c_allocator.h
and c_allocator.c files to support the arena_t data type.
slab_t
slab_t is an opaque data structure that is not visibile to the user. This struct
contains metadata for the slab allocator type.
typedef struct slab_t {
buddy_t *buddy; /* backing buddy allocator */
size_t obj_size; /* user-visible object size */
size_t slot_size; /* internal slot size (>= obj_size and >= sizeof(slab_slot_t)) */
size_t align; /* slot alignment (power of two) */
size_t slab_bytes; /* total bytes in each slab page (buddy allocation size we carve up) */
size_t page_hdr_bytes;/* bytes reserved for slab_page header (aligned) */
size_t objs_per_slab;/* slots per page */
size_t len; /* bytes currently in-use by this slab (obj_size * live objects) */
slab_page_t *pages; /* linked list of pages (headers sit at page base) */
slab_slot_t *free_list;/* global free-list of free slots */
} slab_t;
slab_expect_t
slab_expect_t is an error handling struct to be used in the creation
of slab_t data types to catch and convey errors to a user.
typedef struct {
bool has_value;
union {
slab_t* value;
error_code_t error;
} u;
} slab_expect_t;
Initialization and Memory Management
The functions in this section can be used to initialize memory for a slab allocator, parse that memory to variables.
init_slab_allocator
-
slab_expect_t init_slab_allocator(buddy_t *buddy, size_t obj_size, size_t align, size_t slab_bytes_hint)
Initialize a slab allocator backed by a buddy allocator.
Constructs a new slab_t instance whose control structure is allocated from the buddy allocator referenced by
buddy. The slab allocator manages fixed-size objects of sizeobj_size, with per-object alignment of at leastalignbytes. If the code is compiled with aSTATIC_ONLYflag, none of the slab allocator functions will compile with the code base.The slab allocator does not own the underlying memory pool. All slab pages are obtained from the caller-provided buddy_t instance, and the slab allocator remains valid only for the lifetime of that buddy allocator.
Alignment normalization rules:
If
alignis 0,alignof(max_align_t)is used.If
alignis not a power of two, it is rounded up to the next power of two.If alignment normalization fails, initialization fails.
Each slab page contains:
A page header of size at least
sizeof(slab_page_t), aligned toalign.A contiguous array of fixed-size slots, each of size:
rounded up tomax(obj_size, sizeof(slab_slot_t))
align.
The
slab_bytes_hintparameter influences the total size of each slab page:If 0, a default minimum is selected (at least 64 slots or 4 KiB, whichever is larger).
If too small to contain even a single slot, it is increased automatically.
The final slab size is adjusted so that the slot region contains an integer number of slots (no tail fragments).
On successful initialization, the returned slab_t has:
No allocated pages (pages are created lazily).
An empty free list.
Zero bytes in use (
slab->len == 0).
Allocation from the slab is performed using alloc_slab() and related functions, which return a void_ptr_expect_t describing either a valid object pointer or an error code.
has_value == truewithvaluepointing to a fully initialized slab_t on success.has_value == falsewitherrorset to:INVALID_ARG — invalid arguments (NULL buddy, zero size)
ALIGNMENT_ERROR — alignment normalization failure
BAD_ALLOC — underlying buddy allocation failure
#include "buddy.h" #include "slab.h" typedef struct { int id; float value; } my_object_t; int main(void) { buddy_expect_t bexpect = init_buddy_allocator(1 << 20, 64, 0); if (!bexpect.has_value) { return 1; } buddy_t *buddy = bexpect.u.value; slab_expect_t sexpect = init_slab_allocator(buddy, sizeof(my_object_t), alignof(my_object_t), 0); if (!sexpect.has_value) { free_buddy(buddy); return 1; } slab_t *slab = sexpect.u.value; void_ptr_expect_t a = alloc_slab(slab, true); void_ptr_expect_t b = alloc_slab(slab, true); if (a.has_value && b.has_value) { my_object_t *oa = a.u.value; my_object_t *ob = b.u.value; oa->id = 1; ob->id = 2; } return_slab(slab, a.u.value); return_slab(slab, b.u.value); free_buddy(buddy); return 0; }
- Example
Creating and using a slab allocator:
Note
The slab allocator does not free memory itself. Destroying the backing buddy allocator invalidates all slab pages and allocations.
- Parameters:
buddy – Pointer to a valid buddy_t allocator supplying memory for slab pages.
obj_size – Size (in bytes) of each managed object. Must be non-zero.
align – Required alignment for stored objects. If 0,
alignof(max_align_t)is used. Non–power-of-two values are rounded up.slab_bytes_hint – Optional hint for the size (in bytes) of each slab page. If 0, a default size is computed automatically.
- Returns:
A slab_expect_t describing the result:
alloc_slab
-
void_ptr_expect_t alloc_slab(slab_t *slab, bool zeroed)
Allocate a fixed-size object from a slab allocator.
Obtains a single object from the slab allocator referenced by
slab. Slab allocators manage objects of a fixed size (specified during init_slab_allocator()) and maintain an internal free list of available slots. If no free slots remain, the allocator automatically attempts to grow by acquiring a new slab page from the backing buddy allocator via _slab_grow().On success, this function:
Removes one slot from the slab’s free list.
Optionally zero-initializes the object if
zeroedis true.Increments the internal usage counter by
slab->obj_size.
The returned pointer is aligned according to the slab’s configured alignment and must later be returned using return_slab().
On success,
has_value == trueandu.valuecontains a pointer to the allocated object.On failure,
has_value == falseandu.errorcontains an error_code_t describing the reason for failure.
- Error Handling
This function does not use
errno. Instead, it returns a void_ptr_expect_t result:
Possible error codes include (but are not limited to):
INVALID_ARG
slabis NULL
BAD_ALLOC
Slab growth failed
The backing buddy allocator could not provide memory
In all failure cases, the observable state of the slab allocator remains unchanged, except for any internal bookkeeping performed by _slab_grow() prior to failure.
#include "buddy.h" #include "slab.h" typedef struct { int id; float value; } my_object_t; int main(void) { buddy_t *buddy = init_buddy_allocator( 1 << 20, // pool size 64, // minimum block size 0 // natural alignment ); if (!buddy) { return 1; } slab_t *slab = init_slab_allocator( buddy, sizeof(my_object_t), alignof(my_object_t), 0 ); if (!slab) { free_buddy(buddy); return 1; } void_ptr_expect_t a = alloc_slab(slab, true); void_ptr_expect_t b = alloc_slab(slab, false); if (!a.has_value || !b.has_value) { // handle allocation failure } else { my_object_t *obj_a = a.u.value; my_object_t *obj_b = b.u.value; obj_a->id = 1; obj_b->id = 2; return_slab(slab, obj_a); return_slab(slab, obj_b); } reset_slab(slab); free_buddy(buddy); return 0; }
- Example
Allocating and returning objects from a slab allocator:
- Parameters:
slab – Pointer to an initialized slab_t instance.
zeroed – If true, the returned object is zero-initialized to the slab’s configured object size.
- Returns:
A void_ptr_expect_t containing either:
u.value— pointer to the allocated object on success, oru.error— an error_code_t describing the failure.
return_slab
-
bool return_slab(slab_t *slab, void *ptr)
Return an object to a slab allocator.
Returns a previously allocated object back to the slab allocator referenced by
slab. The pointer must have been obtained from alloc_slab() on the same slab allocator instance.Returning an object:
Pushes the slot back onto the slab’s global free list.
Decrements the internal usage counter (
slab->len) by the object size.
This function performs strict pointer validation to ensure that
ptrrefers to a valid slot inside a slab page managed byslab. This includes:Checking that the page exists and belongs to this slab.
Checking that
ptrlies inside the slot region of the page.Verifying that
ptris aligned on a slot boundary.
When returning an object,
slab->lenis decreased by the object size. If the counter underflows (should not happen), it is clamped to zero defensively.- Special Cases
If
slabis NULL, the function returns false.If
ptris NULL, the call is treated likefree(NULL)and returns true with no effect.
- Error Handling
The function returns false.
ptrdoes not belong to any page owned by the slab allocator.ptrfalls outside the slot region of a matching page.ptris misaligned (not on a slot boundary).
typedef struct { int id; float value; } my_object_t; buddy_t *buddy = init_buddy_allocator(1<<20, 64, 0); if (!buddy) { perror("init_buddy_allocator"); return 1; } slab_t *slab = init_slab_allocator( buddy, sizeof(my_object_t), alignof(my_object_t), 0 // default page size ); my_object_t *a = alloc_slab(slab, true); my_object_t *b = alloc_slab(slab, true); // Use objects a and b... // Return them to the slab allocator: if (!return_slab(slab, a)) { perror("return_slab(a)"); } if (!return_slab(slab, b)) { perror("return_slab(b)"); } // Reset the slab or destroy the buddy allocator when finished. reset_slab(slab); free_buddy(buddy);
- Example
The following example allocates then frees several objects:
- Parameters:
slab – Pointer to an initialized slab_t.
ptr – Pointer to an object previously returned by alloc_slab(). May be NULL.
- Return values:
true – Object successfully returned to the slab allocator.
false – Invalid pointer.
Utility Funcitons
is_slab_ptr
-
bool is_slab_ptr(const slab_t *slab, const void *ptr)
Determine whether a pointer was allocated by a given slab allocator.
Returns true if
ptrrefers to a valid object allocated from the slab allocator referenced byslab. The function performs strict validation:These checks ensure that the pointer was returned by alloc_slab() and has not been corrupted or fabricated.
- Validation Rules
The pointer must belong to one of the slab’s pages.
It must fall after the page header region and before the end of the page.
It must be aligned on a slot boundary:
(ptr - slots_start) % slot_size == 0
The pointer must not lie in the header region of the page.
- Parameters:
slab – Pointer to an initialized slab_t instance.
ptr – Pointer to validate.
- Return values:
true –
ptris a valid object allocated fromslab.false – Pointer is invalid or does not belong to this slab.
reset_slab
-
bool reset_slab(slab_t *slab)
Reset a slab allocator, returning all slots to the free list.
Reinitializes the internal state of the slab allocator referenced by
slabso that:All pages remain allocated.
All slots in every page are placed back onto the global free list.
No objects are considered “in use” (
slab->len == 0).
This is a bulk “clear” operation: it does not release any memory to the backing allocator, but makes all capacity immediately available for reuse.
After completion:
slab->len == 0slab->free_listreferences all slots across all pages.
- Geometry Checks
The function first verifies that the slab geometry is valid:
obj_size != 0slot_size != 0slab_bytes >= page_hdr_bytes + slot_size
- Behavior
For each page in the slab:
The slots region is recomputed as the range
[page_base + page_hdr_bytes, page_base + slab_bytes).This region is carved into chunks of
slot_sizebytes.Each chunk is pushed onto the slab’s global free list.
- Error Handling
If
slabis NULL, returns false.If the geometry checks fail, returns false.
- Example
// Assume slab has been created and used previously: slab_t *slab = init_slab_allocator(buddy, sizeof(my_object_t), alignof(my_object_t), 0); my_object_t *a = alloc_slab(slab, true); my_object_t *b = alloc_slab(slab, true); // Use a, b... // Now discard all live objects and recycle all slots: if (!reset_slab(slab)) { perror("reset_slab"); } // After reset, slab_in_use_blocks(slab) == 0 and all slots are free.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
true – Reset succeeded; all slots are free and slab->len == 0.
false – Invalid slab or geometry.
save_slab
-
bool save_slab(const slab_t *slab, void *buffer, size_t buffer_size, size_t *bytes_needed)
Serialize the state of a slab allocator into a caller-provided buffer.
Captures a snapshot of the slab allocator referenced by
slabinto a contiguous memory region provided by the caller. The snapshot consists of:A copy of the slab_t control structure.
A copy of each slab page (in list order), each of size
slab->slab_bytes.
The snapshot is intended for in-process checkpointing and later restoration via restore_slab(). It is not a stable, portable serialization format: it contains raw pointers and is only valid within the same process and allocator lifetime.
size_t bytes_needed = 0; void *buffer = malloc(bytes_needed); if (!buffer) { // handle error } if (!save_slab(slab, buffer, bytes_needed, &bytes_needed)) { perror("save_slab (snapshot)"); }
- Size Query Pattern
The function always computes the number of bytes required to store the snapshot and writes it to
bytes_needed. This allows a two-pass pattern:
On success, the snapshot is written to
bufferand the function returns true.- Error Handling
If
slabis NULL orbytes_neededis NULL.If
bufferis NULL orbuffer_sizeis smaller than the required size, returns false.
- Parameters:
slab – Pointer to an initialized slab_t instance.
buffer – Destination buffer for the snapshot (may be NULL for size query only).
buffer_size – Size of
bufferin bytes.bytes_needed – Output: number of bytes required to store the snapshot.
- Return values:
true – Snapshot successfully written to
buffer.
restore_slab
-
bool restore_slab(slab_t *slab, const void *buffer, size_t buffer_size)
Restore a slab allocator from a previously saved snapshot.
Restores the state of the slab allocator referenced by
slabfrom the snapshot stored inbuffer. The snapshot must have been created by save_slab() for the same slab instance, within the same process, and without changes to the underlying page layout since the snapshot.On success,
slab’sinternal state (including pages and free list) is restored to the moment when save_slab() was called.- Important Limitations
The snapshot is not portable across processes, architectures, or allocator instances; it contains raw pointers.
The snapshot assumes that:
The same pages are still allocated at the same addresses.
The slab geometry (object size, alignment, page size, etc.) has not changed between save and restore.
This function is suitable for in-process checkpoint/rollback scenarios, not for persistent on-disk serialization.
- Restore Procedure (High-Level)
Copy a temporary snapshot header (
slab_t) frombuffer.Use the header’s page list (
snap_header.pages) to count the number of pages that must be restored.Verify that
buffercontains enough data for the header and all pages.Verify that the current
slabhas the same geometry as the snapshot (object size, slot size, alignment, slab bytes, header bytes, objects per slab).For each page in the snapshot’s page list, copy the saved page contents into the live page at the same address.
Finally, overwrite the live slab_t with the snapshot header.
- Example
// Assume slab has been created and used: slab_t *slab = init_slab_allocator(buddy, sizeof(my_object_t), alignof(my_object_t), 0); // ... allocate and use objects ... // 1) Take a snapshot: size_t bytes_needed = 0; (void)save_slab(slab, NULL, 0, &bytes_needed); // expect ERANGE void *snapshot = malloc(bytes_needed); if (!snapshot) { perror("malloc snapshot"); // handle error... } if (!save_slab(slab, snapshot, bytes_needed, &bytes_needed)) { perror("save_slab"); } // ... mutate slab: allocate/free some objects ... // 2) Restore previous state: if (!restore_slab(slab, snapshot, bytes_needed)) { perror("restore_slab"); } free(snapshot);
- Parameters:
slab – Pointer to the live slab_t instance to restore.
buffer – Pointer to the snapshot buffer previously filled by save_slab().
buffer_size – Size of
bufferin bytes.
- Return values:
true – Slab successfully restored from the snapshot.
slab_stats
-
bool slab_stats(const slab_t *slab, char *buffer, size_t buffer_size)
Format human-readable statistics for a slab allocator.
Writes a textual summary of the slab allocator referenced by
slabinto the caller-provided character bufferbuffer. The summary includes geometry (object size, slot stride, alignment, page size), capacity, usage, block counts, utilization, and a per-page listing.The output is intended for debugging, logging, and diagnostic tools. It is not meant to be parsed programmatically.
If
slabis NULL, the function writes:- Buffer Semantics
buffermust be non-NULL.buffer_sizemust be greater than 0.Output is appended using _buf_appendf(), which ensures that the buffer is not overrun and that it is always NUL-terminated on success.
Slab: NULL\n
to
bufferand returns true.Object size (bytes)
Slot stride (bytes)
Alignment (bytes)
Page size (bytes) and page header bytes
Page count
Blocks per page and total blocks
In-use blocks
Free blocks (geometric, derived from totals)
Free blocks (counted from free list, for cross-check)
Used bytes, capacity bytes, remaining bytes
Total footprint (including slab_t struct)
Utilization percentage (used / capacity)
Per-page summary: size and block count for each page
- Reported Fields
The function reports, among others:
- Error Handling
If
bufferis NULL orbuffer_sizeis 0.If
slabis NULL, a simple “Slab: NULL” line is written and the function returns true (no error).If any call to _buf_appendf() fails (e.g., due to insufficient space), the function returns false. errno is not modified by slab_stats() in that case; callers may consult their own logging or wrap _buf_appendf.
- Example
#include <stdio.h> void dump_slab_stats(const slab_t *slab) { char buf[1024]; if (!slab_stats(slab, buf, sizeof(buf))) { perror("slab_stats"); return; } // Print the formatted statistics. fputs(buf, stdout); } int main(void) { buddy_t *buddy = init_buddy_allocator(1<<20, 64, 0); if (!buddy) { perror("init_buddy_allocator"); return 1; } slab_t *slab = init_slab_allocator( buddy, sizeof(int), // object size alignof(int), // alignment 0 // let slab choose page size ); if (!slab) { perror("init_slab_allocator"); free_buddy(buddy); return 1; } // Use the slab: allocate a few ints. int *a = alloc_slab(slab, true); int *b = alloc_slab(slab, true); if (a) *a = 42; if (b) *b = 7; // Dump statistics to stdout. dump_slab_stats(slab); // Clean up. return_slab(slab, a); return_slab(slab, b); reset_slab(slab); free_buddy(buddy); return 0; }
- Parameters:
slab – Pointer to an initialized slab_t instance, or NULL.
buffer – Output buffer for the formatted statistics string.
buffer_size – Size of
bufferin bytes.
- Return values:
true – Statistics successfully written into
buffer.
Getter and Setter Functions
slab_size
-
inline size_t slab_size(const slab_t *slab)
Query the total payload capacity of all pages in a slab allocator.
Returns the total number of bytes reserved for slots across all pages managed by
slab. This is computed as:the number of pages currently allocated, multiplied by
the configured per-page size (
slab->slab_bytes).
This value represents the gross capacity of the slab pages, including both used and free slots, but excluding the memory occupied by the slab_t struct itself.
- Relationship to Other Queries
slab_alloc() reports the subset of this capacity currently in use by live objects.
total_slab_alloc() includes both page capacity and the control structure overhead.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Total size in bytes of all slab pages currently allocated, or 01.
slab_alloc
-
inline size_t slab_alloc(const slab_t *slab)
Query the number of logical bytes currently in use by a slab allocator.
Returns the total number of bytes of user payload currently allocated from the slab allocator referenced by
slab. This value is computed as the number of live objects multiplied by the configured object size (not the internal slot stride).In other words, this reports how many bytes of user data are “in use” from the slab allocator’s point of view, not counting internal metadata, page headers, or unused slots.
- Error Handling
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Number of logical payload bytes currently in use, or 0.
total_slab_alloc
-
inline size_t total_slab_alloc(const slab_t *slab)
Query the full memory footprint of a slab allocator.
Returns the total number of bytes consumed by the slab allocator itself, including:
the aligned size of the slab_t control structure, and
the size of all currently allocated pages (slabs).
This is the quantity you would typically use to answer “how much memory
does this slab allocator occupy in total?”, including metadata and capacity for future allocations.
- Relationship to Other Queries
slab_alloc() reports the logical payload bytes in use.
slab_size() reports only the total size of the pages, excluding the control structure.
total_slab_alloc() includes both the control structure and all pages.
- Error Handling
If
slabis NULL, the function returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Total bytes consumed by the slab allocator, including metadata and pages, or 0 on error.
slab_stride
-
inline size_t slab_stride(const slab_t *slab)
Query the per-slot stride (capacity) of a slab allocator.
Returns the internal slot size used by
slab. This stride is:at least as large as the configured object size, and
large enough to hold internal free-list linkage (e.g., slab_slot_t).
The slot stride may therefore be greater than the nominal object size if additional padding or metadata is required.
- Usage
This function is useful for:
Validating that requested logical sizes do not exceed the slot capacity when using a generic allocator interface.
Understanding internal memory layout for debugging or statistics.
- Error Handling
If
slabis NULL, the function returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Slot stride in bytes, or 0.
slab_total_blocks
-
inline size_t slab_total_blocks(const slab_t *slab)
Query the total number of slots managed by a slab allocator.
Returns the total capacity of
slabin units of objects (slots), across all currently allocated pages. This is computed as:the number of pages currently allocated, multiplied by
the number of objects per page (
objs_per_slab).
In other words, this is the maximum number of objects that could be simultaneously allocated from the slab without growing (adding new pages).
- Relationship to Other Queries
slab_total_blocks() reports the total number of slots (used + free).
slab_alloc() / object size gives an upper bound on the number of currently allocated objects.
A complementary function (e.g., slab_free_blocks()) can be used to compute the number of free slots:
free_slots = slab_total_blocks(slab) - in_use_slots.
- Error Handling
If
slabis NULL, the function returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Total number of slots (objects) available across all pages, or 0 on error.
slab_free_blocks
-
inline size_t slab_free_blocks(const slab_t *slab)
Query the number of free (unallocated) slots in a slab allocator.
Returns the number of currently available allocation slots in
slab. This is computed by walking the slab’s global free list and counting the number of entries. Each entry corresponds to one free slot within some slab page.- Relationship to Other Queries
slab_total_blocks() returns the total slot capacity.
slab_in_use_blocks() returns the number of allocated slots.
Therefore:
free_blocks == slab_total_blocks(slab) - slab_in_use_blocks(slab)
- Performance
This function runs in O(n) time, where n is the number of free slots. It is intended for diagnostics, assertions, and statistics—not hot paths.
- Error Handling
If
slabis NULL, the function returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Number of free slots, or 0 on error.
slab_in_use_blocks
-
inline size_t slab_in_use_blocks(const slab_t *slab)
Query the number of currently allocated objects (slots) in a slab.
Returns the number of slots currently in use by dividing
slab->len— the total logical bytes in use—by the object size configured when the slab was created.This gives the number of currently allocated objects, not counting the free list or empty slots in partially used pages.
- Relationship to Other Queries
slab_total_blocks() gives the total slot capacity.
slab_free_blocks() gives the number of free slots.
slab_in_use_blocks() reports how many are actively allocated.
- Error Handling
If
slabis NULL, returns 0.If the slab’s object size is 0 (should never happen), returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Number of allocated objects, or 0 on error.
slab_alignment
-
inline size_t slab_alignment(const slab_t *slab)
Query the object alignment of a slab allocator.
Returns the minimum alignment (in bytes) guaranteed for all objects allocated from
slab. This alignment is determined when the slab allocator is created via init_slab_allocator().- Usage
Useful when integrating the slab allocator with generic allocator APIs.
Ensures that allocated objects meet the required ABI alignment.
- Error Handling
If
slabis NULL, the function returns 0.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Return values:
size_t – Alignment in bytes, or 0 on error.
Slab Context Functions
The following functions provide the arena-backed implementation of the
allocator vtable interface. They adapt an slab_t instance to the generic
allocator API by exposing allocation, reallocation, and bulk deallocation
operations in a consistent form.
slab_v_alloc
-
static inline void_ptr_expect_t slab_v_alloc(void *ctx, size_t size, bool zeroed)
Allocate a slab-backed object through an allocator vtable.
Allocates a single fixed-size slot from the slab associated with the allocator context. The requested
sizeis validated against the slab slot capacity but does not affect the actual allocation size.- Parameters:
ctx – Allocator context; must point to a valid slab_t.
size – Requested logical size (must be > 0 and <= slab slot size).
zeroed – If true, the object payload is zero-initialized.
- Returns:
A void_ptr_expect_t containing a pointer to the allocated object on success, or an error code on failure.
slab_v_alloc_aligned
-
static inline void_ptr_expect_t slab_v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Allocate a slab-backed object with an alignment constraint.
Behaves like slab_v_alloc but additionally validates that the requested alignment can be satisfied by the slab’s fixed object alignment. No additional alignment is performed.
- Parameters:
ctx – Allocator context; must point to a valid slab_t.
size – Requested logical size (must fit within a slab slot).
align – Requested alignment (must be <= slab alignment).
zeroed – If true, the object payload is zero-initialized.
- Returns:
A void_ptr_expect_t containing a pointer to the allocated object on success, or an error code on failure.
slab_v_realloc
-
static inline void_ptr_expect_t slab_v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Reallocate a slab-backed allocation through an allocator vtable.
Slab reallocations do not move memory. If the new size fits within the slab slot, the original pointer is returned unchanged.
- Parameters:
ctx – Allocator context; must point to a valid slab_t.
old_ptr – Previously allocated slab object, or NULL.
old_size – Logical size of the existing allocation.
new_size – Requested new logical size.
zeroed – If true, newly exposed bytes are zeroed.
- Returns:
A void_ptr_expect_t containing the resulting pointer (which may equal
old_ptr), or an error code on failure.
slab_v_realloc_aligned
-
static inline void_ptr_expect_t slab_v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Reallocate a slab-backed allocation with an alignment constraint.
Behaves like slab_v_realloc but additionally validates that the existing pointer satisfies the requested alignment. No relocation is performed.
- Parameters:
ctx – Allocator context; must point to a valid slab_t.
old_ptr – Previously allocated slab object, or NULL.
old_size – Logical size of the existing allocation.
new_size – Requested new logical size.
zeroed – If true, newly exposed bytes are zeroed.
align – Required alignment (must be <= slab alignment).
- Returns:
A void_ptr_expect_t containing the resulting pointer, or an error code on failure.
slab_v_return
-
static inline void slab_v_return(void *ctx, void *ptr)
Return a slab-backed allocation via an allocator vtable.
Returns the given object to the slab’s free list. The pointer must have been obtained from the same slab instance.
- Parameters:
ctx – Allocator context; must point to a valid slab_t.
ptr – Pointer to an object previously allocated from the slab.
slab_v_free
-
static inline void slab_v_free(void *ctx)
Reset a slab allocator through an allocator vtable.
Releases all slab allocations by resetting internal slab state. The backing buddy allocator is not destroyed.
- Parameters:
ctx – Allocator context; must point to a valid slab_t.
slab_allocator
-
static inline allocator_vtable_t slab_allocator(slab_t *slab)
Construct an allocator vtable backed by a slab allocator.
Returns an allocator_vtable_t whose operations delegate to the provided slab allocator. All allocations are fixed-size and managed by the slab.
- Parameters:
slab – Pointer to an initialized slab_t instance.
- Returns:
An allocator_vtable_t configured to use the slab allocator.
Context Function Pointers
The CSALT allocator framework supports multiple allocation backends (arena, pool, dynamic arena, system heap, etc.). To provide a uniform, “drop-in” interface for all allocator types, the library defines a generic context-based dispatch table. This table contains function pointers corresponding to the fundamental operations that any allocator must implement.
These function pointers are grouped into a single structure,
allocator_vtable_t, which acts as a virtual method table (vtable)
for memory management. Each allocator fills in the appropriate
function pointers and provides an opaque ctx pointer referencing
its internal state (arena instance, pool instance, system-heap adapter,
and so on).
typedef struct {
alloc_prototype allocate;
alloc_aligned_prototype allocate_aligned;
realloc_prototype reallocate;
realloc_aligned_prototype reallocate_aligned;
return_prototype return_element;
free_prototype deallocate;
void* ctx; // backing arena, pool, system heap, etc.
} allocator_vtable_t;
The following sections document each function-pointer type in detail.
alloc_prototype
-
typedef void_ptr_expect_t (*alloc_prototype)(void *ctx, size_t size, bool zeroed)
Prototype for allocating a block of memory.
This function allocates
sizebytes using the allocator identified byctx. Ifzeroedis true, the returned memory must be- Param ctx:
Allocator context (arena, pool, heap wrapper, etc.)
- Param size:
Number of bytes to allocate
- Param zeroed:
If true, memory must be zero-initialized
- Return:
void_ptr_expect_t data type with a pointer to data or errors
alloc_aligned_prototype
-
typedef void_ptr_expect_t (*alloc_aligned_prototype)(void *ctx, size_t size, size_t align, bool zeroed)
Prototype for aligned allocation.
This function allocates
sizebytes with at leastalignalignment. Alignment must be a power of two. Ifzeroedis true, the returned memory must be zero-initialized.- Param ctx:
Allocator context
- Param size:
Number of bytes to allocate
- Param align:
Required alignment (power of two)
- Param zeroed:
Whether the returned memory must be zero-initialized
- Return:
void_ptr_expect_t data type with a pointer to data or errors
realloc_prototype
-
typedef void_ptr_expect_t (*realloc_prototype)(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Prototype for resizing an existing allocation.
This function resizes a previously allocated block. Implementations may move the allocation; if they do, the old contents up to
min(old_size, new_size)must remain intact. Ifzeroedis true andnew_size > old_size, the newly added region must be zeroed.- Param ctx:
Allocator context
- Param old_ptr:
Pointer to existing allocation (may be NULL)
- Param old_size:
Previously allocated size
- Param new_size:
Requested new size
- Param zeroed:
Whether any expanded memory must be zero-initialized
- Return:
void_ptr_expect_t data type with a pointer to data or errors
realloc_aligned_prototype
-
typedef void_ptr_expect_t (*realloc_aligned_prototype)(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Prototype for aligned reallocation.
This function behaves like realloc_prototype but also enforces a minimum alignment requirement for the resulting allocation.
alignmust be a power of two. If expanded, new memory must be zeroed whenzeroedis true.- Param ctx:
Allocator context
- Param old_ptr:
Pointer to an existing allocation
- Param old_size:
Previous allocation size
- Param new_size:
Requested new size
- Param zeroed:
Whether expanded memory must be zero-initialized
- Param align:
Required alignment (power of two)
- Return:
void_ptr_expect_t data type with a pointer to data or errors
return_prototype
-
typedef void (*return_prototype)(void *ctx, void *ptr)
Prototype for returning (or releasing) memory back to the allocator.
Some allocators may support returning individual blocks for reuse (e.g., free lists, object pools). Others may ignore this operation.
This function must not free the memory using the system allocator unless that is appropriate for the backing implementation.
- Param ctx:
Allocator context
- Param ptr:
Memory block previously allocated by this allocator
free_prototype
-
typedef void (*free_prototype)(void *ctx)
Prototype for tearing down or freeing an allocator.
This function releases any resources held by the allocator identified by
ctx. For arena- or pool-style allocators, this typically frees all chunks in bulk. After this call, the allocator context must no longer be used.- Param ctx:
Allocator context to destroy
Standard Allocator Implementation
The allocator_vtable_t type is meant for robust custom allocators but it
can also be used for standard malloc, realloc and free functions that
are part of the standard C library.
v_alloc
-
static inline void_ptr_expect_t v_alloc(void *ctx, size_t size, bool zeroed)
Allocate a heap block and return it as a void_ptr_expect_t.
This is a thin wrapper around
malloc()that returns a structured void_ptr_expect_t result instead of a raw pointer. It provides:INVALID_ARGwhensize== 0.BAD_ALLOCwhenmalloc()fails.
On success, the block is optionally zero-initialized.
The
ctxparameter is accepted for compatibility with generic allocator vtable interfaces but is ignored in this simple backend.void_ptr_expect_t r = v_alloc(NULL, 256u, true); if (!r.has_value) { fprintf(stderr, "alloc failed: %d\n", r.u.error); return; } void *p = r.u.value; // use p ... free(p);
Note
The caller is responsible for eventually freeing the returned block with
free(), or managing it according to the higher-level system that uses this backend.This function does not track or validate ownership.
- Parameters:
ctx – Optional allocator context (unused by this implementation).
size – Number of bytes to allocate. Must be greater than zero. A request of
size== 0 is treated as invalid and returns an error expect.zeroed – If true, the allocated block is fully zero-initialized via
memset(). If false, the contents are uninitialized.
- Returns:
void_ptr_expect_t
On success:
has_value= trueu.value= pointer returned bymalloc()
On failure:
has_value= falseu.error= one of:INVALID_ARG (size == 0)
BAD_ALLOC (underlying
malloc()failed)
v_alloc_aligned
-
static inline void_ptr_expect_t v_alloc_aligned(void *ctx, size_t size, size_t align, bool zeroed)
Allocate a heap block with an alignment constraint, using void_ptr_expect_t.
This function implements an alignment-aware interface atop v_alloc(). It validates the requested alignment and then delegates to v_alloc(), relying on the guarantee that this backend provides at least
alignof(max_align_t)alignment for all allocations.Alignment semantics:
If
align== 0, the effective alignment becomesalignof(max_align_t).If
align> alignof(max_align_t), the request is rejected.If
alignis non-power-of-two, the request is rejected.
Because this simple backend does not provide actual over-aligned allocations, any request with
align<= alignof(max_align_t) is treated as satisfied by a normal v_alloc() call.Any value >
alignof(max_align_t)or non-power-of-two produces ALIGNMENT_ERROR.// Request 16-byte alignment, which is ≤ alignof(max_align_t) on many ABIs. void_ptr_expect_t r = v_alloc_aligned(NULL, 128u, 16u, true); if (!r.has_value) { fprintf(stderr, "aligned alloc failed: %d\n", r.u.error); return; } void *p = r.u.value; // use p ... free(p);
Note
This backend does not provide “true” over-aligned allocations; it only enforces that
aligndoes not demand more thanalignof(max_align_t).Ownership and eventual
free()are the caller’s responsibility.
- Parameters:
ctx – Optional allocator context (unused by this implementation).
size – Number of bytes to allocate. Must be greater than zero. A request of
size== 0 yields an INVALID_ARG error.align – Requested alignment in bytes. May be:
0 → use
alignof(max_align_t)power-of-two ≤ alignof(max_align_t)
zeroed – If true, the allocated block is fully zero-initialized; otherwise its contents are uninitialized.
- Returns:
void_ptr_expect_t
v_realloc
-
static inline void_ptr_expect_t v_realloc(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed)
Resize a heap block using realloc semantics, returning void_ptr_expect_t.
This is a thin wrapper around
realloc()that reports success or failure via void_ptr_expect_t and preserves the usual C realloc rules, with one important difference: anew_sizeof zero is treated as an error (INVALID_ARG), not as a “free” request.Semantics:
If
new_size== 0: → return error expect (INVALID_ARG).If
old_ptr== NULL: → behave like an allocation and delegate to v_alloc().Otherwise: → call
realloc(old_ptr, new_size).On success: optionally zero-fill the tail if the block grew and
zeroedis true.On failure: return BAD_ALLOC and leave
old_ptrvalid.
void_ptr_expect_t r = v_alloc(NULL, 64u, false); if (!r.has_value) { // handle error } void *p = r.u.value; void_ptr_expect_t rr = v_realloc(NULL, p, 64u, 128u, true); if (!rr.has_value) { fprintf(stderr, "realloc failed: %d\n", rr.u.error); // p is still valid here } else { p = rr.u.value; }
Note
On
BAD_ALLOCfromrealloc(), the originalold_ptrremains valid and is not freed.This function does not attempt to free memory on failure.
Ownership and eventual
free()remain the caller’s responsibility.
- Parameters:
ctx – Optional allocator context (unused by this implementation).
old_ptr – Pointer to an existing allocation obtained from this backend, or NULL to request a fresh allocation. Passing a foreign pointer yields undefined behavior (this function does not validate ownership).
old_size – Previous size of the allocation in bytes. Used only to determine the tail region to zero when
zeroedis true and the block grows. Passing an incorrect size leads to undefined behavior (e.g., memset past end).new_size – Requested new size in bytes. Must be > 0. If greater than
old_size, the block may grow; if smaller, it may shrink in place.zeroed – If true and
new_size>old_sizeand reallocation succeeds, the newly added region [old_size,new_size) is zero-filled.
- Returns:
void_ptr_expect_t
On success:
has_value= trueu.value= the (possibly moved) pointer returned byrealloc(), or a freshly allocated pointer ifold_ptr== NULL.
On failure:
has_value= falseu.error= one of:INVALID_ARG (new_size == 0)
BAD_ALLOC (underlying
realloc()or v_alloc() failed)
v_realloc_aligned
-
static inline void_ptr_expect_t v_realloc_aligned(void *ctx, void *old_ptr, size_t old_size, size_t new_size, bool zeroed, size_t align)
Resize a heap block with an alignment requirement, returning void_ptr_expect_t.
This function combines the semantics of v_realloc() with the alignment validation rules of v_alloc_aligned(). Because this simple backend does not provide true over-aligned allocations, any alignment requirement is restricted to
alignof(max_align_t).Alignment semantics:
If
align== 0, the effective alignment isalignof(max_align_t).If
alignis non-zero and not a power-of-two, the call fails with ALIGNMENT_ERROR.If
align(after normalization) is greater thanalignof(max_align_t), the call fails with ALIGNMENT_ERROR.
Reallocation semantics:
If
old_ptr== NULL: → behave like ::v_alloc_aligned(ctx, new_size, eff_align, zeroed).Otherwise: → behave like ::v_realloc(ctx, old_ptr, old_size, new_size, zeroed).
The alignment check is performed up front; no attempt is made to use platform-specific over-aligned allocation APIs.
// Allocate aligned void_ptr_expect_t a = v_realloc_aligned(NULL, NULL, 0u, 128u, true, 16u); if (!a.has_value) { // handle error } void *p = a.u.value; // Grow with the same alignment requirement void_ptr_expect_t r = v_realloc_aligned(NULL, p, 128u, 256u, true, 16u); if (!r.has_value) { // handle error } p = r.u.value; free(p);
Note
This backend does not provide real over-aligned allocations; it only enforces that requested alignments do not exceed
alignof(max_align_t).Ownership and
free()are still the caller’s responsibility.
- Parameters:
ctx – Optional allocator context (currently unused).
old_ptr – Pointer to an existing allocation, or NULL to request an aligned allocation. Foreign pointers produce undefined behavior.
old_size – Previous size in bytes of
old_ptr, used to determine how much of the block may be zero-filled on growth. Must be accurate whenold_ptris non-NULL.new_size – Requested new size in bytes. For the
old_ptr!= NULL case, the behavior is that of v_realloc(). Forold_ptr== NULL, it is passed through to v_alloc_aligned().zeroed – If true and
new_size>old_sizein theold_ptr!= NULL case, the extended region is zero-filled. For the allocation case (old_ptr== NULL), the entire block is zeroed if true.align – Requested alignment in bytes. May be:
0 → effective alignment =
alignof(max_align_t)power-of-two ≤ alignof(max_align_t)
- Returns:
void_ptr_expect_t
On success:
has_value= trueu.value= allocation/reallocation result pointer
On failure:
has_value= falseu.error= one of:ALIGNMENT_ERROR (invalid or overly strict alignment)
INVALID_ARG (from v_realloc when new_size == 0)
BAD_ALLOC (allocation or reallocation failure)
v_return
-
static inline void v_return(void *ctx, void *ptr)
Return a previously-allocated block to the system allocator.
This is a simple wrapper for
free().
Unlike some custom allocators, freeing a NULL pointer is legal and results in a no-op.
- Parameters:
ctx – Unused.
ptr – Pointer to memory allocated by the malloc backend. May be NULL.
v_free
-
static inline void v_free(void *ctx)
Finalizer for the malloc allocator backend.
For malloc-backed allocators, there is no global allocator state to clean up. This function exists only to satisfy the vtable interface.
- Parameters:
ctx – Unused.
heap_allocator
-
static inline allocator_vtable_t heap_allocator(void)
Create an allocator vtable backed by the C standard library.
This function constructs an allocator_vtable_t whose operations are implemented using the standard C library memory functions (
malloc,realloc, andfree). It provides a convenient plug-in backend for APIs expecting a vtable-style allocator interface.The returned vtable provides the following semantics:
allocateUsesmalloc(). Optionally zeroes the memory.allocate_alignedAccepts alignment requests up toalignof(max_align_t).
cannot guarantee stricter alignment.
reallocateWrapsrealloc(). On failure, returns NULL and leaves the input pointer valid.reallocate_alignedBehaves likereallocate, but with the same alignment policy asallocate_aligned.return_elementA thin wrapper overfree().deallocateNo-op. Included only for interface consistency.
The
.ctxfield is always NULL, because this backend does not require a context object.allocator_vtable_t alloc = malloc_allocator(); void* p = alloc.allocate(alloc.ctx, 128, true); if (!p) { perror("malloc_allocator allocate"); return 1; } // Resize the block p = alloc.reallocate(alloc.ctx, p, 128, 256, false); // Free it alloc.return_element(alloc.ctx, p);
- Returns:
A fully-populated allocator_vtable_t initialized to use the standard library allocator backend.