ph_eventing/lib.rs
1//! Deterministic handoff primitives for no-std embedded targets.
2//!
3//! # Primitives
4//!
5//! | Type | When to reach for it |
6//! |------|----------------------|
7//! | [`Block`] / [`BlockBuilder`] | Complete contiguous sample windows; compose with a transport. |
8//! | [`RingBuf`] | Single-owner ring — simple, no atomics, `&mut` access. |
9//! | [`SeqRing`] | Lock-free SPSC ring that **overwrites** old entries (lossy, high-throughput). |
10//! | [`EventBuf`] | Lock-free SPSC ring with **backpressure** — rejects pushes when full. |
11//! | [`CountedSignal`] | Saturating SPSC count for identical, payload-free events. |
12//! | [`EventFlags`] | Coalesced SPSC condition set — one bit per condition, one atomic operation per hot path. |
13//! | [`LatestBuf`] | Freshness-first SPSC snapshot — retains one newest unread value. |
14//!
15//! All are fixed-size and zero-allocation. The buffer types are generic
16//! over `T: Copy`; [`CountedSignal`] carries no payload and [`EventFlags`]
17//! provides exactly 32 payload-free conditions.
18//!
19//! # Common traits
20//!
21//! | Trait | Role | Implementors |
22//! |-------|------|--------------|
23//! | [`Sink<T>`](traits::Sink) | Accept events | `RingBuf`, `seq_ring::Producer`, `event_buf::Producer` |
24//! | [`Source<T>`](traits::Source) | Yield events | `seq_ring::Consumer`, `event_buf::Consumer` |
25//! | [`Link<In,Out>`](traits::Link) | Both | Blanket impl for any `Sink<In> + Source<Out>` |
26//! | [`LatestSink<T>`](traits::LatestSink) | Publish a newest value | `latest_buf::Producer` |
27//! | [`LatestSource<T>`](traits::LatestSource) | Take the newest value with loss evidence | `latest_buf::Consumer` |
28//!
29//! [`forward`](traits::forward) bridges the stream pair only; `LatestBuf`
30//! deliberately stands outside it (decision D2), and the signal types
31//! implement neither family.
32//!
33//! The [`traits::forward`] function transfers items from any `Source` to any
34//! `Sink`, making it easy to bridge different buffer types.
35//!
36//! # Static bring-up
37//!
38//! [`static_spsc!`](crate::static_spsc) declares a `static` buffer together with
39//! named handle types, so a signature need not spell out
40//! `event_buf::Producer<'static, T, N>`:
41//!
42//! ```
43//! ph_eventing::static_spsc! {
44//! pub mod telemetry: EventBuf<u32, 64>;
45//! }
46//!
47//! fn on_sample(tx: &telemetry::Tx, v: u32) { let _ = tx.push(v); }
48//!
49//! let (tx, rx) = telemetry::take().expect("first take");
50//! on_sample(&tx, 1);
51//! assert_eq!(rx.pop(), Some(1));
52//! ```
53//!
54//! # Quick start — `RingBuf`
55//! ```
56//! use ph_eventing::RingBuf;
57//!
58//! let mut ring = RingBuf::<u32, 4>::new();
59//! ring.push(1);
60//! ring.push(2);
61//! ring.push(3);
62//! assert_eq!(ring.latest(), Some(3));
63//! ```
64//!
65//! # Quick start — `SeqRing`
66//! ```
67//! use ph_eventing::SeqRing;
68//!
69//! let ring = SeqRing::<u32, 64>::new();
70//! let producer = ring.try_producer().expect("producer");
71//! let mut consumer = ring.try_consumer().expect("consumer");
72//!
73//! producer.push(42);
74//! // Assert the delivery flag, not just the hook body: an empty ring would
75//! // skip the hook and the assertions inside it would pass vacuously.
76//! let delivered = consumer.poll_one(|seq, v| {
77//! assert_eq!(seq, 1);
78//! assert_eq!(*v, 42);
79//! });
80//! assert!(delivered);
81//! ```
82//!
83//! # Quick start — `EventBuf`
84//! ```
85//! use ph_eventing::EventBuf;
86//!
87//! let buf = EventBuf::<u32, 4>::new();
88//! let producer = buf.try_producer().expect("producer");
89//! let consumer = buf.try_consumer().expect("consumer");
90//!
91//! assert!(producer.push(1).is_ok());
92//! assert!(producer.push(2).is_ok());
93//! assert_eq!(consumer.pop(), Some(1));
94//! ```
95//!
96//! # Quick start — `forward`
97//! ```
98//! use ph_eventing::{SeqRing, EventBuf};
99//! use ph_eventing::traits::{Source, Sink, forward};
100//!
101//! let seq = SeqRing::<u32, 8>::new();
102//! let sp = seq.try_producer().expect("producer");
103//! let mut sc = seq.try_consumer().expect("consumer");
104//!
105//! sp.push(1); sp.push(2);
106//!
107//! let eb = EventBuf::<u32, 8>::new();
108//! let mut ep = eb.try_producer().expect("producer");
109//!
110//! let (n, err) = forward(&mut sc, &mut ep, 10);
111//! assert_eq!(n, 2);
112//! assert!(err.is_none());
113//! ```
114//!
115//! # No-std
116//! The crate is `#![no_std]` by default. Tests require `std`.
117//!
118//! # Targets without atomics
119//! Every concurrent primitive — `SeqRing`, `EventBuf`, `EventFlags`,
120//! `CountedSignal`, and `LatestBuf` — requires 32-bit atomics. For targets that lack them
121//! (for example `thumbv6m-none-eabi`), enable
122//! `portable-atomic-unsafe-assume-single-core` or `portable-atomic-critical-section`.
123//! The crate always compiles those modules, so no-atomic targets need one of
124//! those features even when only [`RingBuf`] is used. `RingBuf` itself uses no
125//! atomics.
126//!
127//! # Safety and concurrency
128//! - `RingBuf` has no atomics and no interior mutability — standard Rust borrow
129//! rules apply. It stores slots as `MaybeUninit<T>` and reads only live
130//! entries, so it does contain `unsafe`.
131//! - `SeqRing`, `EventBuf`, and `EventFlags` are SPSC by design: exactly one
132//! producer and one consumer must be active. Handle acquisition is
133//! `try_producer()` / `try_consumer()`, which return `None` rather than
134//! panicking — on a microcontroller a panic is a reset. (The panicking
135//! `producer()` / `consumer()`, deprecated since 0.2.0, were removed in
136//! 0.3.0.) Using unsafe to bypass these constraints is undefined behavior.
137//!
138//! The examples here use `.expect(...)` for brevity, which is a panic. That
139//! is fine in a doctest on a host; in firmware, branch on the `None`:
140//!
141//! ```
142//! # use ph_eventing::EventBuf;
143//! # let buf = EventBuf::<u32, 4>::new();
144//! let Some(tx) = buf.try_producer() else {
145//! return; // already claimed -- report it, do not reset the device
146//! };
147//! # let _ = tx;
148//! ```
149//! - [`EventBuf`] is race-free by construction — its producer and consumer
150//! never touch the same slot — and passes Miri with the data-race detector
151//! enabled.
152//! - [`SeqRing`] is a seqlock and carries a **known formal data race**. A
153//! raced copy is discarded and never becomes an invalid value — within the
154//! whole-span bound: the discard compares `u32` sequences, so a consumer
155//! stalled mid-read for a full `2^32 - 1` publications can pass both checks
156//! against a rewritten slot (the [`seq_ring`] "whole-span sequence
157//! aliasing" section carries the reachability arithmetic and escape
158//! hatches). The access itself is undefined behaviour by the letter of the
159//! memory model. Practical
160//! consequence: running Miri over a test that drives this ring from two
161//! threads reports UB inside this crate — that is the deviation, not a new
162//! bug. It is a deliberate trade of formal soundness for accepting any
163//! `T: Copy`; the [`seq_ring`] module docs give the alternatives and why each
164//! was rejected. [`EventBuf`] has no such caveat, but applies backpressure
165//! rather than overwriting, so it is not a drop-in replacement.
166//!
167//! # Using it across contexts
168//! The typical embedded shape is a producer in an interrupt handler and a
169//! consumer in a task loop.
170//!
171//! - [`SeqRing`] and [`EventBuf`] are `Sync` when `T: Send`, and [`EventFlags`]
172//! is `Sync`, so a shared reference can be handed to both contexts. The
173//! `Producer` and `Consumer` handles are
174//! `Send + !Sync`: move each into the context that owns it, never share one.
175//! - The handles borrow the buffer, so the buffer must outlive them.
176//! - **`new()` is a `const fn`** on the normal build, so
177//! `static BUF: EventBuf<u32, 64> = EventBuf::new();` works. (Under
178//! `--cfg loom` it is non-const because Loom's atomics are not
179//! const-constructible.) Handles still borrow the buffer, so an ISR /
180//! task split typically pairs the `static` with a `StaticCell` or similar
181//! for the handles themselves.
182//!
183//! `N` is fixed at compile time and the buffer lives inline —
184//! `N * size_of::<T>()` bytes, no allocation. For [`EventBuf`] it is the
185//! backpressure threshold; for [`SeqRing`] it is how far the consumer may lag
186//! before entries are lost. It need not be a power of two.
187//!
188//! # SeqRing semantics
189//! - Sequence numbers are monotonically increasing `u32` values; `0` is reserved for "empty".
190//! - `poll_one`/`poll_up_to` drain in-order and return `PollStats`; `poll_one_value`
191//! returns `(seq, T)` without a hook.
192//! - `latest` / `latest_value` read the newest value without advancing the consumer cursor.
193//! - If the consumer lags by more than `N`, it skips ahead and reports drops via `PollStats`.
194//! - Once every `2^32 - 1` pushes the sequence counter wraps, and a few extra entries are dropped
195//! there because `push` skips the reserved sequence `0`: exactly one for a power-of-two `N`,
196//! none if `N` divides `2^32 - 1`, up to `N - 1` otherwise. Reported as ordinary drops, never a
197//! stale or torn value within the whole-span bound stated in the [`seq_ring`]
198//! module docs. Prefer a power of two for `N`.
199//! - `Consumer::dropped` saturates rather than wrapping; `usize` is 32 bits on
200//! the targets this crate ships to, so a long-lived lagging consumer can
201//! reach the top of the range.
202//!
203//! # EventBuf semantics
204//! - `push` returns `Err(val)` when the buffer is full — no data is silently lost.
205//! - `pop` returns the oldest item, or `None` when empty.
206//! - `peek` copies the oldest item without advancing the consumer cursor.
207//! - `drain(max, hook)` consumes up to `max` items through a callback.
208//!
209//! # EventFlags semantics
210//! - [`event_flags::Producer::raise`] unions a mask into the pending set.
211//! - [`event_flags::Consumer::take_all`] atomically returns and clears that set.
212//! - Duplicate raises may coalesce; ordering and multiplicity are not retained.
213//! - A take that observes a raise also observes memory actions sequenced before it.
214#![no_std]
215
216#[cfg(all(not(target_has_atomic = "32"), not(feature = "portable-atomic")))]
217compile_error!(
218 "ph-eventing requires 32-bit atomics. For thumbv6m and other no-atomic targets, \
219enable either the portable-atomic-unsafe-assume-single-core or portable-atomic-critical-section feature."
220);
221
222// NOTE: `portable-atomic-unsafe-assume-single-core` and
223// `portable-atomic-critical-section` select different portable-atomic backends
224// and cannot both be enabled — which makes `--all-features` unsupported for
225// this crate. The guard for it lives in build.rs, not here: both features
226// forward straight to portable-atomic, whose own `compile_error!` fires while
227// the dependency compiles, so a guard in this file would never be reached. A
228// build script does not depend on portable-atomic and runs regardless.
229
230#[macro_use]
231mod macros;
232
233pub mod block;
234pub mod counted_signal;
235pub mod event_buf;
236pub mod event_flags;
237pub mod latest_buf;
238pub mod ring;
239pub mod seq_ring;
240pub(crate) mod sync;
241pub mod traits;
242
243pub use block::{Block, BlockBuilder, FillError};
244pub use counted_signal::{CountSnapshot, CountedSignal};
245pub use event_buf::EventBuf;
246pub use event_flags::{EventFlags, EventMask};
247pub use latest_buf::{LatestBuf, LatestItem, PublishReport};
248pub use ring::RingBuf;
249pub use seq_ring::{PollStats, SeqRing};
250pub use traits::{LatestSink, LatestSource, Link, Sink, Source};
251
252#[cfg(all(loom, test))]
253mod loom_tests;
254
255#[cfg(test)]
256extern crate std;
257
258/// Helpers shared by the concurrency tests.
259#[cfg(test)]
260pub(crate) mod test_support {
261 /// Scale a stress-test loop count for the current interpreter.
262 ///
263 /// Miri executes MIR rather than machine code, so a native iteration count
264 /// would take hours. Miri's value is schedule exploration, not volume — a
265 /// few hundred interleavings under its weak-memory model catch far more
266 /// than millions of native iterations on a strongly-ordered x86 host.
267 pub(crate) fn iterations(native: u32) -> u32 {
268 if cfg!(miri) { 200 } else { native }
269 }
270}