ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
scratch.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 scratch.h
6 * @brief Shared per-dispatch scratch arena (Layer 5, session-scoped memory).
7 *
8 * Fixed BSS arenas that codec / protocol handlers borrow transient working
9 * memory from, instead of each feature carrying its own dedicated scratch
10 * buffer. Many such buffers are mutually exclusive in time - a connection is
11 * doing HTTP *or* WebSocket *or* SSH at any instant, and a worker runs one
12 * event to completion before the next - so overlapping them in one arena cuts
13 * peak DRAM without weakening the zero-heap / deterministic guarantee (fixed
14 * size, no runtime growth).
15 *
16 * There is one arena per slot (PC_SCRATCH_SLOTS): one per worker, plus one the
17 * SSH client task owns when PC_ENABLE_SSH_CLIENT is set. scratch_alloc()
18 * resolves the caller's slot with pc_worker_self(), so a borrow never crosses
19 * workers.
20 *
21 * **Model - region reset per dispatch.** scratch_alloc() bump-allocates from the
22 * caller's arena; scratch_reset() empties that one arena in O(1).
23 * dispatch_event() calls scratch_reset() before handing an event to its
24 * protocol handler, so a borrow is valid only until the handler returns. There
25 * is no per-allocation free - the whole arena is reclaimed at once.
26 *
27 * **Race-safety.** Each arena has exactly one accessor - the worker that owns
28 * its slot - so allocation is a plain bump with no lock. Work reaches a worker
29 * through its queue, so a context that is not a worker never borrows: the lwIP
30 * callbacks run in tcpip_thread and only fill the rx ring + enqueue events, and
31 * an ISR posts a fixed-size item to a preempt-queue lane whose task does the
32 * work. In debug builds an owner-task assertion (xTaskGetCurrentTaskHandle)
33 * records the first task to touch each arena and fails loud if a second one
34 * does, turning a future mistake into an immediate visible failure instead of a
35 * silent cross-core race.
36 *
37 * **Exhaustion-safety.** Borrows live only within one dispatch and are
38 * auto-reclaimed by the reset, so a forgotten free cannot accumulate (no
39 * creeping exhaustion). An over-budget scratch_alloc() returns nullptr; every
40 * caller must take a defined fail-closed path (drop the optional optimization,
41 * close the connection, answer 503) and must never dereference a null borrow.
42 *
43 * **No implicit zeroing.** scratch_alloc() returns uninitialized memory and the
44 * reset does not wipe. This pool is for plaintext: anything whose bytes are key
45 * material belongs in the secure pool (server/mmgr/secure.h), which is the same
46 * mechanism with one added control - reclaiming wipes, before the bytes become
47 * available again.
48 *
49 * @author Douglas Quigg (dstroy0)
50 * @date 2026
51 */
52
53#ifndef PROTOCORE_SCRATCH_H
54#define PROTOCORE_SCRATCH_H
55
56#include "protocore_config.h"
58#include <stddef.h>
59
60/**
61 * @brief Borrow @p n bytes of scratch, aligned to @p align.
62 *
63 * The returned pointer is valid only until the next scratch_reset() (i.e. only
64 * within the current session dispatch). Returns nullptr if the request does not
65 * fit the remaining arena - callers MUST handle null and fail closed.
66 *
67 * @param n bytes requested (0 yields a valid non-null pointer when space
68 * remains).
69 * @param align required alignment in bytes, a power of two (0 selects the
70 * platform default).
71 * @return pointer to @p n writable bytes, or nullptr if it does not fit.
72 */
73void *scratch_alloc(size_t n, size_t align);
74
75/**
76 * @brief Borrow @p n bytes as a span whose capacity is bound to the allocation.
77 *
78 * The preferred form. scratch_alloc() hands back a bare pointer, which leaves the caller to carry
79 * the length separately and keep the two in agreement by hand at every call - the same severed
80 * binding that makes `sizeof()` on a converted array read 4 bytes instead of the extent. Here one
81 * argument sets both fields, so the run length is stated once and cannot drift.
82 *
83 * Fails closed: an over-budget request yields `{nullptr, 0}`, so a caller that omits the
84 * pc_span_ok() check writes nothing rather than dereferencing null. Callers should still check and
85 * take their defined fail-closed path.
86 *
87 * @param n bytes requested.
88 * @param align required alignment in bytes, a power of two (0 selects the platform default).
89 * @return a span over @p n writable bytes, or an empty span if it does not fit.
90 */
91pc_span pc_scratch_span(size_t n, size_t align);
92
93/**
94 * @brief Reclaim the whole arena (empties it).
95 *
96 * Called by server_tick() before each event dispatch. Invalidates every pointer
97 * previously returned by scratch_alloc().
98 */
99void scratch_reset(void);
100
101/**
102 * @brief Capture the current arena offset (a savepoint for scratch_release()).
103 * @return an opaque mark to pass to scratch_release().
104 */
105size_t scratch_mark(void);
106
107/**
108 * @brief Reclaim everything allocated since @p mark (LIFO).
109 *
110 * Restores the arena to a previous scratch_mark(), freeing every scratch_alloc()
111 * made in between. Marks must be released in reverse order (nested scopes). Use
112 * ScratchScope for return-safe scoping.
113 *
114 * @param mark a value previously returned by scratch_mark() (must be <= the
115 * current offset).
116 */
117void scratch_release(size_t mark);
118
119/** @brief Bytes currently handed out (0 immediately after a reset). */
120size_t scratch_used(void);
121
122/** @brief Largest scratch_used() value seen since boot (for sizing the arena). */
123size_t scratch_high_water(void);
124
125/** @brief Total arena capacity in bytes (PC_SCRATCH_ARENA_SIZE). */
126size_t scratch_capacity(void);
127
128/**
129 * @brief True if @p p points inside the plaintext pool.
130 *
131 * Ownership is an address-range property, not bookkeeping. The slot count and slot size are both
132 * compile-time, so the whole pool is ONE region of known extent and the test is a single unsigned
133 * subtract and compare - no loop, no per-slot comparison, no per-allocation metadata. A pointer
134 * below the base wraps to a huge offset and fails the same bound as one past the end, so a buffer
135 * overrun cannot test as still-inside.
136 *
137 * This is the plaintext half of the control strategy. The secure pool is a disjoint region and
138 * answers its own question, so a secure-pool pointer can never be accepted where a plaintext one
139 * is required, or the reverse.
140 */
141bool pc_scratch_owns(const void *p);
142
143/**
144 * @brief Which plaintext slot owns @p p, or -1 if @p p is not in the plaintext pool.
145 *
146 * A divide by a compile-time constant, so a multiply-and-shift rather than real division. Use to
147 * assert that a borrow being handed back belongs to the calling worker: crossing slots is the one
148 * way the lock-free single-accessor invariant can be violated, and this makes it checkable.
149 */
150int pc_scratch_slot_of(const void *p);
151
152/**
153 * @brief A scratch borrow whose acquire and release are one call each.
154 *
155 * ScratchScope + pc_scratch_span costs three crossings of this module's boundary: mark, alloc, then
156 * release. Mark and alloc always happen together, so one of those is structure rather than work.
157 * This does both in the constructor and the release in the destructor - two calls, and the slot is
158 * resolved once instead of twice.
159 *
160 * The pool's state stays private to the .cpp; that is exactly why these cannot be inlined instead,
161 * and why LTO would have been the alternative (it is unavailable here - the ESP32 core does not link
162 * with -flto).
163 *
164 * Fails closed like every other borrow: an exhausted arena leaves span() empty, so a caller that
165 * omits the check writes nothing rather than dereferencing null.
166 */
168{
169 public:
170 ScratchBorrow(size_t n, size_t align);
172 ScratchBorrow(const ScratchBorrow &) = delete;
174
175 /** @brief The borrowed region; empty if the arena could not satisfy it. */
176 const pc_span &span() const
177 {
178 return m_span;
179 }
180
181 private:
182 size_t m_mark;
183 pc_span m_span;
184};
185
186/**
187 * @brief RAII scope guard for transient scratch borrows.
188 *
189 * Marks the arena on construction and restores it on destruction, so every
190 * scratch_alloc() made within the scope is reclaimed on *every* exit path
191 * (return, break, fall-through) - the safe way to borrow transient scratch in a
192 * function with multiple returns. Scopes must nest (LIFO); the per-dispatch
193 * scratch_reset() is the backstop if one is ever skipped.
194 */
196{
197 public:
199 {
200 }
202 {
203 scratch_release(m_mark);
204 }
205 ScratchScope(const ScratchScope &) = delete;
207
208 private:
209 size_t m_mark;
210};
211
212#endif // PROTOCORE_SCRATCH_H
A scratch borrow whose acquire and release are one call each.
Definition scratch.h:168
const pc_span & span() const
The borrowed region; empty if the arena could not satisfy it.
Definition scratch.h:176
ScratchBorrow(const ScratchBorrow &)=delete
ScratchBorrow & operator=(const ScratchBorrow &)=delete
RAII scope guard for transient scratch borrows.
Definition scratch.h:196
ScratchScope(const ScratchScope &)=delete
ScratchScope & operator=(const ScratchScope &)=delete
User-facing configuration for ProtoCore.
size_t scratch_mark(void)
Capture the current arena offset (a savepoint for scratch_release()).
Definition scratch.cpp:165
pc_span pc_scratch_span(size_t n, size_t align)
Borrow n bytes as a span whose capacity is bound to the allocation.
Definition scratch.cpp:146
bool pc_scratch_owns(const void *p)
True if p points inside the plaintext pool.
Definition scratch.cpp:206
size_t scratch_capacity(void)
Total arena capacity in bytes (PC_SCRATCH_ARENA_SIZE).
Definition scratch.cpp:201
void * scratch_alloc(size_t n, size_t align)
Borrow n bytes of scratch, aligned to align.
Definition scratch.cpp:135
int pc_scratch_slot_of(const void *p)
Which plaintext slot owns p, or -1 if p is not in the plaintext pool.
Definition scratch.cpp:211
size_t scratch_used(void)
Bytes currently handed out (0 immediately after a reset).
Definition scratch.cpp:179
size_t scratch_high_water(void)
Largest scratch_used() value seen since boot (for sizing the arena).
Definition scratch.cpp:185
void scratch_reset(void)
Reclaim the whole arena (empties it).
Definition scratch.cpp:154
void scratch_release(size_t mark)
Reclaim everything allocated since mark (LIFO).
Definition scratch.cpp:172
A byte region whose run length is bound in both directions.
A writable byte region: the storage, the capacity that belongs to it, and what has been produced into...
Definition span.h:65