ph-eventing
Stack-allocated ring buffers for no-std embedded targets.
What's in the box
| Type | Use case |
|---|---|
RingBuf<T, N> |
Single-owner ring buffer — simple, no atomics, &mut access. |
SeqRing<T, N> |
Lock-free SPSC ring that overwrites old entries (lossy, high-throughput). |
EventBuf<T, N> |
Lock-free SPSC ring with backpressure — rejects pushes when full. |
All three are fixed-size, #![no_std], zero-allocation, and generic over T: Copy.
Features
- Three ring buffer flavours: single-owner, lossy SPSC, and backpressure SPSC.
- Common
Sink/Source/Linktraits for writing generic event-processing code. forward(src, snk, max)utility to bridge anySource→Sink.- No heap, no dynamic dispatch, no required dependencies.
- Optional
portable-atomicsupport for targets without native 32-bit atomics. - Designed for
#![no_std]environments (std only for tests).
Compatibility
- MSRV: Rust 1.92.0.
SeqRing::new()andEventBuf::new()assertN > 0.SeqRingandEventBufrequire 32-bit atomics by default.- For
thumbv6m-none-eabi(and other no-atomic targets), enable one of:portable-atomic-unsafe-assume-single-coreportable-atomic-critical-section(requires a critical-section implementation in the binary)
Usage
RingBuf
A straightforward, single-owner ring buffer for collecting values when you don't need cross-thread access. When full, new pushes silently overwrite the oldest entry.
use RingBuf;
let mut ring = new;
ring.push;
ring.push;
ring.push;
assert_eq!;
assert_eq!; // oldest
// iterate oldest → newest
for val in ring.iter
SeqRing
A lock-free SPSC ring for high-rate telemetry. The producer never blocks;
the consumer reports drops when it lags behind by more than N.
use SeqRing;
let ring = new;
let producer = ring.producer;
let mut consumer = ring.consumer;
producer.push;
consumer.poll_one;
EventBuf
A bounded SPSC queue with backpressure. When the buffer is full, push
returns Err(val) so the producer can decide what to do — no data is
silently lost.
use EventBuf;
let buf = new;
let producer = buf.producer;
let consumer = buf.consumer;
assert!;
assert!;
assert_eq!; // full — value returned
assert_eq!;
assert!; // space freed
Common Traits
All producers implement Sink<T> and all consumers implement Source<T>,
so you can write generic code that works with any combination:
use ;
use ;
// bridge a SeqRing producer → EventBuf consumer
let seq = new;
let sp = seq.producer;
let mut sc = seq.consumer;
sp.push; sp.push;
let eb = new;
let mut ep = eb.producer;
let = forward;
assert_eq!;
assert!;
| 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 Sink<In> + Source<Out> |
Semantics
RingBuf
- Single-owner (
&mut selfto push). get(i)returns thei-th element where0is the oldest.latest()returns the most recently pushed element.iter()yields elements oldest → newest.
SeqRing
- Sequence numbers are monotonically increasing
u32values;0is reserved for "empty". - When the producer wraps the ring, old values are overwritten.
poll_oneandpoll_up_todrain in-order and returnPollStats(read,dropped,newest).latestreads the newest value without advancing the consumer cursor.- If the consumer lags by more than
N, it skips ahead and reports drops viaPollStats.
EventBuf
- FIFO order:
popalways returns the oldest item. pushreturnsOk(())on success orErr(val)when the buffer is full.drain(max, hook)consumes up tomaxitems through a callback and returns the count.- No data is silently lost — the producer always knows when the buffer cannot accept more.
Safety and Concurrency
RingBufis a plain struct with no interior mutability — standard Rust borrow rules apply.SeqRingandEventBufare SPSC by design: exactly one producer and one consumer may be active.producer()/consumer()will panic if called while another handle of the same kind is active. Using unsafe to bypass these constraints (or sharing handles concurrently) is undefined behavior.T: Copyis required by all types to avoid allocation and return values by copy.
Testing
46 unit tests and 7 doctests covering all three buffer types plus the trait
system. Host tests require std:
cargo test
| Module | Tests |
|---|---|
event_buf |
12 |
ring |
10 |
seq_ring |
13 |
traits |
11 |
| doctests | 7 |
| Total | 53 |
Coverage snapshot (2026-02-08, via cargo llvm-cov):
| Metric | Covered | Total | % |
|---|---|---|---|
| Lines | 784 | 857 | 91.5 |
| Functions | 115 | 130 | 88.5 |
| Regions | 1392 | 1490 | 93.4 |
| Instantiations | 220 | 237 | 92.8 |
To regenerate:
cargo llvm-cov --json --summary-only --output-path target/llvm-cov/summary.json
License
MIT. See LICENSE.