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.

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 size bytes.

The returned pointer is aligned to DS_ALIGNMENT and the block’s physical capacity is size rounded 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 len elements of size_of_elem bytes 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 size for 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:

*   DS_NOT_USED --(bump allocation)--> DS_ALLOCATED <--(free/reuse)--> DS_FREE
*        ^                                                               |
*        +----------------(bump-head rollback / cascade)----------------+
*
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.

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:

  1. 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.

  2. Ordering: among non-DS_NOT_USED records, array index order equals head order (heads strictly increase with index).

  3. 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).