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) {
172 unsafe {
173 std::ptr::write_bytes(ptr, 0, spsc_ring_file_size(capacity));
174 let header_ptr = ptr as *mut SpscHeader;
175 (*header_ptr).capacity = capacity as u64;
176 (*header_ptr).slot_size = SPSC_SLOT_SIZE as u64;
177 std::ptr::write_volatile(&raw mut (*header_ptr).magic, SPSC_MAGIC);
178 }
179}
180
181impl SpscRingCore {
182 pub fn create_anon(capacity: usize) -> Result<Self, RingError> {
185 assert!(capacity.is_power_of_two() && capacity >= 2,
186 "capacity must be pow2 >= 2");
187 let total = spsc_ring_file_size(capacity);
188 let mut mmap = MmapOptions::new().len(total).map_anon()?;
189 init_spsc_layout(&mut mmap, capacity);
190 let raw_ptr = mmap.as_mut_ptr();
191 Ok(Self {
192 _backing: SpscBacking::Anon(mmap),
193 raw_ptr, capacity,
194 })
195 }
196
197 pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
204 assert!(capacity.is_power_of_two() && capacity >= 2,
205 "capacity must be pow2 >= 2");
206 let total = spsc_ring_file_size(capacity);
207 let (file, mut mmap) = crate::mmf_attach::create_or_attach(
208 path.as_ref(),
209 total,
210 |ptr| unsafe { init_spsc_layout_raw(ptr, capacity) },
211 |ptr| unsafe { (*(ptr as *const SpscHeader)).magic == SPSC_MAGIC },
212 )?;
213 let header = unsafe { &*(mmap.as_ptr() as *const SpscHeader) };
214 if header.magic != SPSC_MAGIC
215 || header.capacity != capacity as u64
216 || header.slot_size != SPSC_SLOT_SIZE as u64
217 {
218 return Err(RingError::LayoutMismatch);
219 }
220 let raw_ptr = mmap.as_mut_ptr();
221 Ok(Self {
222 _backing: SpscBacking::File(file, mmap),
223 raw_ptr, capacity,
224 })
225 }
226
227 pub fn reset(path: impl AsRef<Path>, capacity: usize) -> Result<Self, RingError> {
231 assert!(capacity.is_power_of_two() && capacity >= 2,
232 "capacity must be pow2 >= 2");
233 let total = spsc_ring_file_size(capacity);
234 let (file, mut mmap) = crate::mmf_attach::reset(
235 path.as_ref(),
236 total,
237 |ptr| unsafe { init_spsc_layout_raw(ptr, capacity) },
238 )?;
239 let raw_ptr = mmap.as_mut_ptr();
240 Ok(Self {
241 _backing: SpscBacking::File(file, mmap),
242 raw_ptr, capacity,
243 })
244 }
245
246 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, RingError> {
248 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
249 let total = spsc_ring_file_size(expected_capacity);
250 let actual_len = file.metadata()?.len();
251 if (actual_len as usize) < total {
252 return Err(RingError::LayoutMismatch);
253 }
254 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
255 let header = unsafe { &*(mmap.as_ptr() as *const SpscHeader) };
256 if header.magic != SPSC_MAGIC
257 || header.capacity != expected_capacity as u64
258 || header.slot_size != SPSC_SLOT_SIZE as u64
259 {
260 return Err(RingError::LayoutMismatch);
261 }
262 let raw_ptr = mmap.as_mut_ptr();
263 Ok(Self {
264 _backing: SpscBacking::File(file, mmap),
265 raw_ptr, capacity: expected_capacity,
266 })
267 }
268
269 pub fn create_from_shm(
275 mut shm: crate::shm_file::ShmFile,
276 capacity: usize,
277 ) -> Result<Self, RingError> {
278 assert!(capacity.is_power_of_two() && capacity >= 2,
279 "capacity must be pow2 >= 2");
280 let total = spsc_ring_file_size(capacity);
281 if shm.len() < total {
282 return Err(RingError::LayoutMismatch);
283 }
284 let slice = shm.as_mut_slice();
286 let raw_ptr = slice.as_mut_ptr();
287 unsafe {
288 init_spsc_layout_raw(raw_ptr, capacity);
289 }
290 Ok(Self {
291 _backing: SpscBacking::Shm(shm),
292 raw_ptr, capacity,
293 })
294 }
295
296 pub fn open_from_shm(
301 mut shm: crate::shm_file::ShmFile,
302 expected_capacity: usize,
303 ) -> Result<Self, RingError> {
304 let total = spsc_ring_file_size(expected_capacity);
305 if shm.len() < total {
306 return Err(RingError::LayoutMismatch);
307 }
308 let slice = shm.as_mut_slice();
309 let raw_ptr = slice.as_mut_ptr();
310 let header = unsafe { &*(raw_ptr as *const SpscHeader) };
311 if header.magic != SPSC_MAGIC
312 || header.capacity != expected_capacity as u64
313 || header.slot_size != SPSC_SLOT_SIZE as u64
314 {
315 return Err(RingError::LayoutMismatch);
316 }
317 Ok(Self {
318 _backing: SpscBacking::Shm(shm),
319 raw_ptr, capacity: expected_capacity,
320 })
321 }
322
323 pub fn create_in_region<R: RegionOwner>(
328 mut region: R, capacity: usize,
329 ) -> Result<Self, RingError> {
330 assert!(capacity.is_power_of_two() && capacity >= 2,
331 "capacity must be pow2 >= 2");
332 if region.region_len() < spsc_ring_file_size(capacity) {
333 return Err(RingError::LayoutMismatch);
334 }
335 let raw_ptr = region.region_ptr();
336 if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
337 return Err(RingError::LayoutMismatch);
338 }
339 unsafe { init_spsc_layout_raw(raw_ptr, capacity) };
340 Ok(Self {
341 _backing: SpscBacking::Region(Box::new(region)),
342 raw_ptr, capacity,
343 })
344 }
345
346 pub fn open_in_region<R: RegionOwner>(
350 mut region: R, expected_capacity: usize,
351 ) -> Result<Self, RingError> {
352 if region.region_len() < spsc_ring_file_size(expected_capacity) {
353 return Err(RingError::LayoutMismatch);
354 }
355 let raw_ptr = region.region_ptr();
356 if !(raw_ptr as usize).is_multiple_of(REGION_ALIGN) {
357 return Err(RingError::LayoutMismatch);
358 }
359 let header = unsafe { &*(raw_ptr as *const SpscHeader) };
360 if header.magic != SPSC_MAGIC
361 || header.capacity != expected_capacity as u64
362 || header.slot_size != SPSC_SLOT_SIZE as u64
363 {
364 return Err(RingError::LayoutMismatch);
365 }
366 Ok(Self {
367 _backing: SpscBacking::Region(Box::new(region)),
368 raw_ptr, capacity: expected_capacity,
369 })
370 }
371
372 pub fn capacity(&self) -> usize { self.capacity }
374
375 fn header(&self) -> &SpscHeader {
376 unsafe { &*(self.raw_ptr as *const SpscHeader) }
377 }
378
379 fn slot(&self, idx: usize) -> &SpscSlot {
380 let slots_base = unsafe {
381 self.raw_ptr.add(std::mem::size_of::<SpscHeader>())
382 };
383 let masked = idx & (self.capacity - 1);
384 unsafe { &*(slots_base.add(masked * SPSC_SLOT_SIZE) as *const SpscSlot) }
385 }
386
387 pub fn head(&self) -> u64 { self.header().head.load(Ordering::Acquire) }
389
390 pub fn tail(&self) -> u64 { self.header().tail.load(Ordering::Acquire) }
392
393 pub fn head_signal(&self) -> &AtomicU64 {
397 &self.header().head
398 }
399
400 pub fn approx_len(&self) -> usize {
402 let h = self.head();
403 let t = self.tail();
404 h.saturating_sub(t) as usize
405 }
406
407 pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
412 if payload.len() > SPSC_PAYLOAD_BYTES {
413 return Err(RingError::PayloadTooLarge);
414 }
415 let header = self.header();
416 let head = header.head.load(Ordering::Relaxed);
417 let tail = header.tail.load(Ordering::Acquire);
418 if head.wrapping_sub(tail) >= self.capacity as u64 {
419 return Err(RingError::Full);
420 }
421 let slot = self.slot(head as usize);
422 unsafe {
428 let dst = (*slot.payload.get()).as_mut_ptr();
429 std::ptr::copy_nonoverlapping(payload.as_ptr(), dst, payload.len());
430 if payload.len() < SPSC_PAYLOAD_BYTES {
431 std::ptr::write_bytes(
432 dst.add(payload.len()), 0,
433 SPSC_PAYLOAD_BYTES - payload.len(),
434 );
435 }
436 }
437 header.head.store(head + 1, Ordering::Release);
438 crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
441 Ok(())
442 }
443
444 pub fn try_pop(&self, out: &mut [u8]) -> Result<usize, RingError> {
448 if out.len() < SPSC_PAYLOAD_BYTES {
449 return Err(RingError::PayloadTooLarge);
450 }
451 let header = self.header();
452 let tail = header.tail.load(Ordering::Relaxed);
453 let head = header.head.load(Ordering::Acquire);
454 if tail == head {
455 return Err(RingError::Empty);
456 }
457 let slot = self.slot(tail as usize);
458 unsafe {
459 let src = (*slot.payload.get()).as_ptr();
460 std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), SPSC_PAYLOAD_BYTES);
461 }
462 header.tail.store(tail + 1, Ordering::Release);
463 crate::cache_ops::cldemote(slot as *const SpscSlot as *const u8);
465 Ok(SPSC_PAYLOAD_BYTES)
466 }
467
468 pub fn peek_slot(&self) -> Option<PeekedSlot<'_>> {
480 let header = self.header();
481 let tail = header.tail.load(Ordering::Relaxed);
482 let head = header.head.load(Ordering::Acquire);
483 if tail == head {
484 return None;
485 }
486 let slot = self.slot(tail as usize);
487 let payload_ptr = unsafe { (*slot.payload.get()).as_ptr() };
488 let payload_slice = unsafe {
489 std::slice::from_raw_parts(payload_ptr, SPSC_PAYLOAD_BYTES)
490 };
491 Some(PeekedSlot {
492 ring: self,
493 tail,
494 payload: payload_slice,
495 })
496 }
497
498 pub fn flush(&self) -> Result<(), RingError> {
502 match &self._backing {
503 SpscBacking::File(_, mmap) => {
504 mmap.flush()?;
505 }
506 SpscBacking::Anon(_)
507 | SpscBacking::Shm(_)
508 | SpscBacking::Region(_) => {
509 }
512 }
513 Ok(())
514 }
515}
516
517pub struct PeekedSlot<'a> {
525 ring: &'a SpscRingCore,
526 tail: u64,
527 payload: &'a [u8],
528}
529
530impl<'a> PeekedSlot<'a> {
531 pub fn as_slice(&self) -> &[u8] { self.payload }
534
535 pub fn len(&self) -> usize { self.payload.len() }
537
538 pub fn is_empty(&self) -> bool { self.payload.is_empty() }
541
542 pub fn confirm(self) {
544 let header = self.ring.header();
545 header.tail.store(self.tail + 1, Ordering::Release);
546 }
547}
548
549impl<'a> std::ops::Deref for PeekedSlot<'a> {
550 type Target = [u8];
551 fn deref(&self) -> &[u8] { self.payload }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use std::sync::Arc;
558 use std::thread;
559
560 #[test]
561 fn single_thread_round_trip() {
562 let ring = SpscRingCore::create_anon(8).unwrap();
563 let payload = [0xABu8; SPSC_PAYLOAD_BYTES];
564 ring.try_push(&payload).unwrap();
565 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
566 ring.try_pop(&mut out).unwrap();
567 assert_eq!(out, payload);
568 assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
569 }
570
571 #[test]
574 fn second_create_attaches_and_keeps_items() {
575 let p = std::env::temp_dir().join(format!(
576 "subetha-spsc-attach-{}.bin", std::process::id(),
577 ));
578 std::fs::remove_file(&p).ok();
579
580 let ring = SpscRingCore::create(&p, 8).unwrap();
581 let payload = [0x5Eu8; SPSC_PAYLOAD_BYTES];
582 ring.try_push(&payload).unwrap();
583
584 let ring2 = SpscRingCore::create(&p, 8).unwrap();
585 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
586 ring2.try_pop(&mut out).unwrap();
587 assert_eq!(out, payload, "attach lost a queued item");
588 assert!(matches!(
589 SpscRingCore::create(&p, 4),
590 Err(RingError::LayoutMismatch),
591 ));
592
593 drop(ring);
596 drop(ring2);
597 let fresh = SpscRingCore::reset(&p, 8).unwrap();
598 assert_eq!(fresh.try_pop(&mut out).unwrap_err(), RingError::Empty,
599 "reset kept a queued item");
600 drop(fresh);
601 std::fs::remove_file(&p).ok();
602 }
603
604 #[test]
605 fn shm_round_trip() {
606 use crate::shm_file::ShmFile;
607 let nonce = std::time::SystemTime::now()
608 .duration_since(std::time::UNIX_EPOCH)
609 .map(|d| d.as_nanos())
610 .unwrap_or(0);
611 let name = format!("spsc_shm_rt_{}_{}", std::process::id(), nonce);
612 let capacity = 8;
613 let size = spsc_ring_file_size(capacity);
614 let shm = ShmFile::create_or_open_named(&name, size)
615 .expect("shm create");
616 let ring = SpscRingCore::create_from_shm(shm, capacity).unwrap();
617
618 let payload = [0xCDu8; SPSC_PAYLOAD_BYTES];
619 ring.try_push(&payload).unwrap();
620 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
621 ring.try_pop(&mut out).unwrap();
622 assert_eq!(out, payload);
623 }
624
625 #[test]
626 fn shm_cross_handle_visibility() {
627 use crate::shm_file::ShmFile;
628 let nonce = std::time::SystemTime::now()
629 .duration_since(std::time::UNIX_EPOCH)
630 .map(|d| d.as_nanos())
631 .unwrap_or(0);
632 let name = format!("spsc_shm_xshare_{}_{}", std::process::id(), nonce);
633 let capacity = 8;
634 let size = spsc_ring_file_size(capacity);
635
636 let shm_a = ShmFile::create_or_open_named(&name, size).expect("shm A");
638 let producer_ring = SpscRingCore::create_from_shm(shm_a, capacity).unwrap();
639
640 let shm_b = ShmFile::create_or_open_named(&name, size).expect("shm B");
643 let consumer_ring = SpscRingCore::open_from_shm(shm_b, capacity).unwrap();
644
645 let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
648 producer_ring.try_push(&payload).unwrap();
649 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
650 consumer_ring.try_pop(&mut out).unwrap();
651 assert_eq!(out, payload);
652 }
653
654 #[test]
655 fn fills_to_capacity_then_full() {
656 let ring = SpscRingCore::create_anon(4).unwrap();
657 for i in 0..4u8 {
658 ring.try_push(&[i; SPSC_PAYLOAD_BYTES]).unwrap();
659 }
660 assert_eq!(
661 ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap_err(),
662 RingError::Full,
663 );
664 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
665 ring.try_pop(&mut out).unwrap();
666 ring.try_push(&[99u8; SPSC_PAYLOAD_BYTES]).unwrap();
667 }
668
669 #[test]
670 fn two_thread_high_volume_round_trip() {
671 let ring = Arc::new(SpscRingCore::create_anon(64).unwrap());
672 let ring_p = ring.clone();
673 let ring_c = ring.clone();
674 const N: u32 = 100_000;
675
676 let producer = thread::spawn(move || {
677 for i in 0..N {
678 let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
679 buf[..4].copy_from_slice(&i.to_le_bytes());
680 while ring_p.try_push(&buf).is_err() {
681 std::hint::spin_loop();
682 }
683 }
684 });
685
686 let consumer = thread::spawn(move || {
687 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
688 let mut sum: u64 = 0;
689 let mut received: u32 = 0;
690 while received < N {
691 if ring_c.try_pop(&mut out).is_ok() {
692 sum += u32::from_le_bytes(out[..4].try_into().unwrap()) as u64;
693 received += 1;
694 } else {
695 std::hint::spin_loop();
696 }
697 }
698 sum
699 });
700
701 producer.join().unwrap();
702 let sum = consumer.join().unwrap();
703 let expected: u64 = (0..N).map(u64::from).sum();
704 assert_eq!(sum, expected);
705 }
706
707 #[test]
708 fn peek_drop_without_confirm_leaves_item_in_place() {
709 let ring = SpscRingCore::create_anon(8).unwrap();
710 let payload = [0x5Au8; SPSC_PAYLOAD_BYTES];
711 ring.try_push(&payload).unwrap();
712
713 {
715 let peek = ring.peek_slot().unwrap();
716 assert_eq!(peek.as_slice(), &payload[..]);
717 }
718 assert_eq!(ring.approx_len(), 1,
719 "dropping a peek must not consume the slot");
720
721 let peek = ring.peek_slot().unwrap();
723 assert_eq!(peek.as_slice(), &payload[..]);
724 peek.confirm();
725 assert!(ring.peek_slot().is_none());
726 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
727 assert_eq!(ring.try_pop(&mut out).unwrap_err(), RingError::Empty);
728 }
729
730 #[repr(C, align(64))]
737 #[derive(Clone, Copy)]
738 struct Block64([u8; 64]);
739
740 struct HeapRegion {
741 blocks: Vec<Block64>,
742 }
743 impl HeapRegion {
744 fn new(bytes: usize) -> Self {
745 Self { blocks: vec![Block64([0u8; 64]); bytes.div_ceil(64)] }
746 }
747 }
748 impl RegionOwner for HeapRegion {
749 fn region_ptr(&mut self) -> *mut u8 {
750 self.blocks.as_mut_ptr() as *mut u8
751 }
752 fn region_len(&self) -> usize { self.blocks.len() * 64 }
753 }
754
755 #[test]
756 fn create_in_region_round_trips() {
757 let cap = 16usize;
758 let region = HeapRegion::new(spsc_ring_file_size(cap));
759 let ring = SpscRingCore::create_in_region(region, cap).unwrap();
760 assert_eq!(ring.capacity(), cap);
761
762 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
765 for round in 0..3u64 {
766 for i in 0..cap as u64 {
767 let v = round * cap as u64 + i;
768 let mut p = [0u8; SPSC_PAYLOAD_BYTES];
769 p[..8].copy_from_slice(&v.to_le_bytes());
770 ring.try_push(&p).unwrap();
771 }
772 for i in 0..cap as u64 {
773 ring.try_pop(&mut out).unwrap();
774 let got = u64::from_le_bytes(out[..8].try_into().unwrap());
775 assert_eq!(got, round * cap as u64 + i);
776 }
777 }
778 }
779
780 #[test]
781 fn create_in_region_rejects_short_region() {
782 let cap = 16usize;
784 let short = spsc_ring_file_size(cap) - 64;
785 let region = HeapRegion::new(short);
786 assert!(region.region_len() < spsc_ring_file_size(cap));
787 assert!(matches!(
789 SpscRingCore::create_in_region(region, cap),
790 Err(RingError::LayoutMismatch),
791 ));
792 }
793
794 #[test]
795 fn open_in_region_attaches_to_initialised_layout() {
796 let cap = 8usize;
801 let bytes = spsc_ring_file_size(cap);
802 let mut whole: Vec<Block64> = vec![Block64([0u8; 64]); bytes.div_ceil(64)];
805 let base = whole.as_mut_ptr() as *mut u8;
806 unsafe { init_spsc_layout_raw(base, cap) };
807
808 struct ViewRegion { ptr: *mut u8, len: usize }
811 unsafe impl Send for ViewRegion {}
812 unsafe impl Sync for ViewRegion {}
813 impl RegionOwner for ViewRegion {
814 fn region_ptr(&mut self) -> *mut u8 { self.ptr }
815 fn region_len(&self) -> usize { self.len }
816 }
817
818 let producer = SpscRingCore::open_in_region(
819 ViewRegion { ptr: base, len: bytes }, cap,
820 ).unwrap();
821 let consumer = SpscRingCore::open_in_region(
822 ViewRegion { ptr: base, len: bytes }, cap,
823 ).unwrap();
824
825 let payload = [0x42u8; SPSC_PAYLOAD_BYTES];
826 producer.try_push(&payload).unwrap();
827 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
828 consumer.try_pop(&mut out).unwrap();
829 assert_eq!(out, payload);
830 }
833
834 #[test]
835 fn open_round_trips_with_file() {
836 let p = std::env::temp_dir().join(format!(
837 "subetha-test-spsc-{}.bin", std::process::id(),
838 ));
839 std::fs::remove_file(&p).ok();
840 {
841 let _r = SpscRingCore::create(&p, 16).unwrap();
842 }
843 let r2 = SpscRingCore::open(&p, 16).unwrap();
844 assert_eq!(r2.capacity(), 16);
845 std::fs::remove_file(&p).ok();
846 }
847}