Expand description
Stack-allocated ring buffers for no-std embedded targets.
§Primitives
| Type | When to reach for it |
|---|---|
RingBuf | Single-owner ring — simple, no atomics, &mut access. |
SeqRing | Lock-free SPSC ring that overwrites old entries (lossy, high-throughput). |
EventBuf | Lock-free SPSC ring with backpressure — rejects pushes when full. |
All three are fixed-size, zero-allocation, and generic over T: Copy.
§Common traits
| Trait | Role | Implementors |
|---|---|---|
Sink<T> | Accept events | RingBuf, seq_ring::Producer, event_buf::Producer |
Source<T> | Yield events | seq_ring::Consumer, event_buf::Consumer |
Link<In,Out> | Both | Blanket impl for any Sink<In> + Source<Out> |
The traits::forward function transfers items from any Source to any
Sink, making it easy to bridge different buffer types.
§Quick start — RingBuf
use ph_eventing::RingBuf;
let mut ring = RingBuf::<u32, 4>::new();
ring.push(1);
ring.push(2);
ring.push(3);
assert_eq!(ring.latest(), Some(3));§Quick start — SeqRing
use ph_eventing::SeqRing;
let ring = SeqRing::<u32, 64>::new();
let producer = ring.producer();
let mut consumer = ring.consumer();
producer.push(42);
consumer.poll_one(|seq, v| {
assert_eq!(seq, 1);
assert_eq!(*v, 42);
});§Quick start — EventBuf
use ph_eventing::EventBuf;
let buf = EventBuf::<u32, 4>::new();
let producer = buf.producer();
let consumer = buf.consumer();
assert!(producer.push(1).is_ok());
assert!(producer.push(2).is_ok());
assert_eq!(consumer.pop(), Some(1));§Quick start — forward
use ph_eventing::{SeqRing, EventBuf};
use ph_eventing::traits::{Source, Sink, forward};
let seq = SeqRing::<u32, 8>::new();
let sp = seq.producer();
let mut sc = seq.consumer();
sp.push(1); sp.push(2);
let eb = EventBuf::<u32, 8>::new();
let mut ep = eb.producer();
let (n, err) = forward(&mut sc, &mut ep, 10);
assert_eq!(n, 2);
assert!(err.is_none());§No-std
The crate is #![no_std] by default. Tests require std.
§Targets without atomics
SeqRing and EventBuf require 32-bit atomics. For targets that lack them
(for example thumbv6m-none-eabi), enable
portable-atomic-unsafe-assume-single-core or portable-atomic-critical-section.
The crate always compiles those modules, so no-atomic targets need one of
those features even when only RingBuf is used. RingBuf itself uses no
atomics.
§Safety and concurrency
RingBufis a plain struct — standard Rust borrow rules apply.SeqRingandEventBufare SPSC by design: exactly one producer and one consumer must be active.producer()/consumer()will panic if called while another handle of the same kind is active. Using unsafe to bypass these constraints is undefined behavior.EventBufis race-free by construction — its producer and consumer never touch the same slot — and passes Miri with the data-race detector enabled.SeqRingis a seqlock and carries a known formal data race. The copy is never returned and never becomes an invalid value, but the access is undefined behaviour by the letter of the memory model. Practical consequence: running Miri over a test that drives this ring from two threads reports UB inside this crate — that is the deviation, not a new bug. It is a deliberate trade of formal soundness for accepting anyT: Copy; theseq_ringmodule docs give the alternatives and why each was rejected.EventBufhas no such caveat, but applies backpressure rather than overwriting, so it is not a drop-in replacement.
§Using it across contexts
The typical embedded shape is a producer in an interrupt handler and a consumer in a task loop.
SeqRingandEventBufareSyncwhenT: Send, so&bufcan be handed to both contexts. TheProducerandConsumerhandles areSend + !Sync: move each into the context that owns it, never share one.- The handles borrow the buffer, so the buffer must outlive them.
new()is not aconst fn, sostatic BUF: EventBuf<u32, 64> = EventBuf::new();will not compile. Use aStaticCell, aOnceCell, or a binding inmainthat outlives its borrowers.
N is fixed at compile time and the buffer lives inline —
N * size_of::<T>() bytes, no allocation. For EventBuf it is the
backpressure threshold; for SeqRing it is how far the consumer may lag
before entries are lost. It need not be a power of two.
§SeqRing semantics
- Sequence numbers are monotonically increasing
u32values;0is reserved for “empty”. poll_one/poll_up_todrain in-order and returnPollStats.latestreads the newest value without advancing the consumer cursor.- If the consumer lags by more than
N, it skips ahead and reports drops viaPollStats. Consumer::droppedsaturates rather than wrapping;usizeis 32 bits on the targets this crate ships to, so a long-lived lagging consumer can reach the top of the range.
§EventBuf semantics
pushreturnsErr(val)when the buffer is full — no data is silently lost.popreturns the oldest item, orNonewhen empty.drain(max, hook)consumes up tomaxitems through a callback.
Re-exports§
pub use event_buf::EventBuf;pub use ring::RingBuf;pub use seq_ring::PollStats;pub use seq_ring::SeqRing;pub use traits::Link;pub use traits::Sink;pub use traits::Source;