Skip to main content

ph_eventing/
macros.rs

1//! Declarative bring-up for a `static` SPSC buffer.
2//!
3//! This is the crate's one concession to ergonomics, and it is made the way
4//! `AGENTS.md` says to make it: **at compile time, for zero runtime cost**.
5//! The macro expands to a `static`, two type aliases, and two functions. It
6//! introduces no allocation, no indirection, and no instruction that would not
7//! be there if you wrote it out.
8
9/// Declare a `static` SPSC buffer with its handle types and a paired take.
10///
11/// # What it solves
12///
13/// A `const fn new` puts the buffer in `.bss` — no flash, no startup code. The
14/// handles are a different matter: they are `Send + !Sync`, which is exactly
15/// what makes it sound to move a producer into an ISR and a consumer into a
16/// task loop, and a `static` requires `Sync`. **Handles can therefore never
17/// live in a `static`, whatever the constructor looks like.** Taking them stays
18/// a runtime step, permanently.
19///
20/// What is left is boilerplate, and it is genuinely awkward: every function
21/// that accepts a handle must spell out
22/// `ph_eventing::event_buf::Producer<'static, u32, 64>`. This macro names those
23/// types for you.
24///
25/// # Example
26///
27/// ```
28/// ph_eventing::static_spsc! {
29///     /// Telemetry from the sampling ISR to the reporting task.
30///     pub mod telemetry: EventBuf<u32, 64>;
31/// }
32///
33/// // `Tx` and `Rx` are ordinary type aliases — usable in signatures.
34/// fn on_sample(tx: &telemetry::Tx, v: u32) {
35///     let _ = tx.push(v);
36/// }
37///
38/// let (tx, rx) = telemetry::take().expect("first take");
39/// on_sample(&tx, 7);
40/// assert_eq!(rx.pop(), Some(7));
41///
42/// // SPSC is still enforced: there is only ever one of each.
43/// assert!(telemetry::take().is_none());
44/// ```
45///
46/// `SeqRing` works the same way:
47///
48/// ```
49/// ph_eventing::static_spsc! {
50///     pub mod events: SeqRing<u32, 32>;
51/// }
52///
53/// let (tx, mut rx) = events::take().expect("first take");
54/// tx.push(1);
55/// assert_eq!(rx.poll_one_value(), Some((1, 1)));
56/// ```
57///
58/// # Notes
59///
60/// - `take()` is **all-or-nothing**. If the consumer cannot be taken it drops
61///   the producer rather than leaving the buffer half-claimed, so a failed call
62///   leaves nothing stranded.
63/// - It uses the fallible constructors, so no panic path reaches your binary.
64///   On a microcontroller a panic is a reset, and the panic machinery costs
65///   flash.
66/// - `take()` is not one-shot. Dropping both handles releases their claim
67///   flags, and a later call succeeds again — which is what makes a failed
68///   partial take recoverable rather than permanent.
69/// - The generated module owns its `static`, but `take()` is not the only way
70///   in: the accessor below hands out `&'static` to it, so `try_producer()` /
71///   `try_consumer()` remain reachable. That is deliberate and safe — the SPSC
72///   guarantee is enforced by the buffer's own claim flags, not by hiding it.
73///   The macro removes the boilerplate; it does not add an invariant.
74#[macro_export]
75macro_rules! static_spsc {
76    (
77        $(#[$attr:meta])*
78        $vis:vis mod $name:ident : EventBuf<$t:ty, $n:tt>;
79    ) => {
80        $(#[$attr])*
81        // A macro cannot know its caller's context, so none of these lints can
82        // be judged from inside the expansion: whether the module is reachable,
83        // and whether a caller uses every item it generates. `dead_code` fires
84        // in the crate's own tests, which declare modules to exercise one item
85        // each. Allows are scoped to generated code only, so the lints keep
86        // biting for hand-written code.
87        #[allow(unreachable_pub, dead_code)]
88        $vis mod $name {
89            // Needed when `$t` names a type from the caller's scope; unused
90            // when it is a primitive, which is the common case.
91            #[allow(unused_imports)]
92            use super::*;
93
94            /// Write handle. `Send + !Sync`: move it into the producing context.
95            pub type Tx = $crate::event_buf::Producer<'static, $t, $n>;
96            /// Read handle. `Send + !Sync`: move it into the consuming context.
97            pub type Rx = $crate::event_buf::Consumer<'static, $t, $n>;
98
99            static BUF: $crate::EventBuf<$t, $n> = $crate::EventBuf::new();
100
101            /// Take both handles. `None` if they have already been taken.
102            ///
103            /// All-or-nothing: a failed call leaves the buffer untouched.
104            pub fn take() -> ::core::option::Option<(Tx, Rx)> {
105                let tx = BUF.try_producer()?;
106                match BUF.try_consumer() {
107                    ::core::option::Option::Some(rx) => {
108                        ::core::option::Option::Some((tx, rx))
109                    }
110                    // Dropping the producer restores its flag, so a failed take
111                    // cannot strand the buffer half-claimed.
112                    ::core::option::Option::None => {
113                        ::core::mem::drop(tx);
114                        ::core::option::Option::None
115                    }
116                }
117            }
118
119            /// The buffer itself, for observers like `len()`.
120            pub fn buffer() -> &'static $crate::EventBuf<$t, $n> {
121                &BUF
122            }
123        }
124    };
125
126    (
127        $(#[$attr:meta])*
128        $vis:vis mod $name:ident : SeqRing<$t:ty, $n:tt>;
129    ) => {
130        $(#[$attr])*
131        // A macro cannot know its caller's context, so none of these lints can
132        // be judged from inside the expansion: whether the module is reachable,
133        // and whether a caller uses every item it generates. `dead_code` fires
134        // in the crate's own tests, which declare modules to exercise one item
135        // each. Allows are scoped to generated code only, so the lints keep
136        // biting for hand-written code.
137        #[allow(unreachable_pub, dead_code)]
138        $vis mod $name {
139            // Needed when `$t` names a type from the caller's scope; unused
140            // when it is a primitive, which is the common case.
141            #[allow(unused_imports)]
142            use super::*;
143
144            /// Write handle. `Send + !Sync`: move it into the producing context.
145            pub type Tx = $crate::seq_ring::Producer<'static, $t, $n>;
146            /// Read handle. `Send + !Sync`: move it into the consuming context.
147            pub type Rx = $crate::seq_ring::Consumer<'static, $t, $n>;
148
149            static RING: $crate::SeqRing<$t, $n> = $crate::SeqRing::new();
150
151            /// Take both handles. `None` if they have already been taken.
152            ///
153            /// All-or-nothing: a failed call leaves the ring untouched.
154            pub fn take() -> ::core::option::Option<(Tx, Rx)> {
155                let tx = RING.try_producer()?;
156                match RING.try_consumer() {
157                    ::core::option::Option::Some(rx) => {
158                        ::core::option::Option::Some((tx, rx))
159                    }
160                    ::core::option::Option::None => {
161                        ::core::mem::drop(tx);
162                        ::core::option::Option::None
163                    }
164                }
165            }
166
167            /// The ring itself, for observers like `capacity()`.
168            pub fn ring() -> &'static $crate::SeqRing<$t, $n> {
169                &RING
170            }
171        }
172    };
173}
174
175#[cfg(all(test, not(loom)))]
176mod tests {
177    // Each test gets its own generated module. The statics are process-wide and
178    // `take()` is once-only, so two tests sharing one module can only both pass
179    // if they never overlap -- and `#[test]` functions run in parallel by
180    // default. Sharing did not reproduce a failure here even with a widened
181    // window, because dispatch order happens to favour it, but that is a
182    // property of the current test count and thread pool, not a guarantee.
183    // Isolation costs nothing and removes the question.
184
185    crate::static_spsc! {
186        /// Doc attributes must pass through.
187        pub mod eb_round_trip: EventBuf<u32, 4>;
188    }
189    crate::static_spsc! {
190        mod eb_once: EventBuf<u32, 4>;
191    }
192    crate::static_spsc! {
193        mod eb_partial: EventBuf<u32, 2>;
194    }
195    crate::static_spsc! {
196        pub mod sr_round_trip: SeqRing<u32, 4>;
197    }
198    crate::static_spsc! {
199        mod send_check: EventBuf<u32, 4>;
200    }
201    crate::static_spsc! {
202        mod send_check_sr: SeqRing<u32, 4>;
203    }
204
205    #[test]
206    fn event_buf_module_round_trips() {
207        let (tx, rx) = eb_round_trip::take().expect("first take");
208        tx.push(7).unwrap();
209        assert_eq!(rx.pop(), Some(7));
210        assert_eq!(eb_round_trip::buffer().capacity(), 4);
211    }
212
213    #[test]
214    fn event_buf_take_is_once_only() {
215        let first = eb_once::take();
216        assert!(first.is_some(), "first take must succeed");
217        assert!(eb_once::take().is_none(), "second take must fail");
218        drop(first);
219        assert!(eb_once::take().is_some(), "take succeeds again after drop");
220    }
221
222    #[test]
223    fn seq_ring_module_round_trips() {
224        let (tx, mut rx) = sr_round_trip::take().expect("first take");
225        tx.push(9);
226        assert_eq!(rx.poll_one_value(), Some((1, 9)));
227        assert_eq!(sr_round_trip::ring().capacity(), 4);
228    }
229
230    /// The property that separates `take()` from two separate calls: a failed
231    /// take must not leave the buffer half-claimed.
232    #[test]
233    fn failed_take_strands_nothing() {
234        // Hold the consumer, so `take` gets past the producer and then fails.
235        let rx = eb_partial::buffer().try_consumer().expect("consumer");
236        assert!(eb_partial::take().is_none());
237
238        // If `take` had leaked the producer, this would be None.
239        let tx = eb_partial::buffer()
240            .try_producer()
241            .expect("producer must still be free after a failed take");
242        tx.push(1).unwrap();
243        assert_eq!(rx.pop(), Some(1));
244    }
245
246    #[test]
247    fn handles_are_send() {
248        fn assert_send<T: Send>() {}
249        assert_send::<send_check::Tx>();
250        assert_send::<send_check::Rx>();
251        assert_send::<send_check_sr::Tx>();
252        assert_send::<send_check_sr::Rx>();
253    }
254}