Skip to main content

ph_eventing/
event_buf.rs

1//! Bounded SPSC event buffer with backpressure — no heap, no alloc.
2//!
3//! [`EventBuf`] is a fixed-size, lock-free, single-producer single-consumer
4//! ring buffer that **rejects** pushes when full instead of overwriting.
5//! This gives the producer explicit backpressure so no events are silently
6//! lost.
7//!
8//! # When to use
9//! Use `EventBuf` when every event matters and the producer can afford to
10//! handle a "buffer full" signal (retry, log, or apply its own policy).
11//! If losing old events is acceptable, prefer [`crate::SeqRing`].
12//! If you only need a single-owner ring, see [`crate::RingBuf`].
13//!
14//! # Memory ordering
15//! This is a classic Lamport SPSC queue:
16//! - The producer owns `head` (Relaxed load, Release store) and reads
17//!   `tail` with Acquire to see consumer progress.
18//! - The consumer owns `tail` (Relaxed load, Release store) and reads
19//!   `head` with Acquire to see producer progress.
20//! - A slot is written before `head` is advanced and read before `tail` is
21//!   advanced, so the Release/Acquire pairs on the cursors act as the
22//!   publication fence.
23//! - The producer and consumer never touch the same slot: `push` writes at
24//!   `head` only while `head - tail < N`, so there is no data race on the
25//!   slots themselves, only on the cursors.
26//! - [`EventBuf::len`] is the one observer that reads both cursors, so it
27//!   brackets its `head` load between two `tail` samples to get a consistent
28//!   pair.
29//!
30//! # Example
31//! ```
32//! use ph_eventing::EventBuf;
33//!
34//! let buf = EventBuf::<u32, 4>::new();
35//! let producer = buf.try_producer().expect("producer");
36//! let consumer = buf.try_consumer().expect("consumer");
37//!
38//! assert!(producer.push(1).is_ok());
39//! assert!(producer.push(2).is_ok());
40//! assert_eq!(consumer.peek(), Some(1));
41//! assert_eq!(consumer.pop(), Some(1));
42//! assert_eq!(consumer.pop(), Some(2));
43//! assert_eq!(consumer.pop(), None); // empty
44//! ```
45
46use crate::sync::{AtomicBool, AtomicU32, Ordering, TrackedCell, fence};
47use core::cell::Cell;
48use core::marker::PhantomData;
49use core::mem::MaybeUninit;
50
51// Const on the host path so `EventBuf::new` can be const. Loom's cell is not
52// const-constructible, so the Loom build keeps a non-const helper.
53//
54// Prefer `[const { … }; N]` over `array::from_fn`: the latter is not
55// const-callable with these constructors on the MSRV toolchain.
56#[cfg(not(loom))]
57const fn slot_array<T, const N: usize>() -> [TrackedCell<MaybeUninit<T>>; N] {
58    [const { TrackedCell::new(MaybeUninit::uninit()) }; N]
59}
60
61#[cfg(loom)]
62fn slot_array<T, const N: usize>() -> [TrackedCell<MaybeUninit<T>>; N] {
63    core::array::from_fn(|_| TrackedCell::new(MaybeUninit::uninit()))
64}
65
66/// How many times [`EventBuf::len`] retries its snapshot before falling back
67/// to a clamped estimate. Bounded so `len` is always wait-free.
68const RETRY_LIMIT: usize = 2;
69
70/// Bounded SPSC event buffer with backpressure.
71///
72/// When the buffer is full, [`Producer::push`] returns `Err(val)` instead
73/// of overwriting, giving the producer a chance to retry, drop, or log.
74/// The consumer drains items with [`Consumer::pop`] or [`Consumer::drain`],
75/// and can inspect the oldest item with [`Consumer::peek`] without consuming it.
76///
77/// # Panics
78/// - `EventBuf::new()` fails to compile (const assertion) when `N == 0` on the
79///   host path; under Loom it panics at runtime.
80/// - Handle acquisition never panics: [`EventBuf::try_producer`] /
81///   [`EventBuf::try_consumer`] return `None` while a handle of that kind is
82///   active. (The panicking `producer()` / `consumer()` were removed in 0.3.0.)
83pub struct EventBuf<T: Copy, const N: usize> {
84    head: AtomicU32,
85    tail: AtomicU32,
86    slots: [TrackedCell<MaybeUninit<T>>; N],
87    producer_taken: AtomicBool,
88    consumer_taken: AtomicBool,
89}
90
91// SAFETY: EventBuf is Sync because the producer/consumer handles enforce
92// SPSC usage, and the head/tail cursors are accessed via atomics with
93// Release/Acquire ordering that guarantees slot visibility. T: Send ensures
94// values can be transferred across threads safely.
95unsafe impl<T: Copy + Send, const N: usize> Sync for EventBuf<T, N> {}
96
97impl<T: Copy, const N: usize> EventBuf<T, N> {
98    /// Create a new, empty event buffer.
99    ///
100    /// On the normal (non-Loom) build this is a `const fn`, so the buffer can
101    /// be placed in a `static`:
102    /// `static BUF: EventBuf<u32, 64> = EventBuf::new();`.
103    /// Under `--cfg loom` it is deliberately non-const — Loom's atomics are
104    /// not const-constructible.
105    ///
106    /// # Capacity `0` is a build failure
107    /// The `N > 0` check is a *const* assertion, so a zero-capacity buffer
108    /// cannot be constructed at all -- there is no runtime panic left to
109    /// catch, and therefore no way to write the negative case as a `#[test]`.
110    /// This `compile_fail` doctest is that coverage, and pinning the error code
111    /// keeps it honest: without it the test would also pass on a typo.
112    ///
113    /// ```compile_fail,E0080
114    /// let _ = ph_eventing::EventBuf::<u32, 0>::new();
115    /// ```
116    ///
117    /// # Panics
118    /// Does not panic on the host path. Under Loom, where `new` is non-const,
119    /// `N == 0` is a runtime assertion instead.
120    #[cfg(not(loom))]
121    pub const fn new() -> Self {
122        const {
123            assert!(N > 0, "EventBuf capacity N must be > 0");
124        }
125        Self {
126            head: AtomicU32::new(0),
127            tail: AtomicU32::new(0),
128            slots: slot_array::<T, N>(),
129            producer_taken: AtomicBool::new(false),
130            consumer_taken: AtomicBool::new(false),
131        }
132    }
133
134    /// Create a new, empty event buffer (Loom build — non-const).
135    ///
136    /// # Panics
137    /// Panics if `N == 0`.
138    #[cfg(loom)]
139    pub fn new() -> Self {
140        assert!(N > 0, "EventBuf capacity N must be > 0");
141        Self {
142            head: AtomicU32::new(0),
143            tail: AtomicU32::new(0),
144            slots: slot_array::<T, N>(),
145            producer_taken: AtomicBool::new(false),
146            consumer_taken: AtomicBool::new(false),
147        }
148    }
149
150    #[inline(always)]
151    const fn slot_index(pos: u32) -> usize {
152        (pos as usize) % N
153    }
154
155    /// Maximum number of items the buffer can hold.
156    #[inline]
157    pub const fn capacity(&self) -> usize {
158        N
159    }
160
161    /// Approximate number of items currently buffered.
162    ///
163    /// Returns a consistent `(tail, head)` snapshot. The value may still be
164    /// stale by the time the caller acts on it, but it will never spuriously
165    /// exceed [`capacity`](Self::capacity).
166    ///
167    /// This never blocks: it makes a bounded number of attempts and then falls
168    /// back to a clamped estimate, so a busy consumer cannot stall the caller.
169    #[inline]
170    pub fn len(&self) -> usize {
171        // Seqlock-style read. Sampling `tail` on both sides of the `head` load
172        // and requiring the samples to match means `head` was observed while
173        // `tail` held still, so `head.wrapping_sub(tail)` cannot appear as a
174        // huge unsigned value after a concurrent consumer advance.
175        //
176        // The two barriers pin the `head` load between the samples. They also
177        // make equality a sound bound: if `h` observes a producer publication
178        // that reused consumer-freed space, the following Acquire fence
179        // synchronizes through that Relaxed load. The producer acquired the
180        // newer `tail` before publishing `h`, so `t2` cannot then observe an
181        // older tail. Thus `t1 == t2` implies `h - t1 <= N`.
182        //
183        // This depends on EventBuf's backpressure protocol: the producer reads
184        // `tail` with Acquire before advancing `head`. It is not a generic
185        // double-sampling property. See AGENTS.md for the full happens-before
186        // chain.
187        for _ in 0..RETRY_LIMIT {
188            let t1 = self.tail.load(Ordering::Acquire);
189            let h = self.head.load(Ordering::Relaxed);
190            fence(Ordering::Acquire);
191            let t2 = self.tail.load(Ordering::Relaxed);
192
193            if t1 == t2 {
194                return h.wrapping_sub(t1) as usize;
195            }
196        }
197
198        // The consumer moved during every attempt. Read `tail` first so a
199        // further advance can only make the count an over-estimate rather
200        // than an underflow, then clamp to preserve the capacity bound.
201        let t = self.tail.load(Ordering::Acquire);
202        let h = self.head.load(Ordering::Relaxed);
203        (h.wrapping_sub(t) as usize).min(N)
204    }
205
206    /// Returns `true` if the buffer contains no items (approximate).
207    #[inline]
208    pub fn is_empty(&self) -> bool {
209        self.len() == 0
210    }
211
212    /// Returns `true` if the buffer is at capacity (approximate).
213    #[inline]
214    pub fn is_full(&self) -> bool {
215        self.len() >= N
216    }
217
218    /// Try to create the producer handle.
219    ///
220    /// Returns `None` if a producer is already active — never panics. On the
221    /// targets this crate exists for a panic is a reset, so fallible bring-up
222    /// is the only handle-acquisition API. (The panicking `producer()` was
223    /// deprecated in 0.2.0 and removed in 0.3.0.)
224    #[inline]
225    pub fn try_producer(&self) -> Option<Producer<'_, T, N>> {
226        if self.producer_taken.swap(true, Ordering::AcqRel) {
227            None
228        } else {
229            Some(Producer {
230                buf: self,
231                _not_sync: PhantomData,
232            })
233        }
234    }
235
236    /// Try to create the consumer handle.
237    ///
238    /// Returns `None` if a consumer is already active — never panics. On the
239    /// targets this crate exists for a panic is a reset, so fallible bring-up
240    /// is the only handle-acquisition API. (The panicking `consumer()` was
241    /// deprecated in 0.2.0 and removed in 0.3.0.)
242    #[inline]
243    pub fn try_consumer(&self) -> Option<Consumer<'_, T, N>> {
244        if self.consumer_taken.swap(true, Ordering::AcqRel) {
245            None
246        } else {
247            Some(Consumer {
248                buf: self,
249                _not_sync: PhantomData,
250            })
251        }
252    }
253}
254
255impl<T: Copy, const N: usize> Default for EventBuf<T, N> {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl<T: Copy, const N: usize> core::fmt::Debug for EventBuf<T, N> {
262    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
263        f.debug_struct("EventBuf")
264            .field("len", &self.len())
265            .field("capacity", &N)
266            .finish()
267    }
268}
269
270/// Write handle for an [`EventBuf`].
271///
272/// Dropping the producer releases the slot so a new one can be created.
273pub struct Producer<'a, T: Copy, const N: usize> {
274    buf: &'a EventBuf<T, N>,
275    _not_sync: PhantomData<Cell<()>>,
276}
277
278impl<T: Copy, const N: usize> Producer<'_, T, N> {
279    /// Try to push a value into the buffer.
280    ///
281    /// Returns `Ok(())` on success, or `Err(val)` if the buffer is full
282    /// (the value is returned to the caller so nothing is lost).
283    #[inline]
284    pub fn push(&self, val: T) -> Result<(), T> {
285        let head = self.buf.head.load(Ordering::Relaxed);
286        let tail = self.buf.tail.load(Ordering::Acquire);
287        if head.wrapping_sub(tail) as usize >= N {
288            return Err(val);
289        }
290        let idx = EventBuf::<T, N>::slot_index(head);
291        // SAFETY: producer is the only writer to this slot; the consumer
292        // will not read it until head is advanced (Release below).
293        self.buf.slots[idx].with_mut(|slot| unsafe { (*slot).write(val) });
294        self.buf.head.store(head.wrapping_add(1), Ordering::Release);
295        Ok(())
296    }
297}
298
299impl<T: Copy, const N: usize> Drop for Producer<'_, T, N> {
300    fn drop(&mut self) {
301        self.buf.producer_taken.store(false, Ordering::Release);
302    }
303}
304
305impl<T: Copy, const N: usize> core::fmt::Debug for Producer<'_, T, N> {
306    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307        f.debug_struct("event_buf::Producer")
308            .field("capacity", &N)
309            .finish()
310    }
311}
312
313/// Read handle for an [`EventBuf`].
314///
315/// Dropping the consumer releases the slot so a new one can be created.
316pub struct Consumer<'a, T: Copy, const N: usize> {
317    buf: &'a EventBuf<T, N>,
318    _not_sync: PhantomData<Cell<()>>,
319}
320
321impl<T: Copy, const N: usize> Consumer<'_, T, N> {
322    /// Pop the oldest item from the buffer.
323    ///
324    /// Returns `None` if the buffer is empty.
325    #[inline]
326    pub fn pop(&self) -> Option<T> {
327        let tail = self.buf.tail.load(Ordering::Relaxed);
328        let head = self.buf.head.load(Ordering::Acquire);
329        if tail == head {
330            return None;
331        }
332        let idx = EventBuf::<T, N>::slot_index(tail);
333        // SAFETY: consumer is the only reader of this slot; the producer
334        // will not overwrite it until tail is advanced (Release below).
335        let val = self.buf.slots[idx].with(|slot| unsafe { (*slot).assume_init_read() });
336        self.buf.tail.store(tail.wrapping_add(1), Ordering::Release);
337        Some(val)
338    }
339
340    /// Copy the oldest item without removing it.
341    ///
342    /// Returns `None` if the buffer is empty. The consumer cursor is not
343    /// advanced, so a following [`pop`](Self::pop) returns the same value.
344    #[inline]
345    pub fn peek(&self) -> Option<T> {
346        let tail = self.buf.tail.load(Ordering::Relaxed);
347        let head = self.buf.head.load(Ordering::Acquire);
348        if tail == head {
349            return None;
350        }
351        let idx = EventBuf::<T, N>::slot_index(tail);
352        // SAFETY: same slot exclusivity as `pop` — the producer will not
353        // overwrite this slot until `tail` advances. `T: Copy`, so reading
354        // without advancing leaves a valid value for a later `pop`.
355        Some(self.buf.slots[idx].with(|slot| unsafe { (*slot).assume_init_read() }))
356    }
357
358    /// Drain up to `max` items, passing each to `hook`.
359    ///
360    /// Returns the number of items consumed.
361    #[inline]
362    pub fn drain(&self, max: usize, mut hook: impl FnMut(T)) -> usize {
363        let mut count = 0;
364        while count < max {
365            match self.pop() {
366                Some(val) => {
367                    hook(val);
368                    count += 1;
369                }
370                None => break,
371            }
372        }
373        count
374    }
375}
376
377impl<T: Copy, const N: usize> Drop for Consumer<'_, T, N> {
378    fn drop(&mut self) {
379        self.buf.consumer_taken.store(false, Ordering::Release);
380    }
381}
382
383impl<T: Copy, const N: usize> core::fmt::Debug for Consumer<'_, T, N> {
384    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
385        f.debug_struct("event_buf::Consumer")
386            .field("capacity", &N)
387            .finish()
388    }
389}
390
391impl<T: Copy, const N: usize> crate::traits::Sink<T> for Producer<'_, T, N> {
392    type Error = T;
393
394    #[inline]
395    fn try_push(&mut self, val: T) -> Result<(), T> {
396        self.push(val)
397    }
398}
399
400impl<T: Copy, const N: usize> crate::traits::Source<T> for Consumer<'_, T, N> {
401    #[inline]
402    fn try_pop(&mut self) -> Option<T> {
403        self.pop()
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[test]
412    fn new_buf_is_empty() {
413        let buf = EventBuf::<u32, 4>::new();
414        assert!(buf.is_empty());
415        assert!(!buf.is_full());
416        assert_eq!(buf.len(), 0);
417        assert_eq!(buf.capacity(), 4);
418    }
419
420    #[test]
421    fn push_and_pop_fifo() {
422        let buf = EventBuf::<u32, 4>::new();
423        let p = buf.try_producer().unwrap();
424        let c = buf.try_consumer().unwrap();
425
426        assert!(p.push(10).is_ok());
427        assert!(p.push(20).is_ok());
428        assert!(p.push(30).is_ok());
429
430        assert_eq!(c.pop(), Some(10));
431        assert_eq!(c.pop(), Some(20));
432        assert_eq!(c.pop(), Some(30));
433        assert_eq!(c.pop(), None);
434    }
435
436    #[test]
437    fn push_rejects_when_full() {
438        let buf = EventBuf::<u32, 2>::new();
439        let p = buf.try_producer().unwrap();
440        let c = buf.try_consumer().unwrap();
441
442        assert!(p.push(1).is_ok());
443        assert!(p.push(2).is_ok());
444        assert_eq!(p.push(3), Err(3)); // full — value returned
445
446        // drain one, then push succeeds
447        assert_eq!(c.pop(), Some(1));
448        assert!(p.push(3).is_ok());
449    }
450
451    #[test]
452    fn drain_returns_count() {
453        let buf = EventBuf::<u32, 8>::new();
454        let p = buf.try_producer().unwrap();
455        let c = buf.try_consumer().unwrap();
456
457        for i in 0..5 {
458            p.push(i).unwrap();
459        }
460
461        let mut out = std::vec::Vec::new();
462        let n = c.drain(3, |v| out.push(v));
463        assert_eq!(n, 3);
464        assert_eq!(out, [0, 1, 2]);
465
466        // remaining
467        let n = c.drain(100, |v| out.push(v));
468        assert_eq!(n, 2);
469        assert_eq!(out, [0, 1, 2, 3, 4]);
470    }
471
472    #[test]
473    fn drain_on_empty_returns_zero() {
474        let buf = EventBuf::<u32, 4>::new();
475        let _p = buf.try_producer().unwrap();
476        let c = buf.try_consumer().unwrap();
477
478        let n = c.drain(10, |_| panic!("should not be called"));
479        assert_eq!(n, 0);
480    }
481
482    #[test]
483    fn producer_consumer_can_be_recreated() {
484        let buf = EventBuf::<u32, 4>::new();
485        {
486            let p = buf.try_producer().unwrap();
487            p.push(1).unwrap();
488        }
489        // producer dropped — can create a new one
490        let p = buf.try_producer().unwrap();
491        p.push(2).unwrap();
492
493        {
494            let c = buf.try_consumer().unwrap();
495            assert_eq!(c.pop(), Some(1));
496        }
497        // consumer dropped — can create a new one
498        let c = buf.try_consumer().unwrap();
499        assert_eq!(c.pop(), Some(2));
500        assert_eq!(c.pop(), None);
501    }
502
503    #[test]
504    fn wraps_around_correctly() {
505        let buf = EventBuf::<u32, 3>::new();
506        let p = buf.try_producer().unwrap();
507        let c = buf.try_consumer().unwrap();
508
509        // fill, drain, fill again — exercises the wrap
510        for round in 0u32..4 {
511            let base = round * 3;
512            for i in 0..3 {
513                assert!(p.push(base + i).is_ok());
514            }
515            assert_eq!(p.push(99), Err(99)); // full
516            for i in 0..3 {
517                assert_eq!(c.pop(), Some(base + i));
518            }
519            assert_eq!(c.pop(), None); // empty
520        }
521    }
522
523    #[test]
524    fn default_is_new() {
525        let buf: EventBuf<u8, 4> = EventBuf::default();
526        assert!(buf.is_empty());
527    }
528
529    #[test]
530    fn len_and_full_track_state() {
531        let buf = EventBuf::<u32, 3>::new();
532        let p = buf.try_producer().unwrap();
533        let c = buf.try_consumer().unwrap();
534
535        assert_eq!(buf.len(), 0);
536        assert!(buf.is_empty());
537
538        p.push(1).unwrap();
539        assert_eq!(buf.len(), 1);
540
541        p.push(2).unwrap();
542        p.push(3).unwrap();
543        assert_eq!(buf.len(), 3);
544        assert!(buf.is_full());
545
546        c.pop();
547        assert_eq!(buf.len(), 2);
548        assert!(!buf.is_full());
549    }
550
551    #[test]
552    fn len_stays_within_capacity_while_consumer_drains() {
553        let buf = EventBuf::<u32, 8>::new();
554        let done = AtomicBool::new(false);
555        let pushes = crate::test_support::iterations(200_000);
556
557        std::thread::scope(|scope| {
558            scope.spawn(|| {
559                let p = buf.try_producer().unwrap();
560                for i in 0..pushes {
561                    let _ = p.push(i);
562                }
563                done.store(true, Ordering::Release);
564            });
565
566            scope.spawn(|| {
567                let c = buf.try_consumer().unwrap();
568                while !done.load(Ordering::Acquire) {
569                    c.pop();
570                }
571            });
572
573            // `len` races both handles; it may be stale, but it must never
574            // report more than the buffer can hold.
575            while !done.load(Ordering::Acquire) {
576                let observed = buf.len();
577                assert!(
578                    observed <= buf.capacity(),
579                    "len() reported {observed} for a capacity-{} buffer",
580                    buf.capacity()
581                );
582            }
583        });
584    }
585
586    #[test]
587    fn concurrent_spsc_preserves_fifo_and_loses_nothing() {
588        let buf = EventBuf::<u32, 4>::new();
589        let total = crate::test_support::iterations(50_000);
590
591        let received = std::thread::scope(|scope| {
592            scope.spawn(|| {
593                let p = buf.try_producer().unwrap();
594                // Backpressure means push can fail; retry so the stream is
595                // complete and any gap in the consumer's view is a real bug.
596                for i in 0..total {
597                    let mut val = i;
598                    while let Err(rejected) = p.push(val) {
599                        val = rejected;
600                        std::thread::yield_now();
601                    }
602                }
603            });
604
605            let consumer = scope.spawn(|| {
606                let c = buf.try_consumer().unwrap();
607                let mut seen = 0u32;
608                while seen < total {
609                    match c.pop() {
610                        // Strict FIFO: the nth item popped must be n.
611                        Some(val) => {
612                            assert_eq!(val, seen, "out-of-order pop at index {seen}");
613                            seen += 1;
614                        }
615                        None => std::thread::yield_now(),
616                    }
617                }
618                seen
619            });
620
621            consumer.join().unwrap()
622        });
623
624        assert_eq!(received, total);
625        assert_eq!(buf.len(), 0);
626    }
627
628    #[test]
629    fn handles_are_send() {
630        fn assert_send<T: Send>() {}
631        assert_send::<super::Producer<'_, u32, 4>>();
632        assert_send::<super::Consumer<'_, u32, 4>>();
633    }
634
635    #[test]
636    fn try_producer_and_try_consumer() {
637        let buf = EventBuf::<u32, 4>::new();
638        let p = buf.try_producer().expect("first producer");
639        assert!(buf.try_producer().is_none());
640        let c = buf.try_consumer().expect("first consumer");
641        assert!(buf.try_consumer().is_none());
642        p.push(1).unwrap();
643        assert_eq!(c.pop(), Some(1));
644        drop(p);
645        drop(c);
646        assert!(buf.try_producer().is_some());
647        assert!(buf.try_consumer().is_some());
648    }
649
650    #[test]
651    fn peek_copies_without_advancing() {
652        let buf = EventBuf::<u32, 4>::new();
653        let p = buf.try_producer().unwrap();
654        let c = buf.try_consumer().unwrap();
655
656        assert_eq!(c.peek(), None);
657        p.push(10).unwrap();
658        p.push(20).unwrap();
659        assert_eq!(c.peek(), Some(10));
660        assert_eq!(c.peek(), Some(10));
661        assert_eq!(buf.len(), 2);
662        assert_eq!(c.pop(), Some(10));
663        assert_eq!(c.peek(), Some(20));
664        assert_eq!(c.pop(), Some(20));
665        assert_eq!(c.peek(), None);
666    }
667
668    // Loom's `new` is deliberately non-const, so a `static` init only exists
669    // on the host path.
670    #[cfg(not(loom))]
671    #[test]
672    fn const_new_works_in_const_context() {
673        static BUF: EventBuf<u32, 4> = EventBuf::new();
674        assert!(BUF.is_empty());
675        assert_eq!(BUF.capacity(), 4);
676    }
677
678    // The point of the const `new` is not that a `static` compiles -- it is
679    // that handles borrowed from one are `'static` and `Send`, which is what
680    // lets the producer move into an ISR while the consumer stays in a task
681    // loop. A test that only builds the `static` would still pass if the
682    // lifetime were tied to a local, so pin the signature explicitly.
683    #[cfg(not(loom))]
684    #[test]
685    fn static_buf_yields_static_sendable_handles() {
686        static BUF: EventBuf<u32, 4> = EventBuf::new();
687
688        fn producer_for_isr() -> super::Producer<'static, u32, 4> {
689            BUF.try_producer().unwrap()
690        }
691        fn consumer_for_task() -> super::Consumer<'static, u32, 4> {
692            BUF.try_consumer().unwrap()
693        }
694        fn assert_send<T: Send>(_: &T) {}
695
696        let p = producer_for_isr();
697        let c = consumer_for_task();
698        assert_send(&p);
699        assert_send(&c);
700
701        p.push(7).unwrap();
702        assert_eq!(c.pop(), Some(7));
703    }
704}