API reference
The public API is declared in dynostatic-buffer.h. The documentation
below is generated from the Doxygen comments in the source via Breathe.
Implementation of dynamic allocation of memory in static buffer.
- Author
Jakub Brzezowski
- Date
2022-06-14
Defines
-
DS_BUFFER_MEMORY_SIZE
< If You not use CMake and KConfig. Size of buffer prepared for dynostatic-buffer. If You not use CMake and KConfig.
-
DS_LOG_ENABLE
Enable or disable logging from dynostatic-buffer. If You not use CMake and KConfig.
-
DS_ZERO_ON_FREE
Zero the contents of freed blocks in dynostatic-buffer. If You not use CMake and KConfig.
-
DS_MAX_ALLOCATION_COUNT
Set maximal number of allocation which can be made in dynostatic-buffer. If You not use CMake and KConfig.
-
DS_MAX_ALLOCATION_SIZE
Set maximal number of allocation which can be made in dynostatic-buffer.
-
DS_ALIGNMENT
Alignment for memory allocations.
-
DS_STATIC_ASSERT(cond, msg)
Typedefs
-
typedef uint16_t ds_err_code_t
Type of error code used in dynostatic-buffer.
Enums
-
enum ds_allocator_status_t
Lifecycle state of an allocator record (see ds_allocator_t for the full lifecycle diagram and the invariants tied to these states).
Note
CONTRACT: DS_NOT_USED must remain 0. ds_initialize_allocation() establishes the all-records-DS_NOT_USED state by zeroing the allocators array (ds_zero), and the compact-prefix invariant assumes a zeroed record reads as DS_NOT_USED. Renumbering this enum silently breaks initialization.
Values:
-
enumerator DS_NOT_USED
Record carries no block: either never used, or reclaimed by bump-head rollback/cascade. head and size are zero and meaningless. Scans may stop at the first DS_NOT_USED record (compact prefix). Entered from: initialization, rollback of a trailing block. Leaves to: DS_ALLOCATED via a fresh bump allocation.
-
enumerator DS_FREE
Block was freed but its record is parked for reuse: head and size (physical capacity) remain valid, and contents are zeroed when DS_ZERO_ON_FREE is enabled. A later request of size <= capacity may reuse this exact block. Entered from: DS_ALLOCATED via ds_free of a non-trailing block. Leaves to: DS_ALLOCATED via reuse, or DS_NOT_USED via the reclamation cascade.
-
enumerator DS_ALLOCATED
Block is live and owned by the caller: head and size (physical capacity, >= the requested size) are valid, contents belong to the user. Entered from: DS_NOT_USED (bump allocation) or DS_FREE (reuse). Leaves to: DS_FREE or DS_NOT_USED via ds_free, depending on whether the block is trailing.
-
enumerator DS_NOT_USED
Functions
-
ds_err_code_t ds_initialize_allocation(dynostatic_buffer_t *p_ds_buffer)
Initialize an allocator instance over its embedded arena.
Zeroes the arena and all allocator records, resets the bump head, and arms the init_magic marker. After success the full DS_BUFFER_MEMORY_SIZE is allocatable.
- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
- Return values:
ERROR_DS_OK – Instance initialized; the whole arena is available.
ERROR_DS_INVALID_ARG – p_ds_buffer is NULL.
ERROR_DS_ALREADY_INIT – Instance is already initialized; call ds_deinit_allocation() first to reset it.
- Pre:
The structure must be zero-initialized before the FIRST call (see the warning at dynostatic_buffer_t) — garbage content may spuriously read as already initialized.
-
ds_err_code_t ds_malloc(dynostatic_buffer_t *p_ds_buffer, void **p_memory, size_t size)
Allocate a memory block of at least
sizebytes.The returned pointer is aligned to DS_ALIGNMENT and the block’s physical capacity is
sizerounded up to DS_ALIGNMENT. The request is served either by reusing a previously freed block of sufficient capacity or by a fresh allocation from untouched space. On success *p_memory receives the block address; on any failure *p_memory is left unchanged.- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
p_memory – [inout] In: must not point to a live block. Out: address of the allocated block.
size – [in] Requested size in bytes (1..DS_MAX_ALLOCATION_SIZE).
- Return values:
ERROR_DS_OK – Block allocated; *p_memory points to it.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer or p_memory is NULL, or size is 0.
ERROR_DS_TOO_BIG_CHUNK – size exceeds DS_MAX_ALLOCATION_SIZE.
ERROR_DS_PTR_ALLOC_YET – *p_memory already points to a live block; free it first or use ds_realloc().
ERROR_DS_NO_ALLOCATORS – All allocator records are occupied.
ERROR_DS_NO_MEMORY – No free region can satisfy the aligned size.
- Pre:
*p_memory must be initialized before the call — NULL or a previously obtained pointer. The function reads it to reject overwriting a pointer to a live block (a leak guard); passing an uninitialized value is undefined behaviour and may yield spurious ERROR_DS_PTR_ALLOC_YET.
-
ds_err_code_t ds_free(dynostatic_buffer_t *p_ds_buffer, void **p_memory)
Free a block previously obtained from this instance.
Only a pointer to the START of a live block is accepted — interior pointers, foreign pointers, and already-freed addresses are rejected without touching any state. When DS_ZERO_ON_FREE is enabled (default), the block’s full capacity is zeroed before release. A freed trailing block is reclaimed immediately (including a cascade through adjacent freed blocks), making the space available to allocations of any size; a freed interior block is parked for reuse by requests fitting its capacity. On success *p_memory is set to NULL.
- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
p_memory – [inout] In: address of a live block. Out: NULL on success, unchanged on failure.
- Return values:
ERROR_DS_OK – Block released; *p_memory is now NULL.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer, p_memory or *p_memory is NULL.
ERROR_DS_MEMORY_OUT_OF_DS – *p_memory lies outside this instance’s arena.
ERROR_DS_ALLOCATOR_NOT_FOUND – *p_memory is inside the arena but is not the start of a live block — typically a double free or an interior pointer.
-
ds_err_code_t ds_calloc(dynostatic_buffer_t *p_ds_buffer, void **p_memory, size_t len, size_t size_of_elem)
Allocate a zero-filled array of
lenelements ofsize_of_elembytes each.Equivalent to ds_malloc(len * size_of_elem) followed by zeroing the requested bytes. The zero fill is unconditional — it does NOT depend on DS_ZERO_ON_FREE. The multiplication is checked for overflow. Inherits the ds_malloc() precondition on *p_memory (see ERROR_DS_PTR_ALLOC_YET).
- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
p_memory – [inout] In: must not point to a live block. Out: address of the zeroed array.
len – [in] Number of elements; must not be 0.
size_of_elem – [in] Size of one element in bytes; must not be 0.
- Return values:
ERROR_DS_OK – Array allocated and zeroed.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – NULL arguments, zero len/size_of_elem, or len * size_of_elem overflows size_t.
ERROR_DS_TOO_BIG_CHUNK – Total size exceeds DS_MAX_ALLOCATION_SIZE.
ERROR_DS_PTR_ALLOC_YET – *p_memory already points to a live block.
ERROR_DS_NO_ALLOCATORS – All allocator records are occupied.
ERROR_DS_NO_MEMORY – No free region can satisfy the aligned size.
-
ds_err_code_t ds_realloc(dynostatic_buffer_t *p_ds_buffer, void **p_memory, size_t size)
Resize a block, preserving its contents up to the smaller of the old and new sizes.
Semantics follow C realloc with explicit deviations:
size == 0: equivalent to ds_free(p_memory).
*p_memory == NULL: equivalent to ds_malloc(size).
Shrink (new aligned size fits the block’s capacity): in place, pointer unchanged, capacity RETAINED (no split; ds_get_memory_usage() keeps reporting the original capacity).
Grow of the most recently placed block: extended in place when space allows — pointer unchanged, no copy.
Grow otherwise: a new block is allocated, contents are copied, the old block is freed (zeroed under DS_ZERO_ON_FREE). Requires a free allocator record for the transient old+new pair. Bytes beyond the old size have indeterminate values.
A pointer that is not the start of a live block is REJECTED — unlike C realloc’s undefined behaviour, and no new block is allocated.
On any failure the original block and *p_memory are left fully intact.
- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
p_memory – [inout] In: NULL or address of a live block. Out: address of the resized block (may differ from the input), NULL after size == 0, unchanged on failure.
size – [in] New size in bytes (0..DS_MAX_ALLOCATION_SIZE).
- Return values:
ERROR_DS_OK – Operation completed as described above.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer or p_memory is NULL (or, for size == 0, *p_memory is NULL).
ERROR_DS_TOO_BIG_CHUNK – size exceeds DS_MAX_ALLOCATION_SIZE.
ERROR_DS_MEMORY_OUT_OF_DS – *p_memory lies outside this instance’s arena.
ERROR_DS_ALLOCATOR_NOT_FOUND – *p_memory is not the start of a live block (double free / interior pointer).
ERROR_DS_NO_ALLOCATORS – No free record for the moved block.
ERROR_DS_NO_MEMORY – No region can hold the grown block.
-
ds_err_code_t ds_get_memory_usage(const dynostatic_buffer_t *p_ds_buffer, uint8_t *p_memory_usage)
Get the arena occupancy as a percentage (0..100, rounded down).
Sums the PHYSICAL CAPACITIES of live blocks — sizes rounded up to DS_ALIGNMENT and retained across reuse and shrinking — so the result may exceed the sum of requested sizes. Parked freed blocks count as free. 0 means no live blocks; 100 means the arena is fully occupied by live blocks.
Note
Snapshot semantics: valid only until the next mutating call.
- Parameters:
p_ds_buffer – [in] Pointer to dynostatic-buffer structure.
p_memory_usage – [out] Occupancy percentage (0..100).
- Return values:
ERROR_DS_OK – Value successfully computed and written.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer or p_memory_usage is NULL.
ERROR_DS_CRITICAL_ERR – Internal state corrupted: live capacities exceed the arena size (writes 0).
-
ds_err_code_t ds_get_max_new_allocation_size(const dynostatic_buffer_t *p_ds_buffer, size_t *p_max_new_allocation)
Get the largest allocation size that would succeed right now.
Returns the maximal
sizefor which an immediate ds_malloc() call on this instance would return ERROR_DS_OK. Both allocation paths are considered: reuse of the largest parked DS_FREE block and a fresh bump allocation (clamped to DS_MAX_ALLOCATION_SIZE and to the aligned usable remainder — an unaligned buffer tail is excluded as unallocatable).A result of 0 means no allocation of any size can currently succeed — either the memory or the allocator slots are exhausted; use ds_get_free_allocator_cnt() and ds_get_memory_usage() to tell which.
Note
The value is a snapshot: it is guaranteed only until the next call that mutates this instance (ds_malloc/ds_calloc/ds_realloc/ds_free).
- Parameters:
p_ds_buffer – [in] Pointer to dynostatic-buffer structure.
p_max_new_allocation – [out] Largest currently satisfiable allocation size in bytes; 0 when nothing can be allocated.
- Return values:
ERROR_DS_OK – Value successfully computed and written.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer or p_max_new_allocation is NULL.
-
ds_err_code_t ds_get_free_allocator_cnt(const dynostatic_buffer_t *p_ds_buffer, size_t *p_free_allocators)
Get the number of allocator slots not holding a live block.
Counts slots in DS_NOT_USED or DS_FREE state — an UPPER BOUND on how many additional concurrent allocations this instance can hold, NOT a guarantee that any particular allocation will succeed: parked DS_FREE slots serve only requests fitting their retained capacity, and every allocation is further subject to available memory (see ds_get_max_new_allocation_size() for a success-oriented view).
A result of 0 means ds_malloc() is guaranteed to fail with ERROR_DS_NO_ALLOCATORS regardless of the requested size.
Note
The value is a snapshot: it is guaranteed only until the next call that mutates this instance.
- Parameters:
p_ds_buffer – [in] Pointer to dynostatic-buffer structure.
p_free_allocators – [out] Number of slots available for new allocations (0..DS_MAX_ALLOCATION_COUNT).
- Return values:
ERROR_DS_OK – Value successfully computed and written.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – p_ds_buffer or p_free_allocators is NULL.
-
ds_err_code_t ds_deinit_allocation(dynostatic_buffer_t *p_ds_buffer)
Deinitialize dynostatic-buffer.
- Parameters:
p_ds_buffer – [inout] Pointer to dynostatic-buffer structure.
- Return values:
ERROR_DS_OK – Dynostatic-buffer is properly deinitialized.
ERROR_DS_NO_INIT – Dynostatic-buffer is not initialized.
ERROR_DS_INVALID_ARG – Given parameters are invalid.
-
struct ds_allocator_t
- #include <dynostatic-buffer.h>
Bookkeeping record describing one memory block within the buffer.
Lifecycle of a record:
A record returns to DS_NOT_USED only when the bump head is rolled back over it (trailing-block reclamation); an ordinary free parks it as DS_FREE so the block can be reused by a later allocation of equal or smaller size.* DS_NOT_USED --(bump allocation)--> DS_ALLOCATED <--(free/reuse)--> DS_FREE * ^ | * +----------------(bump-head rollback / cascade)----------------+ *
Field semantics depend on allocation_status:
DS_NOT_USED: head and size are zero and carry no meaning (slot never used, or reclaimed by rollback).
DS_ALLOCATED / DS_FREE: head is the block’s offset from the start of dynostatic_buffer_t::memory; size is the block’s PHYSICAL CAPACITY — the requested size rounded up to DS_ALIGNMENT. Reusing a block with a smaller request does NOT shrink size: capacity is retained for the lifetime of the record.
Invariants maintained by ds_malloc/ds_free — allocator scans, the early break on DS_NOT_USED, and the reclamation cascade all rely on them:
Compact prefix: every record past the first DS_NOT_USED slot is also DS_NOT_USED, so scans may legally stop at the first DS_NOT_USED entry.
Ordering: among non-DS_NOT_USED records, array index order equals head order (heads strictly increase with index).
Gapless partition: non-DS_NOT_USED records tile [0, data_head) exactly: head[0] == 0 and head[i+1] == head[i] + size[i]. In particular the block ending at data_head always sits at the highest touched index.
Public Members
-
size_t head
Offset of the block from the start of dynostatic_buffer_t::memory. Meaningful only when allocation_status != DS_NOT_USED.
-
size_t size
Physical capacity of the block (requested size aligned up to DS_ALIGNMENT); preserved on reuse. Meaningful only when allocation_status != DS_NOT_USED.
-
ds_allocator_status_t allocation_status
Lifecycle state of this record; governs the meaning of head and size.
-
struct dynostatic_buffer_t
- #include <dynostatic-buffer.h>
Self-contained allocator instance emulating dynamic allocation inside a caller-provided static buffer — no heap, no globals.
All state lives inline in this structure, so an instance can be placed in static storage (recommended), on a stack, or inside another object, and multiple independent instances may coexist. Note the footprint: roughly DS_BUFFER_MEMORY_SIZE + DS_MAX_ALLOCATION_COUNT * sizeof(ds_allocator_t) bytes — on small targets prefer static storage duration over the stack.
Lifecycle: zero the structure (static storage duration does this for free), then ds_initialize_allocation(), then the allocation API, then ds_deinit_allocation() (zeroes all memory and bookkeeping).
Pointers returned by the allocation API point into memory and are aligned to DS_ALIGNMENT. They become invalid after ds_free() of the block and after ds_deinit_allocation() of the instance.
Warning
Initialization detection relies on init_magic, so the structure MUST be zero-initialized before the first ds_initialize_allocation() call. An instance containing garbage (e.g. a stack variable) may spuriously read as already initialized. Static storage duration satisfies this requirement automatically.
Warning
Not thread-safe and not ISR-safe: no internal locking. The caller must serialize all API calls on a given instance (distinct instances are fully independent).
Public Members
-
uint16_t init_magic
Equals DS_MAGIC_NUMBER while the instance is initialized; any other value means uninitialized. Requires the structure to be zeroed before first init (see warning).
-
size_t data_head
Bump pointer: offset of the first byte of never-touched (or reclaimed) space in memory. Always in [0, DS_BUFFER_MEMORY_SIZE]. Equal to the sum of capacities of all non-DS_NOT_USED records (gapless-partition invariant, see ds_allocator_t). Rolls back when trailing blocks are freed.
-
size_t used_allocators
Number of records currently in DS_ALLOCATED state (live blocks owned by callers). Parked DS_FREE records are NOT counted. Always in [0, DS_MAX_ALLOCATION_COUNT].
-
uint8_t memory[DS_BUFFER_MEMORY_SIZE]
The arena. All user pointers point into this array; alignas guarantees the base address (and therefore every aligned offset) meets DS_ALIGNMENT.
-
ds_allocator_t allocators[DS_MAX_ALLOCATION_COUNT]
Block records; invariants documented at ds_allocator_t. Records are recruited in index order (compact prefix).
-
uint16_t init_magic