Skip to main content

subetha_cxc/
spsc_ring.rs

1//! `SpscRingCore` - Lamport 1983 single-producer / single-consumer
2//! ring backed by a memory-mapped file.
3//!
4//! This is the SPSC-specialised counterpart of
5//! [`SharedRing`](crate::SharedRing). Where `SharedRing` carries
6//! the Vyukov MPMC protocol (per-slot sequence number, CAS on the
7//! producer / consumer counters), `SpscRingCore` strips the protocol
8//! down to its SPSC minimum: a head counter the producer owns, a
9//! tail counter the consumer owns, and payload-only slots.
10//!
11//! # Per-op atomic budget
12//!
13//! Push:
14//!  1. `head.load(Relaxed)` - owner-private; no cross-thread contention.
15//!  2. `tail.load(Acquire)` - read the consumer's position to check full.
16//!  3. write payload (non-atomic memcpy into the slot).
17//!  4. `head.store(head + 1, Release)` - publish to the consumer.
18//!
19//! That is **one Acquire load + one Release store** of cross-thread-
20//! visible atomics, plus one owner-private Relaxed load. Vyukov MPMC
21//! on the same shape needs four cross-thread atomics (load + CAS on
22//! `producer_seq`, then load + store on the slot's sequence number).
23//! Halving the atomic budget is where the Lamport SPSC win comes from.
24//!
25//! Pop mirrors push.
26//!
27//! # False sharing
28//!
29//! `head` and `tail` live on separate 64-byte cache lines. The
30//! producer writes head every push; the consumer writes tail every
31//! pop. Co-locating them would invalidate the peer's cache line on
32//! every op and crater throughput.
33//!
34//! # Crash recovery
35//!
36//! Producer crash: if the sole producer dies between writing payload
37//! and the Release-store on `head`, the head counter never advances
38//! and the consumer sees no new item. The slot at `head % cap`
39//! contains partial / garbage bytes, but the consumer never reads
40//! it because head was not published. There is no stuck-slot
41//! pathology to heal - unlike `SharedRing`'s Vyukov protocol, the
42//! producer never "claims" a slot before publishing.
43//!
44//! Consumer crash: same shape; tail does not advance, head keeps
45//! growing, ring fills, producer eventually returns `Full`.
46//!
47//! In SPSC there is no second producer to take over from the dead
48//! one, so producer-side recovery is "restart the sole producer".
49//! No heal_stuck_slot equivalent is needed or possible here.
50
51use std::cell::UnsafeCell;
52use std::fs::{File, OpenOptions};
53use std::path::Path;
54use std::sync::atomic::{AtomicU64, Ordering};
55
56use memmap2::{MmapMut, MmapOptions};
57
58use crate::shared_ring::RingError;
59
60/// Magic number identifying a Lamport SPSC ring header. ASCII
61/// "SPSC" + version byte.
62pub const SPSC_MAGIC: u64 = 0x5350_5343_0000_0001;
63
64/// Each slot is exactly one cache line. Payload-only (no per-slot
65/// atomic), so the full 64 bytes are available to the caller.
66pub const SPSC_SLOT_SIZE: usize = 64;
67
68/// Payload bytes per slot.
69pub const SPSC_PAYLOAD_BYTES: usize = SPSC_SLOT_SIZE;
70
71/// Header layout for a Lamport SPSC ring. Three cache lines:
72/// metadata, then producer-owned `head`, then consumer-owned `tail`.
73/// Separate cache lines for `head` and `tail` eliminate false
74/// sharing between producer and consumer hot paths.
75#[repr(C, align(64))]
76pub struct SpscHeader {
77    pub magic: u64,
78    pub capacity: u64,
79    pub slot_size: u64,
80    /// Pad metadata line out to 64 bytes.
81    _pad_meta: [u8; 64 - 24],
82    /// Producer-owned head counter; consumer reads via Acquire.
83    pub head: AtomicU64,
84    _pad_head: [u8; 64 - 8],
85    /// Consumer-owned tail counter; producer reads via Acquire.
86    pub tail: AtomicU64,
87    _pad_tail: [u8; 64 - 8],
88}
89
90#[repr(C, align(64))]
91pub struct SpscSlot {
92    pub payload: UnsafeCell<[u8; SPSC_PAYLOAD_BYTES]>,
93}
94
95unsafe impl Sync for SpscSlot {}
96
97/// Total file size for a ring of `capacity` payload slots, including
98/// the three-cache-line header.
99pub const fn spsc_ring_file_size(capacity: usize) -> usize {
100    std::mem::size_of::<SpscHeader>() + capacity * SPSC_SLOT_SIZE
101}
102
103/// Caller-owned memory a ring can be laid out in: huge / large pages,
104/// or any region. The ring takes ownership (keeping it mapped) and
105/// writes its header + slots into the region's bytes. Implemented for
106/// `HugepageRegion` (Linux), `LargePageRegion` / `LargePageSection`
107/// (Windows) - so a ring can sit on 2 MB / 1 GB pages and shed the TLB
108/// pressure of thousands of 4 KB pages, which matters once you have
109/// many rings or one very large one.
110/// The ring header is `align(64)`, so the region base must be
111/// 64-byte aligned. Page-backed regions (huge / large pages, mmap)
112/// satisfy this by construction; a hand-rolled region must align its
113/// buffer or the constructor returns
114/// [`RingError::LayoutMismatch`](crate::shared_ring::RingError).
115pub trait RegionOwner: Send + Sync + 'static {
116    /// Pointer to the start of the region (must be 64-byte aligned).
117    fn region_ptr(&mut self) -> *mut u8;
118    /// Region length in bytes (must be >= the ring's file size).
119    fn region_len(&self) -> usize;
120}
121
122/// Cache-line / header alignment every ring layout requires.
123const REGION_ALIGN: usize = 64;
124
125/// Backing-store discriminator for `SpscRingCore`. The variant
126/// holds the underlying memory owner so it stays alive for the
127/// lifetime of the ring; the raw byte access goes through
128/// `SpscRingCore::raw_ptr`. The held values are intentionally
129/// never read directly (lifetime extension only).
130#[allow(dead_code)]
131enum SpscBacking {
132    /// Anonymous in-process memory.
133    Anon(MmapMut),
134    /// File-backed (cross-process via page cache).
135    File(File, MmapMut),
136    /// Named RAM-resident shared memory (cross-process, no page cache).
137    Shm(crate::shm_file::ShmFile),
138    /// Caller-owned region (huge / large pages, or any `RegionOwner`).
139    Region(Box<dyn RegionOwner>),
140}
141
142/// Lamport SPSC ring core. Used as the storage backing
143/// [`SharedRingSpsc`](crate::SharedRingSpsc); applications normally
144/// reach for the typed `Producer` / `Consumer` halves rather than
145/// this raw core.
146pub struct SpscRingCore {
147    /// Owns the underlying memory; never accessed directly after
148    /// construction (raw_ptr captures the pointer once).
149    _backing: SpscBacking,
150    /// Stable byte pointer into the backing for the lifetime of self.
151    /// Header lives at byte 0; slots start at offset
152    /// `size_of::<SpscHeader>()`. Total mapped size is
153    /// `spsc_ring_file_size(capacity)`.
154    raw_ptr: *mut u8,
155    capacity: usize,
156}
157
158unsafe impl Send for SpscRingCore {}
159unsafe impl Sync for SpscRingCore {}
160
161fn init_spsc_layout(mmap: &mut MmapMut, capacity: usize) {
162    unsafe { init_spsc_layout_raw(mmap.as_mut_ptr(), capacity) };
163}
164
165/// Backing-agnostic layout init. Writes the header and zeroes the
166/// payload slots at the given raw pointer. Caller guarantees that
167/// `ptr` points to at least `spsc_ring_file_size(capacity)` bytes
168/// of mutable, suitably-aligned memory.
169unsafe fn init_spsc_layout_raw(ptr: *mut u8, capacity: usize) {
170    let header_ptr = ptr as *mut SpscHeader;
171    unsafe {
172        std::ptr::write(header_ptr, SpscHeader {
173            magic: SPSC_MAGIC,
174            capacity: capacity as u64,
175            slot_size: SPSC_SLOT_SIZE as u64,
176            _pad_meta: [0; 64 - 24],
177            head: AtomicU64::new(0),
178            _pad_head: [0; 64 - 8],
179            tail: AtomicU64::new(0),
180            _pad_tail: [0; 64 - 8],
181        });
182    }
183    let slots_base = unsafe { ptr.add(std::mem::size_of::<SpscHeader>()) };
184    for i in 0..capacity {
185        let slot_ptr = unsafe { slots_base.add(i * SPSC_SLOT_SIZE) as *mut SpscSlot };
186        unsafe {
187            std::ptr::write(slot_ptr, SpscSlot {
188                payload: UnsafeCell::new([0; SPSC_PAYLOAD_BYTES]),
189            });
190        }
191    }
192}
193
194impl SpscRingCore {
195    /// Anonymous in-memory ring (in-process only). Fastest construction;
196    /// skips file create + ftruncate + first-page-fault.
197    pub fn create_anon(capacity: usize) -> Result<Self, RingError> {
198        assert!(capacity.is_power_of_two() && capacity >= 2,
199                "capacity must be pow2 >= 2");
200        let total = spsc_ring_file_size(capacity);
201        let mut mmap = MmapOptions::new().len(total).map_anon()?;
202        init_spsc_layout(&mut mmap, capacity);
203        let raw_ptr = mmap.as_mut_ptr();
204        Ok(Self {
205            _backing: SpscBacking::Anon(mmap),
206            raw_ptr, capacity,
207        })
208    }
209
210    /// File-backed ring; cross-process visibility via the OS page cache.
211    pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
212        assert!(capacity.is_power_of_two() && capacity >= 2,
213                "capacity must be pow2 >= 2");
214        let total = spsc_ring_file_size(capacity);
215        let file = OpenOptions::new()
216            .read(true).write(true).create(true).truncate(true)
217            .open(path.as_ref())?;
218        file.set_len(total as u64)?;
219        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
220        init_spsc_layout(&mut mmap, capacity);
221        let raw_ptr = mmap.as_mut_ptr();
222        Ok(Self {
223            _backing: SpscBacking::File(file, mmap),
224            raw_ptr, capacity,
225        })
226    }
227
228    /// Open an existing file-backed ring. Validates magic + capacity.
229    pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, RingError> {
230        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
231        let total = spsc_ring_file_size(expected_capacity);
232        let actual_len = file.metadata()?.len();
233        if (actual_len as usize) < total {
234            return Err(RingError::LayoutMismatch);
235        }
236        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
237        let header = unsafe { &*(mmap.as_ptr() as *const SpscHeader) };
238        if header.magic != SPSC_MAGIC
239            || header.capacity != expected_capacity as u64
240            || header.slot_size != SPSC_SLOT_SIZE as u64
241        {
242            return Err(RingError::LayoutMismatch);
243        }
244        let raw_ptr = mmap.as_mut_ptr();
245        Ok(Self {
246            _backing: SpscBacking::File(file, mmap),
247            raw_ptr, capacity: expected_capacity,
248        })
249    }
250
251    /// Build a fresh ring on top of a named RAM-resident
252    /// shared-memory backing. Cross-process visible via the
253    /// `logical_name` of the underlying `ShmFile`; never touches the
254    /// page cache. The `ShmFile` must be sized to at least
255    /// `spsc_ring_file_size(capacity)` bytes.
256    pub fn create_from_shm(
257        mut shm: crate::shm_file::ShmFile,
258        capacity: usize,
259    ) -> Result<Self, RingError> {
260        assert!(capacity.is_power_of_two() && capacity >= 2,
261                "capacity must be pow2 >= 2");
262        let total = spsc_ring_file_size(capacity);
263        if shm.len() < total {
264            return Err(RingError::LayoutMismatch);
265        }
266        // Initialize the layout in the shared region.
267        let slice = shm.as_mut_slice();
268        let raw_ptr = slice.as_mut_ptr();
269        unsafe {
270            init_spsc_layout_raw(raw_ptr, capacity);
271        }
272        Ok(Self {
273            _backing: SpscBacking::Shm(shm),
274            raw_ptr, capacity,
275        })
276    }
277
278    /// Open an existing named ShmFs-backed ring. Validates magic +
279    /// capacity. Does NOT re-initialize the layout - the layout must
280    /// already be present from a prior `create_from_shm` on the same
281    /// logical name.
282    pub fn open_from_shm(
283        mut shm: crate::shm_file::ShmFile,
284        expected_capacity: usize,
285    ) -> Result<Self, RingError> {
286        let total = spsc_ring_file_size(expected_capacity);
287        if shm.len() < total {
288            return Err(RingError::LayoutMismatch);
289        }
290        let slice = shm.as_mut_slice();
291        let raw_ptr = slice.as_mut_ptr();
292        let header = unsafe { &*(raw_ptr as *const SpscHeader) };
293        if header.magic != SPSC_MAGIC
294            || header.capacity != expected_capacity as u64
295            || header.slot_size != SPSC_SLOT_SIZE as u64
296        {
297            return Err(RingError::LayoutMismatch);
298        }
299        Ok(Self {
300            _backing: SpscBacking::Shm(shm),
301            raw_ptr, capacity: expected_capacity,
302        })
303    }
304
305    /// Build a fresh ring laid out in caller-owned memory (huge / large
306    /// pages, or any [`RegionOwner`]). The region must be at least
307    /// `spsc_ring_file_size(capacity)` bytes; the ring owns it for its
308    /// lifetime so the pages stay mapped.
309    pub fn create_in_region<R: RegionOwner>(
310        mut region: R, capacity: usize,
311    ) -> Result<Self, RingError> {
312        assert!(capacity.is_power_of_two() && capacity >= 2,
313                "capacity must be pow2 >= 2");
314        if region.region_len() < spsc_ring_file_size(capacity) {
315            return Err(RingError::LayoutMismatch);
316        }
317        let raw_ptr = region.region_ptr();
318        if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
319            return Err(RingError::LayoutMismatch);
320        }
321        unsafe { init_spsc_layout_raw(raw_ptr, capacity) };
322        Ok(Self {
323            _backing: SpscBacking::Region(Box::new(region)),
324            raw_ptr, capacity,
325        })
326    }
327
328    /// Attach to an existing ring already laid out in `region` - e.g. a
329    /// `LargePageSection` another process created under the same name.
330    /// Validates the header and does NOT re-initialise.
331    pub fn open_in_region<R: RegionOwner>(
332        mut region: R, expected_capacity: usize,
333    ) -> Result<Self, RingError> {
334        if region.region_len() < spsc_ring_file_size(expected_capacity) {
335            return Err(RingError::LayoutMismatch);
336        }
337        let raw_ptr = region.region_ptr();
338        if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
339            return Err(RingError::LayoutMismatch);
340        }
341        let header = unsafe { &*(raw_ptr as *const SpscHeader) };
342        if header.magic != SPSC_MAGIC
343            || header.capacity != expected_capacity as u64
344            || header.slot_size != SPSC_SLOT_SIZE as u64
345        {
346            return Err(RingError::LayoutMismatch);
347        }
348        Ok(Self {
349            _backing: SpscBacking::Region(Box::new(region)),
350            raw_ptr, capacity: expected_capacity,
351        })
352    }
353
354    /// Capacity in slots (always a power of 2).
355    pub fn capacity(&self) -> usize { self.capacity }
356
357    fn header(&self) -> &SpscHeader {
358        unsafe { &*(self.raw_ptr as *const SpscHeader) }
359    }
360
361    fn slot(&self, idx: usize) -> &SpscSlot {
362        let slots_base = unsafe {
363            self.raw_ptr.add(std::mem::size_of::<SpscHeader>())
364        };
365        let masked = idx & (self.capacity - 1);
366        unsafe { &*(slots_base.add(masked * SPSC_SLOT_SIZE) as *const SpscSlot) }
367    }
368
369    /// Producer's published index. Cross-thread visible.
370    pub fn head(&self) -> u64 { self.header().head.load(Ordering::Acquire) }
371
372    /// Consumer's published index. Cross-thread visible.
373    pub fn tail(&self) -> u64 { self.header().tail.load(Ordering::Acquire) }
374
375    /// The producer's publish signal: the head counter the
376    /// consumer-side monitor-wait arms on. The producer's
377    /// Release-store to this atom on every push is the wake.
378    pub fn head_signal(&self) -> &AtomicU64 {
379        &self.header().head
380    }
381
382    /// Number of items waiting (`head - tail`).
383    pub fn approx_len(&self) -> usize {
384        let h = self.head();
385        let t = self.tail();
386        h.saturating_sub(t) as usize
387    }
388
389    /// SPSC push. **Caller is the sole producer** (enforced by the
390    /// `Producer` newtype that owns this ring via `Arc`). Lamport
391    /// pattern: read tail to check fullness, write payload, Release-
392    /// store head to publish.
393    pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
394        if payload.len() > SPSC_PAYLOAD_BYTES {
395            return Err(RingError::PayloadTooLarge);
396        }
397        let header = self.header();
398        let head = header.head.load(Ordering::Relaxed);
399        let tail = header.tail.load(Ordering::Acquire);
400        if head.wrapping_sub(tail) >= self.capacity as u64 {
401            return Err(RingError::Full);
402        }
403        let slot = self.slot(head as usize);
404        // Copy stays on `ptr::copy_nonoverlapping`: at one-line
405        // sizes the baseline inlined movups codegen beats the
406        // dispatched wide-register kernel by ~25% (the dispatch
407        // branch + call cost more than the lanes save; measured by
408        // examples/cacheline_probe.rs).
409        unsafe {
410            let dst = (*slot.payload.get()).as_mut_ptr();
411            std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
412            if payload.len() < SPSC_PAYLOAD_BYTES {
413                std::ptr::write_bytes(
414                    dst.add(payload.len()), 0,
415                    SPSC_PAYLOAD_BYTES - payload.len(),
416                );
417            }
418        }
419        header.head.store(head + 1, Ordering::Release);
420        // The slot line's next reader is the consumer core; demote
421        // it toward the shared LLC (NOP without CLDEMOTE support).
422        crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
423        Ok(())
424    }
425
426    /// SPSC pop. **Caller is the sole consumer.** Lamport pattern:
427    /// read head to check emptiness, read payload, Release-store tail
428    /// to free the slot.
429    pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
430        if out.len() < SPSC_PAYLOAD_BYTES {
431            return Err(RingError::PayloadTooLarge);
432        }
433        let header = self.header();
434        let tail = header.tail.load(Ordering::Relaxed);
435        let head = header.head.load(Ordering::Acquire);
436        if tail == head {
437            return Err(RingError::Empty);
438        }
439        let slot = self.slot(tail as usize);
440        unsafe {
441            let src = (*slot.payload.get()).as_ptr();
442            std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), SPSC_PAYLOAD_BYTES);
443        }
444        header.tail.store(tail + 1, Ordering::Release);
445        // The freed slot's next toucher is the producer core.
446        crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
447        Ok(SPSC_PAYLOAD_BYTES)
448    }
449
450    /// Peek the next slot WITHOUT copying or releasing it. Returns
451    /// a [`PeekedSlot`] guard that derefs to `&[u8]` pointing
452    /// directly into the mapped region. Caller passes this slice to
453    /// downstream consumers (e.g. quinn's `SendStream::write_all`)
454    /// without an intermediate copy. When done, call
455    /// [`PeekedSlot::confirm`] to advance the consumer position and
456    /// release the slot. Drop without confirming leaves the slot
457    /// in place; the next `peek_slot` returns it again.
458    ///
459    /// Returns `None` when the ring is empty. **Caller is the sole
460    /// consumer.**
461    pub fn peek_slot(&self) -> Option<PeekedSlot<'_>> {
462        let header = self.header();
463        let tail = header.tail.load(Ordering::Relaxed);
464        let head = header.head.load(Ordering::Acquire);
465        if tail == head {
466            return None;
467        }
468        let slot = self.slot(tail as usize);
469        let payload_ptr = unsafe { (*slot.payload.get()).as_ptr() };
470        let payload_slice = unsafe {
471            std::slice::from_raw_parts(payload_ptr, SPSC_PAYLOAD_BYTES)
472        };
473        Some(PeekedSlot {
474            ring: self,
475            tail,
476            payload: payload_slice,
477        })
478    }
479
480    /// Force any dirty MMF pages to disk. Only meaningful for the
481    /// file-backed mode; no-op on anonymous and ShmFs mappings
482    /// (which never touch disk).
483    pub fn flush(&self) -> Result<(), RingError> {
484        match &self._backing {
485            SpscBacking::File(_, mmap) => {
486                mmap.flush()?;
487            }
488            SpscBacking::Anon(_)
489            | SpscBacking::Shm(_)
490            | SpscBacking::Region(_) => {
491                // No disk to flush to (region-backed rings live in
492                // huge / large pages or other caller-owned RAM).
493            }
494        }
495        Ok(())
496    }
497}
498
499/// Zero-copy view into the next consumer slot of an [`SpscRingCore`].
500///
501/// Derefs to `&[u8]` pointing INTO the mapped region; pass that
502/// slice directly to downstream consumers (network egress, file
503/// writers) without an intermediate stack copy. Call
504/// [`PeekedSlot::confirm`] when done to release the slot;
505/// dropping without confirming leaves the slot in place.
506pub struct PeekedSlot<'a> {
507    ring: &'a SpscRingCore,
508    tail: u64,
509    payload: &'a [u8],
510}
511
512impl<'a> PeekedSlot<'a> {
513    /// The slot's payload bytes. Same as the `Deref` impl; explicit
514    /// method form for clarity at call sites.
515    pub fn as_slice(&self) -> &[u8] { self.payload }
516
517    /// Length of the payload region (always [`SPSC_PAYLOAD_BYTES`]).
518    pub fn len(&self) -> usize { self.payload.len() }
519
520    /// Whether the payload is empty (always false for a valid peek;
521    /// method exists for clippy's `len_without_is_empty`).
522    pub fn is_empty(&self) -> bool { self.payload.is_empty() }
523
524    /// Release the slot, advancing the consumer position.
525    pub fn confirm(self) {
526        let header = self.ring.header();
527        header.tail.store(self.tail + 1, Ordering::Release);
528    }
529}
530
531impl<'a> std::ops::Deref for PeekedSlot<'a> {
532    type Target = [u8];
533    fn deref(&self) -> &[u8] { self.payload }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use std::sync::Arc;
540    use std::thread;
541
542    #[test]
543    fn single_thread_round_trip() {
544        let ring = SpscRingCore::create_anon(8).unwrap();
545        let payload = [0xABu8; SPSC_PAYLOAD_BYTES];
546        ring.try_push(&payload).unwrap();
547        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
548        ring.try_pop(&mut out).unwrap();
549        assert_eq!(out, payload);
550        assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
551    }
552
553    #[test]
554    fn shm_round_trip() {
555        use crate::shm_file::ShmFile;
556        let nonce = std::time::SystemTime::now()
557            .duration_since(std::time::UNIX_EPOCH)
558            .map(|d| d.as_nanos())
559            .unwrap_or(0);
560        let name = format!("spsc_shm_rt_{}_{}", std::process::id(), nonce);
561        let capacity = 8;
562        let size = spsc_ring_file_size(capacity);
563        let shm = ShmFile::create_or_open_named(&name, size)
564            .expect("shm create");
565        let ring = SpscRingCore::create_from_shm(shm, capacity).unwrap();
566
567        let payload = [0xCDu8; SPSC_PAYLOAD_BYTES];
568        ring.try_push(&payload).unwrap();
569        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
570        ring.try_pop(&mut out).unwrap();
571        assert_eq!(out, payload);
572    }
573
574    #[test]
575    fn shm_cross_handle_visibility() {
576        use crate::shm_file::ShmFile;
577        let nonce = std::time::SystemTime::now()
578            .duration_since(std::time::UNIX_EPOCH)
579            .map(|d| d.as_nanos())
580            .unwrap_or(0);
581        let name = format!("spsc_shm_xshare_{}_{}", std::process::id(), nonce);
582        let capacity = 8;
583        let size = spsc_ring_file_size(capacity);
584
585        // Producer side: create the ring (initialises layout).
586        let shm_a = ShmFile::create_or_open_named(&name, size).expect("shm A");
587        let producer_ring = SpscRingCore::create_from_shm(shm_a, capacity).unwrap();
588
589        // Consumer side: open the SAME named region; layout already
590        // initialised so use open_from_shm.
591        let shm_b = ShmFile::create_or_open_named(&name, size).expect("shm B");
592        let consumer_ring = SpscRingCore::open_from_shm(shm_b, capacity).unwrap();
593
594        // Push via A, pop via B - cross-handle visibility through
595        // the shared RAM region.
596        let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
597        producer_ring.try_push(&payload).unwrap();
598        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
599        consumer_ring.try_pop(&mut out).unwrap();
600        assert_eq!(out, payload);
601    }
602
603    #[test]
604    fn fills_to_capacity_then_full() {
605        let ring = SpscRingCore::create_anon(4).unwrap();
606        for i in 0..4u8 {
607            ring.try_push(&[i; SPSC_PAYLOAD_BYTES]).unwrap();
608        }
609        assert_eq!(
610            ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap_err(),
611            RingError::Full,
612        );
613        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
614        ring.try_pop(&mut out).unwrap();
615        ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap();
616    }
617
618    #[test]
619    fn two_thread_high_volume_round_trip() {
620        let ring = Arc::new(SpscRingCore::create_anon(64).unwrap());
621        let ring_p = ring.clone();
622        let ring_c = ring.clone();
623        const N: u32 = 100_000;
624
625        let producer = thread::spawn(move || {
626            for i in 0..N {
627                let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
628                buf[..4].copy_from_slice(&i.to_le_bytes());
629                while ring_p.try_push(&buf).is_err() {
630                    std::hint::spin_loop();
631                }
632            }
633        });
634
635        let consumer = thread::spawn(move || {
636            let mut out = [0u8; SPSC_PAYLOAD_BYTES];
637            let mut sum: u64 = 0;
638            let mut received: u32 = 0;
639            while received < N {
640                if ring_c.try_pop(&mut out).is_ok() {
641                    sum += u32::from_le_bytes(out[..4].try_into().unwrap()) as u64;
642                    received += 1;
643                } else {
644                    std::hint::spin_loop();
645                }
646            }
647            sum
648        });
649
650        producer.join().unwrap();
651        let sum = consumer.join().unwrap();
652        let expected: u64 = (0..N).map(u64::from).sum();
653        assert_eq!(sum, expected);
654    }
655
656    #[test]
657    fn peek_drop_without_confirm_leaves_item_in_place() {
658        let ring = SpscRingCore::create_anon(8).unwrap();
659        let payload = [0x5Au8; SPSC_PAYLOAD_BYTES];
660        ring.try_push(&payload).unwrap();
661
662        // Peek and drop WITHOUT confirming: the slot must stay.
663        {
664            let peek = ring.peek_slot().unwrap();
665            assert_eq!(peek.as_slice(), &payload[..]);
666        }
667        assert_eq!(ring.approx_len(), 1,
668                   "dropping a peek must not consume the slot");
669
670        // The next peek returns the same item; confirming releases it.
671        let peek = ring.peek_slot().unwrap();
672        assert_eq!(peek.as_slice(), &payload[..]);
673        peek.confirm();
674        assert!(ring.peek_slot().is_none());
675        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
676        assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
677    }
678
679    /// A 64-byte-aligned heap region exercises the `RegionOwner` wiring
680    /// with no huge-page privilege needed. The element type forces the
681    /// Vec's buffer onto a cache-line boundary, matching what page-
682    /// backed regions give for free. The huge-page (Linux) and large-
683    /// page (Windows) backings travel the exact same `create_in_region`
684    /// path, proven end to end in `examples/large_page_ring.rs`.
685    #[repr(C, align(64))]
686    #[derive(Clone, Copy)]
687    struct Block64([u8; 64]);
688
689    struct HeapRegion {
690        blocks: Vec<Block64>,
691    }
692    impl HeapRegion {
693        fn new(bytes: usize) -> Self {
694            Self { blocks: vec![Block64([0u8; 64]); bytes.div_ceil(64)] }
695        }
696    }
697    impl RegionOwner for HeapRegion {
698        fn region_ptr(&mut self) -> *mut u8 {
699            self.blocks.as_mut_ptr() as *mut u8
700        }
701        fn region_len(&self) -> usize { self.blocks.len() * 64 }
702    }
703
704    #[test]
705    fn create_in_region_round_trips() {
706        let cap = 16usize;
707        let region = HeapRegion::new(spsc_ring_file_size(cap));
708        let ring = SpscRingCore::create_in_region(region, cap).unwrap();
709        assert_eq!(ring.capacity(), cap);
710
711        // Three full laps, so head/tail wrap past capacity and the
712        // region-laid-out slots are reused in place.
713        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
714        for round in 0..3u64 {
715            for i in 0..cap as u64 {
716                let v = round * cap as u64 + i;
717                let mut p = [0u8; SPSC_PAYLOAD_BYTES];
718                p[..8].copy_from_slice(&v.to_le_bytes());
719                ring.try_push(&p).unwrap();
720            }
721            for i in 0..cap as u64 {
722                ring.try_pop(&mut out).unwrap();
723                let got = u64::from_le_bytes(out[..8].try_into().unwrap());
724                assert_eq!(got, round * cap as u64 + i);
725            }
726        }
727    }
728
729    #[test]
730    fn create_in_region_rejects_short_region() {
731        // One cache line short of the layout the ring needs.
732        let cap = 16usize;
733        let short = spsc_ring_file_size(cap) - 64;
734        let region = HeapRegion::new(short);
735        assert!(region.region_len() < spsc_ring_file_size(cap));
736        // SpscRingCore is not Debug, so match rather than unwrap_err.
737        assert!(matches!(
738            SpscRingCore::create_in_region(region, cap),
739            Err(RingError::LayoutMismatch),
740        ));
741    }
742
743    #[test]
744    fn open_in_region_attaches_to_initialised_layout() {
745        // Lay a ring out in a region, push an item, then attach a second
746        // handle to the SAME bytes via open_in_region (no re-init) and
747        // drain through it - the cross-process LargePageSection path in
748        // miniature, with a heap region standing in for the section.
749        let cap = 8usize;
750        let bytes = spsc_ring_file_size(cap);
751        // 64-byte-aligned backing both views map (a named section in
752        // miniature; two processes would each hold their own view).
753        let mut whole: Vec<Block64> = vec![Block64([0u8; 64]); bytes.div_ceil(64)];
754        let base = whole.as_mut_ptr() as *mut u8;
755        unsafe { init_spsc_layout_raw(base, cap) };
756
757        // Two non-owning views over the same bytes (this is what two
758        // processes mapping one named section would each hold).
759        struct ViewRegion { ptr: *mut u8, len: usize }
760        unsafe impl Send for ViewRegion {}
761        unsafe impl Sync for ViewRegion {}
762        impl RegionOwner for ViewRegion {
763            fn region_ptr(&mut self) -> *mut u8 { self.ptr }
764            fn region_len(&self) -> usize { self.len }
765        }
766
767        let producer = SpscRingCore::open_in_region(
768            ViewRegion { ptr: base, len: bytes }, cap,
769        ).unwrap();
770        let consumer = SpscRingCore::open_in_region(
771            ViewRegion { ptr: base, len: bytes }, cap,
772        ).unwrap();
773
774        let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
775        producer.try_push(&payload).unwrap();
776        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
777        consumer.try_pop(&mut out).unwrap();
778        assert_eq!(out, payload);
779        // `whole` is declared before the views, so scope order drops it
780        // LAST - the backing bytes outlive both ring handles.
781    }
782
783    #[test]
784    fn open_round_trips_with_file() {
785        let p = std::env::temp_dir().join(format!(
786            "subetha-test-spsc-{}.bin", std::process::id(),
787        ));
788        std::fs::remove_file(&p).ok();
789        {
790            let _r = SpscRingCore::create(&p, 16).unwrap();
791        }
792        let r2 = SpscRingCore::open(&p, 16).unwrap();
793        assert_eq!(r2.capacity(), 16);
794        std::fs::remove_file(&p).ok();
795    }
796}