Skip to main content

ph_eventing/
event_flags.rs

1//! Coalesced condition notification for an ISR-to-task handoff.
2//!
3//! [`EventFlags`] records whether each of exactly 32 payload-free conditions
4//! occurred since the preceding take. The producer raises an [`EventMask`]
5//! with one atomic `fetch_or`; the consumer takes every pending condition with
6//! one atomic `swap(0)`. Repeated raises of the same condition may coalesce,
7//! and conditions carry neither multiplicity nor ordering.
8//!
9//! A raise uses Release ordering and a take uses Acquire ordering. Therefore,
10//! memory actions sequenced before a raise happen-before memory actions
11//! sequenced after a take that observes that raise. The flags publish the fact
12//! that application state is ready; they do not carry that state themselves.
13//!
14//! The handles are deliberately sole-role `Send + !Sync` values. An `&self`
15//! hot-path receiver does not make a handle shareable: move each handle into
16//! the one execution context that owns its role.
17
18use core::cell::Cell;
19use core::marker::PhantomData;
20use core::ops::{BitAnd, BitOr, BitOrAssign};
21
22use crate::sync::{AtomicBool, AtomicU32, Ordering};
23
24/// A set of pending EventFlags conditions.
25///
26/// The representation is exactly one `u32`: bit indices 0 through 31 are the
27/// complete condition namespace. Applications can define named `const` masks
28/// with [`EventMask::from_bits`] without introducing a runtime mapping layer.
29#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
30#[repr(transparent)]
31#[must_use]
32pub struct EventMask(u32);
33
34impl EventMask {
35    /// The empty condition set.
36    pub const EMPTY: Self = Self(0);
37
38    /// The set containing all 32 conditions.
39    pub const ALL: Self = Self(u32::MAX);
40
41    /// Construct a mask from its exact 32-bit representation.
42    #[inline(always)]
43    pub const fn from_bits(bits: u32) -> Self {
44        Self(bits)
45    }
46
47    /// Construct the one-condition mask at `index`.
48    ///
49    /// Returns `None` for an index outside `0..32`; no shift panic is
50    /// reachable, including on a hot path that validates external input.
51    #[inline(always)]
52    #[must_use]
53    pub const fn from_index(index: u32) -> Option<Self> {
54        if index < u32::BITS {
55            Some(Self(1u32 << index))
56        } else {
57            None
58        }
59    }
60
61    /// Return the exact 32-bit representation.
62    #[inline(always)]
63    #[must_use]
64    pub const fn bits(self) -> u32 {
65        self.0
66    }
67
68    /// Whether the set contains no conditions.
69    #[inline(always)]
70    #[must_use]
71    pub const fn is_empty(self) -> bool {
72        self.0 == 0
73    }
74
75    /// Whether every condition in `other` is present in this set.
76    #[inline(always)]
77    #[must_use]
78    pub const fn contains(self, other: Self) -> bool {
79        self.0 & other.0 == other.0
80    }
81
82    /// Whether this set and `other` share at least one condition.
83    #[inline(always)]
84    #[must_use]
85    pub const fn intersects(self, other: Self) -> bool {
86        self.0 & other.0 != 0
87    }
88}
89
90impl BitOr for EventMask {
91    type Output = Self;
92
93    #[inline(always)]
94    fn bitor(self, rhs: Self) -> Self::Output {
95        Self(self.0 | rhs.0)
96    }
97}
98
99impl BitOrAssign for EventMask {
100    #[inline(always)]
101    fn bitor_assign(&mut self, rhs: Self) {
102        self.0 |= rhs.0;
103    }
104}
105
106impl BitAnd for EventMask {
107    type Output = Self;
108
109    #[inline(always)]
110    fn bitand(self, rhs: Self) -> Self::Output {
111        Self(self.0 & rhs.0)
112    }
113}
114
115impl core::fmt::Debug for EventMask {
116    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117        write!(f, "EventMask({:#010x})", self.0)
118    }
119}
120
121/// A coalescing SPSC set of 32 payload-free conditions.
122///
123/// Exactly one [`Producer`] and one [`Consumer`] may be active at a time.
124/// Acquisition is fallible and non-panicking; dropping a handle releases only
125/// its role and leaves the pending set unchanged.
126pub struct EventFlags {
127    pending: AtomicU32,
128    producer_taken: AtomicBool,
129    consumer_taken: AtomicBool,
130}
131
132impl EventFlags {
133    /// Create an empty EventFlags value.
134    #[cfg(not(loom))]
135    #[must_use]
136    pub const fn new() -> Self {
137        Self {
138            pending: AtomicU32::new(0),
139            producer_taken: AtomicBool::new(false),
140            consumer_taken: AtomicBool::new(false),
141        }
142    }
143
144    /// Create an empty EventFlags value under Loom.
145    #[cfg(loom)]
146    #[must_use]
147    pub fn new() -> Self {
148        Self {
149            pending: AtomicU32::new(0),
150            producer_taken: AtomicBool::new(false),
151            consumer_taken: AtomicBool::new(false),
152        }
153    }
154
155    /// Try to acquire the sole producer handle.
156    ///
157    /// Returns `None` while another producer handle is active.
158    #[inline]
159    pub fn try_producer(&self) -> Option<Producer<'_>> {
160        if self.producer_taken.swap(true, Ordering::AcqRel) {
161            None
162        } else {
163            Some(Producer {
164                flags: self,
165                _not_sync: PhantomData,
166            })
167        }
168    }
169
170    /// Try to acquire the sole consumer handle.
171    ///
172    /// Returns `None` while another consumer handle is active.
173    #[inline]
174    pub fn try_consumer(&self) -> Option<Consumer<'_>> {
175        if self.consumer_taken.swap(true, Ordering::AcqRel) {
176            None
177        } else {
178            Some(Consumer {
179                flags: self,
180                _not_sync: PhantomData,
181            })
182        }
183    }
184}
185
186impl Default for EventFlags {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192// Deliberately opaque: printing `pending` would be a non-clearing peek —
193// exactly the advisory observation the frozen API rejects (destructive
194// `take_all` is the only read) — and a Relaxed load carries none of
195// `take_all`'s Acquire publication guarantee. Debug is required by
196// convention; it reports the type, not the state.
197impl core::fmt::Debug for EventFlags {
198    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199        f.debug_struct("EventFlags").finish_non_exhaustive()
200    }
201}
202
203/// The sole raising handle for an [`EventFlags`] value.
204///
205/// This handle is `Send + !Sync`: it may move into an ISR or another execution
206/// context, but it may not be shared between contexts.
207///
208/// ```compile_fail,E0277
209/// use ph_eventing::event_flags::Producer;
210///
211/// fn assert_sync<T: Sync>() {}
212/// assert_sync::<Producer<'static>>();
213/// ```
214pub struct Producer<'a> {
215    flags: &'a EventFlags,
216    _not_sync: PhantomData<Cell<()>>,
217}
218
219impl Producer<'_> {
220    /// Raise every condition in `mask`.
221    ///
222    /// The operation is exactly one source-level atomic `fetch_or`: no
223    /// algorithmic retry loop, and it never waits, allocates, calls user
224    /// code, or panics. How the single RMW is realised is per-ISA — a lone
225    /// `amoor.w` on RISC-V, a gated four-instruction PRIMASK critical
226    /// section on Cortex-M0, and an LDREX/STREX pair on exclusive-monitor
227    /// ARM, where a lost reservation (an intervening interrupt, or the
228    /// concurrent take's `swap`) repeats the pair. That hardware retry is
229    /// bounded by contention on the one shared word, not by anything this
230    /// code does; the uncontended cost is the measured row. A concurrent
231    /// take observes this raise in its own snapshot or leaves it pending
232    /// for the following take.
233    #[inline]
234    pub fn raise(&self, mask: EventMask) {
235        self.flags.pending.fetch_or(mask.bits(), Ordering::Release);
236    }
237}
238
239impl Drop for Producer<'_> {
240    fn drop(&mut self) {
241        self.flags.producer_taken.store(false, Ordering::Release);
242    }
243}
244
245impl core::fmt::Debug for Producer<'_> {
246    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
247        f.debug_struct("event_flags::Producer").finish()
248    }
249}
250
251/// The sole taking handle for an [`EventFlags`] value.
252///
253/// This handle is `Send + !Sync` and may take pending conditions while its
254/// paired producer raises them from another context.
255///
256/// ```compile_fail,E0277
257/// use ph_eventing::event_flags::Consumer;
258///
259/// fn assert_sync<T: Sync>() {}
260/// assert_sync::<Consumer<'static>>();
261/// ```
262pub struct Consumer<'a> {
263    flags: &'a EventFlags,
264    _not_sync: PhantomData<Cell<()>>,
265}
266
267impl Consumer<'_> {
268    /// Atomically take every pending condition and clear the set.
269    ///
270    /// Each returned bit was raised at least once after the preceding take.
271    /// Duplicate raises may coalesce and cross-condition order is not retained.
272    #[inline]
273    pub fn take_all(&self) -> EventMask {
274        EventMask::from_bits(self.flags.pending.swap(0, Ordering::Acquire))
275    }
276}
277
278impl Drop for Consumer<'_> {
279    fn drop(&mut self) {
280        self.flags.consumer_taken.store(false, Ordering::Release);
281    }
282}
283
284impl core::fmt::Debug for Consumer<'_> {
285    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
286        f.debug_struct("event_flags::Consumer").finish()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    const DATA_READY: EventMask = EventMask::from_bits(1 << 0);
295    const OVERFLOW: EventMask = EventMask::from_bits(1 << 1);
296
297    #[test]
298    fn event_flags_object_is_eight_bytes() {
299        // Pending AtomicU32 + two packed AtomicBool role claims. Docs and the
300        // admission record cite this figure; keep it from drifting silently.
301        assert_eq!(core::mem::size_of::<EventFlags>(), 8);
302        assert_eq!(core::mem::align_of::<EventFlags>(), 4);
303    }
304
305    #[test]
306    fn event_mask_is_an_explicit_panic_free_32_bit_set() {
307        // Contract M1, R1, and W1-W2.
308        assert_eq!(core::mem::size_of::<EventMask>(), 4);
309        assert!(EventMask::EMPTY.is_empty());
310        assert_eq!(EventMask::ALL.bits(), u32::MAX);
311        assert_eq!(EventMask::from_index(0), Some(DATA_READY));
312        assert_eq!(EventMask::from_index(31).unwrap().bits(), 1 << 31);
313        assert_eq!(EventMask::from_index(32), None);
314        assert_eq!(EventMask::from_index(u32::MAX), None);
315
316        let both = DATA_READY | OVERFLOW;
317        assert!(both.contains(DATA_READY));
318        assert!(both.intersects(OVERFLOW));
319        assert_eq!((both & OVERFLOW).bits(), OVERFLOW.bits());
320    }
321
322    #[test]
323    fn duplicate_raises_coalesce_and_take_clears() {
324        // Contract M1, R1-R2, T1-T2, C1, and C3.
325        let flags = EventFlags::new();
326        let producer = flags.try_producer().unwrap();
327        let consumer = flags.try_consumer().unwrap();
328
329        producer.raise(DATA_READY);
330        producer.raise(DATA_READY);
331
332        assert_eq!(consumer.take_all(), DATA_READY);
333        assert_eq!(consumer.take_all(), EventMask::EMPTY);
334    }
335
336    #[test]
337    fn multi_bit_and_all_bit_masks_round_trip() {
338        // Contract R1, T1, C1, C3, and W1.
339        let flags = EventFlags::new();
340        let producer = flags.try_producer().unwrap();
341        let consumer = flags.try_consumer().unwrap();
342
343        producer.raise(DATA_READY | OVERFLOW);
344        assert_eq!(consumer.take_all(), DATA_READY | OVERFLOW);
345        producer.raise(EventMask::ALL);
346        assert_eq!(consumer.take_all(), EventMask::ALL);
347    }
348
349    #[test]
350    fn empty_raise_and_empty_take_are_no_ops() {
351        // Contract R1 and T2.
352        let flags = EventFlags::new();
353        let producer = flags.try_producer().unwrap();
354        let consumer = flags.try_consumer().unwrap();
355
356        producer.raise(EventMask::EMPTY);
357        assert!(consumer.take_all().is_empty());
358    }
359
360    #[test]
361    fn handles_are_exclusive_and_reusable_after_drop() {
362        // Contract H1 and H3.
363        let flags = EventFlags::new();
364        let producer = flags.try_producer().unwrap();
365        let consumer = flags.try_consumer().unwrap();
366        assert!(flags.try_producer().is_none());
367        assert!(flags.try_consumer().is_none());
368
369        producer.raise(DATA_READY);
370        drop(producer);
371        drop(consumer);
372
373        let producer = flags.try_producer().expect("producer role released");
374        let consumer = flags.try_consumer().expect("consumer role released");
375        assert_eq!(consumer.take_all(), DATA_READY);
376        producer.raise(OVERFLOW);
377        assert_eq!(consumer.take_all(), OVERFLOW);
378    }
379
380    #[test]
381    fn handles_are_send_and_container_is_sync() {
382        // Contract H2. The compile-fail examples above pin `!Sync`.
383        fn assert_send<T: Send>() {}
384        fn assert_sync<T: Sync>() {}
385        assert_send::<Producer<'static>>();
386        assert_send::<Consumer<'static>>();
387        assert_sync::<EventFlags>();
388    }
389
390    #[cfg(not(loom))]
391    #[test]
392    fn const_new_works_in_static_context() {
393        // Contract H4.
394        static FLAGS: EventFlags = EventFlags::new();
395        let producer = FLAGS.try_producer().unwrap();
396        let consumer = FLAGS.try_consumer().unwrap();
397        producer.raise(DATA_READY);
398        assert_eq!(consumer.take_all(), DATA_READY);
399    }
400
401    #[cfg(not(loom))]
402    #[test]
403    fn concurrent_raise_and_take_never_loses_the_condition() {
404        // Contract C1-C3 at stress-test scale.
405        use core::sync::atomic::{AtomicBool, Ordering as CoreOrdering};
406
407        let flags = EventFlags::new();
408        let producer = flags.try_producer().unwrap();
409        let consumer = flags.try_consumer().unwrap();
410        let done = AtomicBool::new(false);
411
412        let seen = std::thread::scope(|scope| {
413            let done_for_producer = &done;
414            scope.spawn(move || {
415                for _ in 0..crate::test_support::iterations(100_000) {
416                    producer.raise(DATA_READY);
417                }
418                done_for_producer.store(true, CoreOrdering::Release);
419            });
420
421            let done_for_consumer = &done;
422            let taker = scope.spawn(move || {
423                let mut seen = EventMask::EMPTY;
424                while !done_for_consumer.load(CoreOrdering::Acquire) {
425                    seen |= consumer.take_all();
426                    std::thread::yield_now();
427                }
428                seen | consumer.take_all()
429            });
430
431            taker.join().unwrap()
432        });
433
434        assert_eq!(seen, DATA_READY);
435    }
436
437    #[cfg(not(loom))]
438    #[test]
439    fn observed_raise_publishes_preceding_memory() {
440        // Contract S1 at native/Miri scale; Loom supplies the weak-memory proof.
441        use core::sync::atomic::{AtomicU32, Ordering as CoreOrdering};
442
443        let flags = EventFlags::new();
444        let producer = flags.try_producer().unwrap();
445        let consumer = flags.try_consumer().unwrap();
446        let payload = AtomicU32::new(0);
447
448        std::thread::scope(|scope| {
449            let payload_for_producer = &payload;
450            scope.spawn(move || {
451                payload_for_producer.store(0xA5A5_5A5A, CoreOrdering::Relaxed);
452                producer.raise(DATA_READY);
453            });
454
455            while !consumer.take_all().contains(DATA_READY) {
456                std::thread::yield_now();
457            }
458            assert_eq!(payload.load(CoreOrdering::Relaxed), 0xA5A5_5A5A);
459        });
460    }
461}