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}