1use 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
60pub const SPSC_MAGIC: u64 = 0x5350_5343_0000_0001;
63
64pub const SPSC_SLOT_SIZE: usize = 64;
67
68pub const SPSC_PAYLOAD_BYTES: usize = SPSC_SLOT_SIZE;
70
71#[repr(C, align(64))]
76pub struct SpscHeader {
77 pub magic: u64,
78 pub capacity: u64,
79 pub slot_size: u64,
80 _pad_meta: [u8; 64 - 24],
82 pub head: AtomicU64,
84 _pad_head: [u8; 64 - 8],
85 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
97pub const fn spsc_ring_file_size(capacity: usize) -> usize {
100 std::mem::size_of::<SpscHeader>() + capacity * SPSC_SLOT_SIZE
101}
102
103pub trait RegionOwner: Send + Sync + 'static {
116 fn region_ptr(&mut self) -> *mut u8;
118 fn region_len(&self) -> usize;
120}
121
122const REGION_ALIGN: usize = 64;
124
125#[allow(dead_code)]
131enum SpscBacking {
132 Anon(MmapMut),
134 File(File, MmapMut),
136 Shm(crate::shm_file::ShmFile),
138 Region(Box<dyn RegionOwner>),
140}
141
142pub struct SpscRingCore {
147 _backing: SpscBacking,
150 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
165unsafe 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 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 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 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 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 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 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 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 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 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 pub fn head(&self) -> u64 { self.header().head.load(Ordering::Acquire) }
371
372 pub fn tail(&self) -> u64 { self.header().tail.load(Ordering::Acquire) }
374
375 pub fn head_signal(&self) -> &AtomicU64 {
379 &self.header().head
380 }
381
382 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 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 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 crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
423 Ok(())
424 }
425
426 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 crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
447 Ok(SPSC_PAYLOAD_BYTES)
448 }
449
450 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 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 }
494 }
495 Ok(())
496 }
497}
498
499pub struct PeekedSlot<'a> {
507 ring: &'a SpscRingCore,
508 tail: u64,
509 payload: &'a [u8],
510}
511
512impl<'a> PeekedSlot<'a> {
513 pub fn as_slice(&self) -> &[u8] { self.payload }
516
517 pub fn len(&self) -> usize { self.payload.len() }
519
520 pub fn is_empty(&self) -> bool { self.payload.is_empty() }
523
524 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 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 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 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 {
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 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 #[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 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 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 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 let cap = 8usize;
750 let bytes = spsc_ring_file_size(cap);
751 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 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 }
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}