Skip to main content

ph_eventing/
lib.rs

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