Skip to main content

Crate ph_eventing

Crate ph_eventing 

Source
Expand description

Stack-allocated ring buffers for no-std embedded targets.

§Primitives

TypeWhen to reach for it
RingBufSingle-owner ring — simple, no atomics, &mut access.
SeqRingLock-free SPSC ring that overwrites old entries (lossy, high-throughput).
EventBufLock-free SPSC ring with backpressure — rejects pushes when full.

All three are fixed-size, zero-allocation, and generic over T: Copy.

§Common traits

TraitRoleImplementors
Sink<T>Accept eventsRingBuf, seq_ring::Producer, event_buf::Producer
Source<T>Yield eventsseq_ring::Consumer, event_buf::Consumer
Link<In,Out>BothBlanket 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.

§Static bring-up

static_spsc! declares a static buffer together with named handle types, so a signature need not spell out event_buf::Producer<'static, T, N>:

ph_eventing::static_spsc! {
    pub mod telemetry: EventBuf<u32, 64>;
}

fn on_sample(tx: &telemetry::Tx, v: u32) { let _ = tx.push(v); }

let (tx, rx) = telemetry::take().expect("first take");
on_sample(&tx, 1);
assert_eq!(rx.pop(), Some(1));

§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.try_producer().expect("producer");
let mut consumer = ring.try_consumer().expect("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.try_producer().expect("producer");
let consumer = buf.try_consumer().expect("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.try_producer().expect("producer");
let mut sc = seq.try_consumer().expect("consumer");

sp.push(1); sp.push(2);

let eb = EventBuf::<u32, 8>::new();
let mut ep = eb.try_producer().expect("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

  • RingBuf has no atomics and no interior mutability — standard Rust borrow rules apply. It stores slots as MaybeUninit<T> and reads only live entries, so it does contain unsafe.

  • SeqRing and EventBuf are SPSC by design: exactly one producer and one consumer must be active. Use try_producer() / try_consumer(), which return None rather than panicking — on a microcontroller a panic is a reset. The panicking producer() / consumer() are deprecated since 0.2.0. Using unsafe to bypass these constraints is undefined behavior.

    The examples here use .expect(...) for brevity, which is a panic. That is fine in a doctest on a host; in firmware, branch on the None:

let Some(tx) = buf.try_producer() else {
    return; // already claimed -- report it, do not reset the device
};
  • EventBuf is race-free by construction — its producer and consumer never touch the same slot — and passes Miri with the data-race detector enabled.
  • SeqRing is 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 any T: Copy; the seq_ring module docs give the alternatives and why each was rejected. EventBuf has 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.

  • SeqRing and EventBuf are Sync when T: Send, so &buf can be handed to both contexts. The Producer and Consumer handles are Send + !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 a const fn on the normal build, so static BUF: EventBuf<u32, 64> = EventBuf::new(); works. (Under --cfg loom it is non-const because Loom’s atomics are not const-constructible.) Handles still borrow the buffer, so an ISR / task split typically pairs the static with a StaticCell or similar for the handles themselves.

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 u32 values; 0 is reserved for “empty”.
  • poll_one/poll_up_to drain in-order and return PollStats; poll_one_value returns (seq, T) without a hook.
  • latest / latest_value read the newest value without advancing the consumer cursor.
  • If the consumer lags by more than N, it skips ahead and reports drops via PollStats.
  • Once every 2^32 - 1 pushes the sequence counter wraps, and a few extra entries are dropped there because push skips the reserved sequence 0: exactly one for a power-of-two N, none if N divides 2^32 - 1, up to N - 1 otherwise. Reported as ordinary drops, never a stale or torn value. Prefer a power of two for N; see the seq_ring module docs.
  • Consumer::dropped saturates rather than wrapping; usize is 32 bits on the targets this crate ships to, so a long-lived lagging consumer can reach the top of the range.

§EventBuf semantics

  • push returns Err(val) when the buffer is full — no data is silently lost.
  • pop returns the oldest item, or None when empty.
  • peek copies the oldest item without advancing the consumer cursor.
  • drain(max, hook) consumes up to max items 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::Sink;
pub use traits::Source;

Modules§

event_buf
Bounded SPSC event buffer with backpressure — no heap, no alloc.
ring
Fixed-size, stack-allocated ring buffer — no heap, no alloc, no atomics.
seq_ring
Lock-free SPSC overwrite ring for high-rate telemetry in no-std contexts.
traits
Common traits for event producers and consumers.

Macros§

static_spsc
Declare a static SPSC buffer with its handle types and a paired take.