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; `try_producer()` /
98//!   `try_consumer()` return `None` instead. Using unsafe to bypass these
99//!   constraints is undefined behavior.
100//! - [`EventBuf`] is race-free by construction — its producer and consumer
101//!   never touch the same slot — and passes Miri with the data-race detector
102//!   enabled.
103//! - [`SeqRing`] is a seqlock and carries a **known formal data race**. The
104//!   copy is never returned and never becomes an invalid value, but the access
105//!   is undefined behaviour by the letter of the memory model. Practical
106//!   consequence: running Miri over a test that drives this ring from two
107//!   threads reports UB inside this crate — that is the deviation, not a new
108//!   bug. It is a deliberate trade of formal soundness for accepting any
109//!   `T: Copy`; the [`seq_ring`] module docs give the alternatives and why each
110//!   was rejected. [`EventBuf`] has no such caveat, but applies backpressure
111//!   rather than overwriting, so it is not a drop-in replacement.
112//!
113//! # Using it across contexts
114//! The typical embedded shape is a producer in an interrupt handler and a
115//! consumer in a task loop.
116//!
117//! - [`SeqRing`] and [`EventBuf`] are `Sync` when `T: Send`, so `&buf` can be
118//!   handed to both contexts. The `Producer` and `Consumer` handles are
119//!   `Send + !Sync`: move each into the context that owns it, never share one.
120//! - The handles borrow the buffer, so the buffer must outlive them.
121//! - **`new()` is not a `const fn`**, so
122//!   `static BUF: EventBuf<u32, 64> = EventBuf::new();` will not compile. Use a
123//!   `StaticCell`, a `OnceCell`, or a binding in `main` that outlives its
124//!   borrowers.
125//!
126//! `N` is fixed at compile time and the buffer lives inline —
127//! `N * size_of::<T>()` bytes, no allocation. For [`EventBuf`] it is the
128//! backpressure threshold; for [`SeqRing`] it is how far the consumer may lag
129//! before entries are lost. It need not be a power of two.
130//!
131//! # SeqRing semantics
132//! - Sequence numbers are monotonically increasing `u32` values; `0` is reserved for "empty".
133//! - `poll_one`/`poll_up_to` drain in-order and return `PollStats`; `poll_one_value`
134//!   returns `(seq, T)` without a hook.
135//! - `latest` / `latest_value` read the newest value without advancing the consumer cursor.
136//! - If the consumer lags by more than `N`, it skips ahead and reports drops via `PollStats`.
137//! - Once every `2^32 - 1` pushes the sequence counter wraps, and a few extra entries are dropped
138//!   there because `push` skips the reserved sequence `0`: exactly one for a power-of-two `N`,
139//!   none if `N` divides `2^32 - 1`, up to `N - 1` otherwise. Reported as ordinary drops, never a
140//!   stale or torn value. Prefer a power of two for `N`; see the [`seq_ring`] module docs.
141//! - `Consumer::dropped` saturates rather than wrapping; `usize` is 32 bits on
142//!   the targets this crate ships to, so a long-lived lagging consumer can
143//!   reach the top of the range.
144//!
145//! # EventBuf semantics
146//! - `push` returns `Err(val)` when the buffer is full — no data is silently lost.
147//! - `pop` returns the oldest item, or `None` when empty.
148//! - `peek` copies the oldest item without advancing the consumer cursor.
149//! - `drain(max, hook)` consumes up to `max` items through a callback.
150#![no_std]
151
152#[cfg(all(not(target_has_atomic = "32"), not(feature = "portable-atomic")))]
153compile_error!(
154    "ph-eventing requires 32-bit atomics. For thumbv6m and other no-atomic targets, \
155enable either the portable-atomic-unsafe-assume-single-core or portable-atomic-critical-section feature."
156);
157
158// NOTE: `portable-atomic-unsafe-assume-single-core` and
159// `portable-atomic-critical-section` select different portable-atomic backends
160// and cannot both be enabled — which makes `--all-features` unsupported for
161// this crate. The guard for it lives in build.rs, not here: both features
162// forward straight to portable-atomic, whose own `compile_error!` fires while
163// the dependency compiles, so a guard in this file would never be reached. A
164// build script does not depend on portable-atomic and runs regardless.
165
166pub mod event_buf;
167pub mod ring;
168pub mod seq_ring;
169pub(crate) mod sync;
170pub mod traits;
171
172pub use event_buf::EventBuf;
173pub use ring::RingBuf;
174pub use seq_ring::{PollStats, SeqRing};
175pub use traits::{Link, Sink, Source};
176
177#[cfg(all(loom, test))]
178mod loom_tests;
179
180#[cfg(test)]
181extern crate std;
182
183/// Helpers shared by the concurrency tests.
184#[cfg(test)]
185pub(crate) mod test_support {
186    /// Scale a stress-test loop count for the current interpreter.
187    ///
188    /// Miri executes MIR rather than machine code, so a native iteration count
189    /// would take hours. Miri's value is schedule exploration, not volume — a
190    /// few hundred interleavings under its weak-memory model catch far more
191    /// than millions of native iterations on a strongly-ordered x86 host.
192    pub(crate) fn iterations(native: u32) -> u32 {
193        if cfg!(miri) { 200 } else { native }
194    }
195}