ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
arena.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file arena.h
6 * @brief Unified double-ended server arena (Phase 1: core allocator, one region).
7 *
8 * One contiguous region is shared by two allocators that grow toward each other, with
9 * the free space floating in the middle:
10 *
11 * [ persistent --grows up--> | free | <--grows down-- scratch ]
12 * low addr (floating boundary) high addr
13 *
14 * - **Persistent** (bottom): a first-fit free-list. Long-lived objects that are freed
15 * individually, in arbitrary order (e.g. per-connection state). Grows up into the
16 * middle only as far as the scratch end allows; a freed top block shrinks it back.
17 * - **Scratch** (top): a bump allocator reclaimed in bulk. Transient per-dispatch
18 * buffers. `scratch_reset()` empties it in O(1); `mark`/`release` give nested savepoints.
19 *
20 * Whichever side needs more room grows into the shared middle - that is the win over two
21 * fixed pools. Both ends fail closed (return NULL) rather than crossing the boundary.
22 *
23 * All state lives in ::pc_arena (no globals), so it is unit-testable and can back several
24 * arenas (a DRAM base and a PSRAM extension, in a later phase). No heap; no stdlib.
25 *
26 * @author Douglas Quigg (dstroy0)
27 * @date 2026
28 */
29
30#ifndef PROTOCORE_ARENA_H
31#define PROTOCORE_ARENA_H
32
33#include <stddef.h>
34#include <stdint.h>
35
36/** @brief Baseline alignment (bytes) applied to every allocation and to headers. */
37#define PC_ARENA_ALIGN 8u
38
39/** @brief Round @p n up to PC_ARENA_ALIGN. */
40inline size_t pc_arena_align_up(size_t n)
41{
42 return (n + (PC_ARENA_ALIGN - 1)) & ~(size_t)(PC_ARENA_ALIGN - 1);
43}
44
45/** @brief Strongest alignment a scratch borrow may request; the region base is aligned to it. */
46#define PC_ARENA_MAX_ALIGN 16u
47
48/**
49 * @brief Double-ended arena over one region `[base, base+size)`.
50 *
51 * `persist_end` and `scratch_top` are byte offsets from `base`; the free middle is
52 * `[persist_end, scratch_top)`. The persistent pool owns `[0, persist_end)` (a chain of
53 * first-fit blocks); scratch owns `[scratch_top, size)` (bump). Initialize with
54 * pc_arena_init(); do not touch the fields directly.
55 */
56typedef struct
57{
58 uint8_t *base; ///< Region start.
59 size_t size; ///< Region length in bytes.
60 size_t persist_end; ///< Persistent pool occupies [0, persist_end).
61 size_t scratch_top; ///< Scratch occupies [scratch_top, size).
62 size_t persist_used; ///< Bytes currently handed out by the persistent pool (payload).
63 size_t persist_hw; ///< High-water of persist_end (for sizing).
64 size_t scratch_hw; ///< High-water of scratch use (size - min scratch_top).
65} pc_arena;
66
67/**
68 * @brief Initialize @p a over the region `[base, base+size)`.
69 *
70 * @p base should be PC_ARENA_ALIGN-aligned; @p size must be at least a few blocks.
71 */
72void pc_arena_init(pc_arena *a, void *base, size_t size);
73
74// --- persistent end (first-fit, individual free, grows up) ------------------
75
76/**
77 * @brief Allocate @p n zero-initialized bytes of long-lived storage.
78 *
79 * First-fits an existing free block; otherwise carves a new block from the free middle
80 * (growing the persistent end up) if it will not cross the scratch end.
81 * @return aligned, zeroed pointer, or NULL if the arena cannot satisfy it.
82 */
83void *pc_arena_persist_alloc(pc_arena *a, size_t n);
84
85/**
86 * @brief Free a pointer previously returned by pc_arena_persist_alloc().
87 *
88 * Coalesces with adjacent free blocks; if the freed block sits at the top of the
89 * persistent region it returns that space to the free middle (shrinks the boundary).
90 * Passing NULL is a no-op.
91 */
92void pc_arena_persist_free(pc_arena *a, void *p);
93
94// --- scratch end (bump, bulk reset, grows down) -----------------------------
95//
96// Defined inline, not in arena.cpp, because these are the pool's hot path and they are small: a
97// borrow is a few loads, a mask and a store. Out of line they cost three cross-TU calls per
98// borrow+release (measured: 190 cycles on an ESP32-S3 against a stack local's 24). Inlined, and with
99// the literal size/alignment every real call site passes, the clamp and the round-up constant-fold
100// and what remains is the pointer arithmetic itself.
101
102/**
103 * @brief Bump-allocate @p n bytes of transient storage, aligned to @p align.
104 *
105 * @param align power-of-two alignment for the returned pointer; clamped to
106 * `[PC_ARENA_ALIGN, PC_ARENA_MAX_ALIGN]`.
107 * @return aligned pointer (NOT zeroed), or NULL if it would cross the persistent end.
108 */
109inline void *pc_arena_scratch_alloc_aligned(pc_arena *a, size_t n, size_t align)
110{
111 if (align < PC_ARENA_ALIGN)
112 {
113 align = PC_ARENA_ALIGN;
114 }
115 if (align > PC_ARENA_MAX_ALIGN)
116 {
117 align = PC_ARENA_MAX_ALIGN; // the base only guarantees this much
118 }
120 if (a->scratch_top < n)
121 {
122 return nullptr;
123 }
124 // The base is PC_ARENA_MAX_ALIGN-aligned, so aligning the offset down aligns the pointer.
125 size_t nt = (a->scratch_top - n) & ~(size_t)(align - 1);
126 // The "nt > a->scratch_top" half is unreachable: the guard above already established
127 // n <= a->scratch_top, so the subtraction cannot underflow, and masking off low bits can
128 // only ever decrease the value further - nt <= a->scratch_top always holds.
129 if (nt < a->persist_end || nt > a->scratch_top) // GCOVR_EXCL_BR_LINE
130 {
131 return nullptr; // would cross the persistent end (or underflow)
132 }
133 a->scratch_top = nt;
134 size_t used = a->size - a->scratch_top;
135 if (used > a->scratch_hw)
136 {
137 a->scratch_hw = used;
138 }
139 return a->base + a->scratch_top;
140}
141
142/** @brief Bump-allocate @p n transient bytes at the baseline alignment (PC_ARENA_ALIGN). */
143void *pc_arena_scratch_alloc(pc_arena *a, size_t n);
144
145/** @brief Capture the current scratch position (a savepoint for pc_arena_scratch_release()). */
146inline size_t pc_arena_scratch_mark(const pc_arena *a)
147{
148 return a->scratch_top;
149}
150
151/** @brief Free every scratch allocation made since @p mark (a value from pc_arena_scratch_mark()). */
152inline void pc_arena_scratch_release(pc_arena *a, size_t mark)
153{
154 // A mark is an earlier (higher) scratch_top; releasing frees everything below it.
155 if (mark >= a->scratch_top && mark <= a->size)
156 {
157 a->scratch_top = mark;
158 }
159}
160
161/** @brief Free ALL scratch allocations in O(1). */
163{
164 a->scratch_top = a->size;
165}
166
167// --- ownership (the access control) -----------------------------------------
168
169/**
170 * @brief True if @p p points inside this pool's region.
171 *
172 * Ownership is an address-range property, not bookkeeping. The pools occupy disjoint regions, so a
173 * pointer belongs to exactly one of them and the owner is recoverable from the address alone. That
174 * is what stops a secret-pool pointer from ever being accepted where a plaintext one is expected,
175 * and what makes a write that ran off the end land outside the range instead of silently into a
176 * neighbour.
177 *
178 * The accessors answer this more cheaply still: their slot count and slot size are compile-time, so
179 * the whole pool is one region of known extent and the test is a single unsigned subtract against a
180 * constant bound - see pc_scratch_owns() / pc_secure_owns(). This general version is two compares,
181 * for an arena whose base and size are only known at run time.
182 */
183inline bool pc_arena_owns(const pc_arena *a, const void *p)
184{
185 const uint8_t *q = (const uint8_t *)p;
186 return a->base != nullptr && q >= a->base && q < a->base + a->size;
187}
188
189// --- observability ----------------------------------------------------------
190
191/** @brief Free bytes in the middle (max a single new allocation could take, minus a header). */
192size_t pc_arena_free_bytes(const pc_arena *a);
193
194/** @brief Persistent payload bytes currently allocated. */
195size_t pc_arena_persist_used(const pc_arena *a);
196
197/** @brief Scratch bytes currently allocated. */
198inline size_t pc_arena_scratch_used(const pc_arena *a)
199{
200 return a->size - a->scratch_top;
201}
202
203// ===========================================================================
204// Multi-region extension: a DRAM base + an optional PSRAM extension.
205// ===========================================================================
206//
207// A ::pc_arena_set chains a few ::pc_arena regions in preference order (add DRAM
208// first, PSRAM second). Allocations try each region in turn and take the first
209// that fits, so hot state stays in fast internal RAM and only the overflow
210// spills into external RAM. Frees are routed to the owning region by address.
211// This is how "arena extension" works: enable PSRAM by adding a second region;
212// leave it out and the set is just the single DRAM arena.
213
214/** @brief Max regions in a ::pc_arena_set (DRAM base + PSRAM extension). */
215#ifndef PC_ARENA_MAX_REGIONS
216#define PC_ARENA_MAX_REGIONS 2u
217#endif
218
219/** @brief A set of ::pc_arena regions searched in insertion (preference) order. */
220typedef struct
221{
223 size_t count; ///< Regions in use.
225
226/** @brief A scratch savepoint across every region of a ::pc_arena_set. */
227typedef struct
228{
230 size_t count;
232
233/** @brief Initialize an empty set (no regions yet). */
235
236/**
237 * @brief Add a region `[base, base+size)`; regions are searched in the order added.
238 * @return true if added, false if the set is full or the region is too small.
239 */
240bool pc_arena_set_add(pc_arena_set *s, void *base, size_t size);
241
242/** @brief Persistent alloc from the first region that fits (see pc_arena_persist_alloc()). */
243void *pc_arena_set_persist_alloc(pc_arena_set *s, size_t n);
244
245/** @brief Free a persistent pointer, routed to its owning region by address. */
247
248/** @brief Aligned scratch alloc from the first region that fits (see pc_arena_scratch_alloc_aligned()). */
249void *pc_arena_set_scratch_alloc_aligned(pc_arena_set *s, size_t n, size_t align);
250
251/** @brief Scratch alloc from the first region that fits (see pc_arena_scratch_alloc()). */
252void *pc_arena_set_scratch_alloc(pc_arena_set *s, size_t n);
253
254/** @brief Capture the scratch position of every region. */
256
257/** @brief Restore every region's scratch position to @p m (frees scratch made since). */
259
260/** @brief Reset scratch in every region. */
262
263/** @brief Total free middle bytes summed over all regions. */
265
266/** @brief Persistent payload bytes allocated, summed over all regions. */
268
269/** @brief Scratch bytes allocated, summed over all regions. */
271
272#endif // PROTOCORE_ARENA_H
size_t pc_arena_set_scratch_used(const pc_arena_set *s)
Scratch bytes allocated, summed over all regions.
Definition arena.cpp:295
void pc_arena_scratch_reset(pc_arena *a)
Free ALL scratch allocations in O(1).
Definition arena.h:162
void * pc_arena_set_scratch_alloc_aligned(pc_arena_set *s, size_t n, size_t align)
Aligned scratch alloc from the first region that fits (see pc_arena_scratch_alloc_aligned()).
Definition arena.cpp:229
bool pc_arena_set_add(pc_arena_set *s, void *base, size_t size)
Add a region [base, base+size); regions are searched in the order added.
Definition arena.cpp:182
void * pc_arena_scratch_alloc(pc_arena *a, size_t n)
Bump-allocate n transient bytes at the baseline alignment (PC_ARENA_ALIGN).
Definition arena.cpp:153
void * pc_arena_set_scratch_alloc(pc_arena_set *s, size_t n)
Scratch alloc from the first region that fits (see pc_arena_scratch_alloc()).
Definition arena.cpp:242
size_t pc_arena_set_persist_used(const pc_arena_set *s)
Persistent payload bytes allocated, summed over all regions.
Definition arena.cpp:285
void pc_arena_set_scratch_reset(pc_arena_set *s)
Reset scratch in every region.
Definition arena.cpp:267
size_t pc_arena_align_up(size_t n)
Round n up to PC_ARENA_ALIGN.
Definition arena.h:40
#define PC_ARENA_MAX_ALIGN
Strongest alignment a scratch borrow may request; the region base is aligned to it.
Definition arena.h:46
size_t pc_arena_scratch_mark(const pc_arena *a)
Capture the current scratch position (a savepoint for pc_arena_scratch_release()).
Definition arena.h:146
void pc_arena_set_persist_free(pc_arena_set *s, void *p)
Free a persistent pointer, routed to its owning region by address.
Definition arena.cpp:211
#define PC_ARENA_ALIGN
Baseline alignment (bytes) applied to every allocation and to headers.
Definition arena.h:37
void pc_arena_set_scratch_release(pc_arena_set *s, const pc_arena_mark *m)
Restore every region's scratch position to m (frees scratch made since).
Definition arena.cpp:258
#define PC_ARENA_MAX_REGIONS
Max regions in a pc_arena_set (DRAM base + PSRAM extension).
Definition arena.h:216
void pc_arena_scratch_release(pc_arena *a, size_t mark)
Free every scratch allocation made since mark (a value from pc_arena_scratch_mark()).
Definition arena.h:152
pc_arena_mark pc_arena_set_scratch_mark(const pc_arena_set *s)
Capture the scratch position of every region.
Definition arena.cpp:247
void pc_arena_persist_free(pc_arena *a, void *p)
Free a pointer previously returned by pc_arena_persist_alloc().
Definition arena.cpp:98
size_t pc_arena_free_bytes(const pc_arena *a)
Free bytes in the middle (max a single new allocation could take, minus a header).
Definition arena.cpp:162
void * pc_arena_scratch_alloc_aligned(pc_arena *a, size_t n, size_t align)
Bump-allocate n bytes of transient storage, aligned to align.
Definition arena.h:109
size_t pc_arena_scratch_used(const pc_arena *a)
Scratch bytes currently allocated.
Definition arena.h:198
void * pc_arena_set_persist_alloc(pc_arena_set *s, size_t n)
Persistent alloc from the first region that fits (see pc_arena_persist_alloc()).
Definition arena.cpp:198
void pc_arena_set_init(pc_arena_set *s)
Initialize an empty set (no regions yet).
Definition arena.cpp:177
size_t pc_arena_persist_used(const pc_arena *a)
Persistent payload bytes currently allocated.
Definition arena.cpp:168
void * pc_arena_persist_alloc(pc_arena *a, size_t n)
Allocate n zero-initialized bytes of long-lived storage.
Definition arena.cpp:49
void pc_arena_init(pc_arena *a, void *base, size_t size)
Initialize a over the region [base, base+size).
Definition arena.cpp:29
size_t pc_arena_set_free_bytes(const pc_arena_set *s)
Total free middle bytes summed over all regions.
Definition arena.cpp:275
bool pc_arena_owns(const pc_arena *a, const void *p)
True if p points inside this pool's region.
Definition arena.h:183
A scratch savepoint across every region of a pc_arena_set.
Definition arena.h:228
size_t count
Definition arena.h:230
A set of pc_arena regions searched in insertion (preference) order.
Definition arena.h:221
size_t count
Regions in use.
Definition arena.h:223
Double-ended arena over one region [base, base+size).
Definition arena.h:57
size_t persist_used
Bytes currently handed out by the persistent pool (payload).
Definition arena.h:62
size_t scratch_top
Scratch occupies [scratch_top, size).
Definition arena.h:61
uint8_t * base
Region start.
Definition arena.h:58
size_t scratch_hw
High-water of scratch use (size - min scratch_top).
Definition arena.h:64
size_t persist_hw
High-water of persist_end (for sizing).
Definition arena.h:63
size_t size
Region length in bytes.
Definition arena.h:59
size_t persist_end
Persistent pool occupies [0, persist_end).
Definition arena.h:60