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//! # Quick start — `RingBuf`
25//! ```
26//! use ph_eventing::RingBuf;
27//!
28//! let mut ring = RingBuf::<u32, 4>::new();
29//! ring.push(1);
30//! ring.push(2);
31//! ring.push(3);
32//! assert_eq!(ring.latest(), Some(3));
33//! ```
34//!
35//! # Quick start — `SeqRing`
36//! ```
37//! use ph_eventing::SeqRing;
38//!
39//! let ring = SeqRing::<u32, 64>::new();
40//! let producer = ring.producer();
41//! let mut consumer = ring.consumer();
42//!
43//! producer.push(42);
44//! consumer.poll_one(|seq, v| {
45//!     assert_eq!(seq, 1);
46//!     assert_eq!(*v, 42);
47//! });
48//! ```
49//!
50//! # Quick start — `EventBuf`
51//! ```
52//! use ph_eventing::EventBuf;
53//!
54//! let buf = EventBuf::<u32, 4>::new();
55//! let producer = buf.producer();
56//! let consumer = buf.consumer();
57//!
58//! assert!(producer.push(1).is_ok());
59//! assert!(producer.push(2).is_ok());
60//! assert_eq!(consumer.pop(), Some(1));
61//! ```
62//!
63//! # Quick start — `forward`
64//! ```
65//! use ph_eventing::{SeqRing, EventBuf};
66//! use ph_eventing::traits::{Source, Sink, forward};
67//!
68//! let seq = SeqRing::<u32, 8>::new();
69//! let sp = seq.producer();
70//! let mut sc = seq.consumer();
71//!
72//! sp.push(1); sp.push(2);
73//!
74//! let eb = EventBuf::<u32, 8>::new();
75//! let mut ep = eb.producer();
76//!
77//! let (n, err) = forward(&mut sc, &mut ep, 10);
78//! assert_eq!(n, 2);
79//! assert!(err.is_none());
80//! ```
81//!
82//! # No-std
83//! The crate is `#![no_std]` by default. Tests require `std`.
84//!
85//! # Targets without atomics
86//! `SeqRing` and `EventBuf` require 32-bit atomics. For targets that lack them
87//! (for example `thumbv6m-none-eabi`), enable
88//! `portable-atomic-unsafe-assume-single-core` or `portable-atomic-critical-section`.
89//! The crate always compiles those modules, so no-atomic targets need one of
90//! those features even when only [`RingBuf`] is used. `RingBuf` itself uses no
91//! atomics.
92//!
93//! # Safety and concurrency
94//! - `RingBuf` is a plain struct — standard Rust borrow rules apply.
95//! - `SeqRing` and `EventBuf` are SPSC by design: exactly one producer and one
96//!   consumer must be active. `producer()`/`consumer()` will panic if called
97//!   while another handle of the same kind is active. Using unsafe to bypass
98//!   these constraints is undefined behavior.
99//! - [`EventBuf`] is race-free by construction — its producer and consumer
100//!   never touch the same slot — and passes Miri with the data-race detector
101//!   enabled.
102//! - [`SeqRing`] is a seqlock and carries a **known formal data race**. The
103//!   copy is never returned and never becomes an invalid value, but the access
104//!   is undefined behaviour by the letter of the memory model. Practical
105//!   consequence: running Miri over a test that drives this ring from two
106//!   threads reports UB inside this crate — that is the deviation, not a new
107//!   bug. It is a deliberate trade of formal soundness for accepting any
108//!   `T: Copy`; the [`seq_ring`] module docs give the alternatives and why each
109//!   was rejected. [`EventBuf`] has no such caveat, but applies backpressure
110//!   rather than overwriting, so it is not a drop-in replacement.
111//!
112//! # Using it across contexts
113//! The typical embedded shape is a producer in an interrupt handler and a
114//! consumer in a task loop.
115//!
116//! - [`SeqRing`] and [`EventBuf`] are `Sync` when `T: Send`, so `&buf` can be
117//!   handed to both contexts. The `Producer` and `Consumer` handles are
118//!   `Send + !Sync`: move each into the context that owns it, never share one.
119//! - The handles borrow the buffer, so the buffer must outlive them.
120//! - **`new()` is not a `const fn`**, so
121//!   `static BUF: EventBuf<u32, 64> = EventBuf::new();` will not compile. Use a
122//!   `StaticCell`, a `OnceCell`, or a binding in `main` that outlives its
123//!   borrowers.
124//!
125//! `N` is fixed at compile time and the buffer lives inline —
126//! `N * size_of::<T>()` bytes, no allocation. For [`EventBuf`] it is the
127//! backpressure threshold; for [`SeqRing`] it is how far the consumer may lag
128//! before entries are lost. It need not be a power of two.
129//!
130//! # SeqRing semantics
131//! - Sequence numbers are monotonically increasing `u32` values; `0` is reserved for "empty".
132//! - `poll_one`/`poll_up_to` drain in-order and return `PollStats`.
133//! - `latest` reads the newest value without advancing the consumer cursor.
134//! - If the consumer lags by more than `N`, it skips ahead and reports drops via `PollStats`.
135//! - `Consumer::dropped` saturates rather than wrapping; `usize` is 32 bits on
136//!   the targets this crate ships to, so a long-lived lagging consumer can
137//!   reach the top of the range.
138//!
139//! # EventBuf semantics
140//! - `push` returns `Err(val)` when the buffer is full — no data is silently lost.
141//! - `pop` returns the oldest item, or `None` when empty.
142//! - `drain(max, hook)` consumes up to `max` items through a callback.
143#![no_std]
144
145#[cfg(all(not(target_has_atomic = "32"), not(feature = "portable-atomic")))]
146compile_error!(
147    "ph-eventing requires 32-bit atomics. For thumbv6m and other no-atomic targets, \
148enable either the portable-atomic-unsafe-assume-single-core or portable-atomic-critical-section feature."
149);
150
151// NOTE: `portable-atomic-unsafe-assume-single-core` and
152// `portable-atomic-critical-section` select different portable-atomic backends
153// and cannot both be enabled — which makes `--all-features` unsupported for
154// this crate. The guard for it lives in build.rs, not here: both features
155// forward straight to portable-atomic, whose own `compile_error!` fires while
156// the dependency compiles, so a guard in this file would never be reached. A
157// build script does not depend on portable-atomic and runs regardless.
158
159pub mod event_buf;
160pub mod ring;
161pub mod seq_ring;
162pub(crate) mod sync;
163pub mod traits;
164
165pub use event_buf::EventBuf;
166pub use ring::RingBuf;
167pub use seq_ring::{PollStats, SeqRing};
168pub use traits::{Link, Sink, Source};
169
170#[cfg(all(loom, test))]
171mod loom_tests;
172
173#[cfg(test)]
174extern crate std;
175
176/// Helpers shared by the concurrency tests.
177#[cfg(test)]
178pub(crate) mod test_support {
179    /// Scale a stress-test loop count for the current interpreter.
180    ///
181    /// Miri executes MIR rather than machine code, so a native iteration count
182    /// would take hours. Miri's value is schedule exploration, not volume — a
183    /// few hundred interleavings under its weak-memory model catch far more
184    /// than millions of native iterations on a strongly-ordered x86 host.
185    pub(crate) fn iterations(native: u32) -> u32 {
186        if cfg!(miri) { 200 } else { native }
187    }
188}