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}