DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ring.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#ifndef DETERMINISTICESPASYNCWEBSERVER_DET_RING_H
4#define DETERMINISTICESPASYNCWEBSERVER_DET_RING_H
5
6/**
7 * @file ring.h
8 * @brief Shared single-producer / single-consumer byte-ring primitive.
9 *
10 * The one implementation of the receive-ring drain math, used by BOTH transports:
11 * the server (det_conn_* in tcp.h, over conn_pool slots) and the outbound
12 * client (det_client_* over its pool). Sharing it is deliberate - it keeps the ring
13 * invariants in a single place so a consumer in any layer drains identically and
14 * cannot reimplement (and subtly break) the wrap/ordering by hand.
15 *
16 * Ownership rule: exactly one producer advances `head`, exactly one consumer
17 * advances `tail`; indices are DetAtomic so a producer's buffer writes are visible
18 * before the consumer observes the advanced index (acquire/release), correct across
19 * the tcpip_thread <-> worker/caller boundary on either core. No locks, no RMW.
20 */
21
22#include <atomic>
23#include <stddef.h>
24#include <stdint.h>
25#include <string.h> // memcpy (producer span copy)
26
27// ---------------------------------------------------------------------------
28// Cross-thread field wrapper
29// ---------------------------------------------------------------------------
30//
31// Fields shared across a producer/consumer thread boundary (ring head/tail, slot
32// state) are wrapped in DetAtomic so the ordering is enforced by construction
33// rather than by convention: every read is an acquire load and every write a
34// release store, so a producer's buffer writes are visible before the consumer
35// observes the advanced index (single-producer / single-consumer, so no
36// read-modify-write atomicity is needed - only ordering). On Xtensa/x86 an aligned
37// acquire/release load/store is a plain load/store plus a compiler barrier, so this
38// adds no lock and no measurable cost - determinism (bounded latency) is preserved.
39// The conversion operators keep every ==/=/arithmetic call site unchanged; the copy
40// ctor/assignment let a containing aggregate still be value-reset (`x = {}`).
41template <typename T> struct DetAtomic
42{
43 std::atomic<T> v;
44 DetAtomic() noexcept : v(T())
45 {
46 }
47 DetAtomic(T x) noexcept : v(x)
48 {
49 }
50 DetAtomic(const DetAtomic &o) noexcept : v(o.v.load(std::memory_order_acquire))
51 {
52 }
53 DetAtomic &operator=(const DetAtomic &o) noexcept
54 {
55 v.store(o.v.load(std::memory_order_acquire), std::memory_order_release);
56 return *this;
57 }
58 DetAtomic &operator=(T x) noexcept
59 {
60 v.store(x, std::memory_order_release);
61 return *this;
62 }
63 operator T() const noexcept
64 {
65 return v.load(std::memory_order_acquire);
66 }
67};
68
69// ---------------------------------------------------------------------------
70// SPSC ring drain math (consumer side)
71// ---------------------------------------------------------------------------
72// The caller owns the storage (`buf` of `cap` bytes) and the indices; these advance
73// `tail` only (the producer owns `head`). A read reads `tail` once and publishes it
74// once at the end (one release store), not per byte.
75
76/** @brief Bytes available to read (head - tail, modulo cap). */
77static inline size_t det_ring_available(const DetAtomic<size_t> &head, const DetAtomic<size_t> &tail, size_t cap)
78{
79 return ((size_t)head + cap - (size_t)tail) % cap;
80}
81
82/** @brief Pop one byte into @p out; false if empty. */
83static inline bool det_ring_read_byte(const uint8_t *buf, size_t cap, const DetAtomic<size_t> &head,
84 DetAtomic<size_t> &tail, uint8_t *out)
85{
86 size_t t = tail;
87 if (t == (size_t)head)
88 return false;
89 *out = buf[t];
90 tail = (t + 1) % cap;
91 return true;
92}
93
94/** @brief Pop up to @p maxn bytes into @p dst; returns the count read. */
95static inline size_t det_ring_read(const uint8_t *buf, size_t cap, const DetAtomic<size_t> &head,
96 DetAtomic<size_t> &tail, uint8_t *dst, size_t maxn)
97{
98 size_t h = head;
99 size_t t = tail;
100 size_t n = 0;
101 while (n < maxn && t != h)
102 {
103 dst[n++] = buf[t];
104 t = (t + 1) % cap;
105 }
106 tail = t;
107 return n;
108}
109
110/** @brief Copy @p n bytes at @p off ahead of the tail into @p dst WITHOUT consuming. */
111static inline void det_ring_peek(const uint8_t *buf, size_t cap, const DetAtomic<size_t> &tail, size_t off,
112 uint8_t *dst, size_t n)
113{
114 size_t idx = ((size_t)tail + off) % cap;
115 for (size_t i = 0; i < n; i++)
116 {
117 dst[i] = buf[idx];
118 idx = (idx + 1) % cap;
119 }
120}
121
122/** @brief Drop @p n bytes from the tail (advance past already-peeked data). */
123static inline void det_ring_consume(DetAtomic<size_t> &tail, size_t cap, size_t n)
124{
125 tail = ((size_t)tail + n) % cap;
126}
127
128// ---------------------------------------------------------------------------
129// SPSC ring fill (producer side)
130// ---------------------------------------------------------------------------
131// The producer owns `head`. The recv callback checks det_ring_free() against the
132// whole inbound segment (refuse it for lossless backpressure if it will not fit),
133// then copies each source span with det_ring_write_span() advancing a LOCAL head,
134// and publishes that head once at the end (one release store, not per byte).
135
136/** @brief Free space to write: (cap-1) - used, one slot reserved to tell full from empty. */
137static inline size_t det_ring_free(const DetAtomic<size_t> &head, const DetAtomic<size_t> &tail, size_t cap)
138{
139 size_t used = ((size_t)head + cap - (size_t)tail) % cap;
140 return (cap - 1) - used;
141}
142
143/**
144 * @brief Copy @p len bytes from @p src into @p buf at local index @p head, wrap-aware
145 * (a contiguous memcpy, at most two spans across the wrap), returning the advanced
146 * local head. Caller checks det_ring_free() first and publishes the returned head
147 * once. Bulk memcpy, not a per-byte loop.
148 */
149static inline size_t det_ring_write_span(uint8_t *buf, size_t cap, size_t head, const uint8_t *src, size_t len)
150{
151 while (len > 0)
152 {
153 size_t chunk = cap - head; // bytes until the buffer end (wrap point)
154 if (chunk > len)
155 chunk = len;
156 memcpy(&buf[head], src, chunk);
157 head = (head + chunk) % cap;
158 src += chunk;
159 len -= chunk;
160 }
161 return head;
162}
163
164#endif // DETERMINISTICESPASYNCWEBSERVER_DET_RING_H
DetAtomic(const DetAtomic &o) noexcept
Definition ring.h:50
DetAtomic(T x) noexcept
Definition ring.h:47
DetAtomic() noexcept
Definition ring.h:44
std::atomic< T > v
Definition ring.h:43
DetAtomic & operator=(T x) noexcept
Definition ring.h:58
DetAtomic & operator=(const DetAtomic &o) noexcept
Definition ring.h:53