Skip to main content

static_spsc

Macro static_spsc 

Source
macro_rules! static_spsc {
    (
        $(#[$attr:meta])*
        $vis:vis mod $name:ident : EventBuf<$t:ty, $n:tt>;
    ) => { ... };
    (
        $(#[$attr:meta])*
        $vis:vis mod $name:ident : SeqRing<$t:ty, $n:tt>;
    ) => { ... };
}
Expand description

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

§What it solves

A const fn new puts the buffer in .bss — no flash, no startup code. The handles are a different matter: they are Send + !Sync, which is exactly what makes it sound to move a producer into an ISR and a consumer into a task loop, and a static requires Sync. Handles can therefore never live in a static, whatever the constructor looks like. Taking them stays a runtime step, permanently.

What is left is boilerplate, and it is genuinely awkward: every function that accepts a handle must spell out ph_eventing::event_buf::Producer<'static, u32, 64>. This macro names those types for you.

§Example

ph_eventing::static_spsc! {
    /// Telemetry from the sampling ISR to the reporting task.
    pub mod telemetry: EventBuf<u32, 64>;
}

// `Tx` and `Rx` are ordinary type aliases — usable in signatures.
fn on_sample(tx: &telemetry::Tx, v: u32) {
    let _ = tx.push(v);
}

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

// SPSC is still enforced: there is only ever one of each.
assert!(telemetry::take().is_none());

SeqRing works the same way:

ph_eventing::static_spsc! {
    pub mod events: SeqRing<u32, 32>;
}

let (tx, mut rx) = events::take().expect("first take");
tx.push(1);
assert_eq!(rx.poll_one_value(), Some((1, 1)));

§Notes

  • take() is all-or-nothing. If the consumer cannot be taken it drops the producer rather than leaving the buffer half-claimed, so a failed call leaves nothing stranded.
  • It uses the fallible constructors, so no panic path reaches your binary. On a microcontroller a panic is a reset, and the panic machinery costs flash.
  • take() is not one-shot. Dropping both handles releases their claim flags, and a later call succeeds again — which is what makes a failed partial take recoverable rather than permanent.
  • The generated module owns its static, but take() is not the only way in: the accessor below hands out &'static to it, so try_producer() / try_consumer() remain reachable. That is deliberate and safe — the SPSC guarantee is enforced by the buffer’s own claim flags, not by hiding it. The macro removes the boilerplate; it does not add an invariant.