ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 PROTOCORE_RING_H
4#define PROTOCORE_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 (pc_conn_* in tcp.h, over conn_pool slots) and the outbound
12 * client (pc_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 pc_atomic 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 pc_atomic 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 pc_atomic
42{
43 std::atomic<T> v;
44 pc_atomic() noexcept : v(T())
45 {
46 }
47 pc_atomic(T x) noexcept : v(x)
48 {
49 }
50 pc_atomic(const pc_atomic &o) noexcept : v(o.v.load(std::memory_order_acquire))
51 {
52 }
53 pc_atomic &operator=(const pc_atomic &o) noexcept
54 {
55 v.store(o.v.load(std::memory_order_acquire), std::memory_order_release);
56 return *this;
57 }
58 pc_atomic &operator=(T x) noexcept
59 {
60 v.store(x, std::memory_order_release); // GCOVR_EXCL_BR_LINE - the only branches here come from
61 // ThreadSanitizer's atomic-store instrumentation (fixed release memory-order dispatch);
62 // the residual arm is unreachable in a correct SPSC program - see test_spsc_ring_no_race.
63 return *this;
64 }
65 operator T() const noexcept
66 {
67 return v.load(std::memory_order_acquire); // GCOVR_EXCL_BR_LINE - the only branches here come
68 // from ThreadSanitizer's atomic-load instrumentation (fixed acquire memory-order dispatch);
69 // the residual arm is unreachable in a correct SPSC program - see test_spsc_ring_no_race.
70 }
71};
72
73// ---------------------------------------------------------------------------
74// SPSC ring drain math (consumer side)
75// ---------------------------------------------------------------------------
76// The caller owns the storage (`buf` of `cap` bytes) and the indices; these advance
77// `tail` only (the producer owns `head`). A read reads `tail` once and publishes it
78// once at the end (one release store), not per byte.
79
80/** @brief Bytes available to read (head - tail, modulo cap). */
81static inline size_t pc_ring_available(const pc_atomic<size_t> &head, const pc_atomic<size_t> &tail, size_t cap)
82{
83 return ((size_t)head + cap - (size_t)tail) % cap;
84}
85
86/** @brief Pop one byte into @p out; false if empty. */
87static inline bool pc_ring_read_byte(const uint8_t *buf, size_t cap, const pc_atomic<size_t> &head,
88 pc_atomic<size_t> &tail, uint8_t *out)
89{
90 size_t t = tail;
91 if (t == (size_t)head)
92 {
93 return false;
94 }
95 *out = buf[t];
96 tail = (t + 1) % cap;
97 return true;
98}
99
100/** @brief Pop up to @p maxn bytes into @p dst; returns the count read. */
101static inline size_t pc_ring_read(const uint8_t *buf, size_t cap, const pc_atomic<size_t> &head,
102 pc_atomic<size_t> &tail, uint8_t *dst, size_t maxn)
103{
104 size_t h = head;
105 size_t t = tail;
106 size_t n = 0;
107 while (n < maxn && t != h)
108 {
109 dst[n++] = buf[t];
110 t = (t + 1) % cap;
111 }
112 tail = t;
113 return n;
114}
115
116/** @brief Copy @p n bytes at @p off ahead of the tail into @p dst WITHOUT consuming. */
117static inline void pc_ring_peek(const uint8_t *buf, size_t cap, const pc_atomic<size_t> &tail, size_t off, uint8_t *dst,
118 size_t n)
119{
120 size_t idx = ((size_t)tail + off) % cap;
121 for (size_t i = 0; i < n; i++)
122 {
123 dst[i] = buf[idx];
124 idx = (idx + 1) % cap;
125 }
126}
127
128/** @brief Drop @p n bytes from the tail (advance past already-peeked data). */
129static inline void pc_ring_consume(pc_atomic<size_t> &tail, size_t cap, size_t n)
130{
131 tail = ((size_t)tail + n) % cap;
132}
133
134// ---------------------------------------------------------------------------
135// SPSC ring fill (producer side)
136// ---------------------------------------------------------------------------
137// The producer owns `head`. The recv callback checks pc_ring_free() against the
138// whole inbound segment (refuse it for lossless backpressure if it will not fit),
139// then copies each source span with pc_ring_write_span() advancing a LOCAL head,
140// and publishes that head once at the end (one release store, not per byte).
141
142/** @brief Free space to write: (cap-1) - used, one slot reserved to tell full from empty. */
143static inline size_t pc_ring_free(const pc_atomic<size_t> &head, const pc_atomic<size_t> &tail, size_t cap)
144{
145 size_t used = ((size_t)head + cap - (size_t)tail) % cap;
146 return (cap - 1) - used;
147}
148
149/**
150 * @brief Copy @p len bytes from @p src into @p buf at local index @p head, wrap-aware
151 * (a contiguous memcpy, at most two spans across the wrap), returning the advanced
152 * local head. Caller checks pc_ring_free() first and publishes the returned head
153 * once. Bulk memcpy, not a per-byte loop.
154 */
155static inline size_t pc_ring_write_span(uint8_t *buf, size_t cap, size_t head, const uint8_t *src, size_t len)
156{
157 while (len > 0)
158 {
159 size_t chunk = cap - head; // bytes until the buffer end (wrap point)
160 if (chunk > len)
161 {
162 chunk = len;
163 }
164 memcpy(&buf[head], src, chunk);
165 head = (head + chunk) % cap;
166 src += chunk;
167 len -= chunk;
168 }
169 return head;
170}
171
172#endif // PROTOCORE_RING_H
pc_atomic(T x) noexcept
Definition ring.h:47
pc_atomic & operator=(const pc_atomic &o) noexcept
Definition ring.h:53
pc_atomic() noexcept
Definition ring.h:44
pc_atomic(const pc_atomic &o) noexcept
Definition ring.h:50
pc_atomic & operator=(T x) noexcept
Definition ring.h:58
std::atomic< T > v
Definition ring.h:43