Stack-allocated ring buffers for no-std embedded targets.
| 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.
- 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).
- 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)
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 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));
assert_eq!(ring.get(0), Some(1)); // oldest
// iterate oldest → newest
for val in ring.iter() {
// 1, 2, 3
}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 ph_eventing::SeqRing;
let ring = SeqRing::<u32, 64>::new();
let producer = ring.producer();
let mut consumer = ring.consumer();
producer.push(123);
consumer.poll_one(|seq, v| {
assert_eq!(seq, 1);
assert_eq!(*v, 123);
});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 ph_eventing::EventBuf;
let buf = EventBuf::<u32, 2>::new();
let producer = buf.producer();
let consumer = buf.consumer();
assert!(producer.push(1).is_ok());
assert!(producer.push(2).is_ok());
assert_eq!(producer.push(3), Err(3)); // full — value returned
assert_eq!(consumer.pop(), Some(1));
assert!(producer.push(3).is_ok()); // space freedAll producers implement Sink<T> and all consumers implement Source<T>,
so you can write generic code that works with any combination:
use ph_eventing::{SeqRing, EventBuf};
use ph_eventing::traits::{Source, Sink, forward};
// bridge a SeqRing producer → EventBuf consumer
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());| 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> |
- 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.
- 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.
- 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.
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.
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
MIT. See LICENSE.