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 FRAME_MAGIC: u64 = 0x4652_4d52_0000_0001;
62
63pub const DESC_HEADER_BYTES: usize = 8;
66
67pub const MIN_SLOT_SIZE: usize = DESC_HEADER_BYTES + 8;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[repr(u8)]
74pub enum FrameClass {
75 Inline = 0,
77 Offset = 1,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum LayoutHint {
85 #[default]
87 Auto,
88 ForceInline,
91 ForceOffset,
93}
94
95#[repr(C, align(64))]
98struct FrameHeader {
99 magic: u64,
100 capacity: u64,
101 slot_size: u64,
102 region_bytes: u64,
103 inline_budget: u64,
104 _pad_meta: [u8; 64 - 40],
105 desc_head: AtomicU64,
107 _pad_dh: [u8; 64 - 8],
108 desc_tail: AtomicU64,
110 _pad_dt: [u8; 64 - 8],
111 region_head: AtomicU64,
113 _pad_rh: [u8; 64 - 8],
114 region_tail: AtomicU64,
116 _pad_rt: [u8; 64 - 8],
117}
118
119pub const fn frame_ring_file_size(
122 capacity: usize, slot_size: usize, region_bytes: usize,
123) -> usize {
124 std::mem::size_of::<FrameHeader>() + capacity * slot_size + region_bytes
125}
126
127#[allow(dead_code)]
129struct FrameCell(UnsafeCell<u8>);
130
131#[allow(dead_code)]
132enum FrameBacking {
133 Anon(MmapMut),
134 File(File, MmapMut),
135 Shm(crate::shm_file::ShmFile),
136}
137
138pub struct FrameRing {
143 _backing: FrameBacking,
144 raw_ptr: *mut u8,
145 capacity: usize,
146 slot_size: usize,
147 region_bytes: usize,
148 inline_budget: usize,
149 desc_base: usize,
150 region_base: usize,
151}
152
153unsafe impl Send for FrameRing {}
154unsafe impl Sync for FrameRing {}
155
156fn validate_params(capacity: usize, slot_size: usize, region_bytes: usize)
157 -> Result<(), RingError>
158{
159 if !capacity.is_power_of_two() || capacity < 2 {
160 return Err(RingError::LayoutMismatch);
161 }
162 if slot_size < MIN_SLOT_SIZE {
163 return Err(RingError::LayoutMismatch);
164 }
165 if !region_bytes.is_power_of_two() || region_bytes < 2 {
166 return Err(RingError::LayoutMismatch);
167 }
168 Ok(())
169}
170
171unsafe fn init_frame_layout_raw(
180 ptr: *mut u8, capacity: usize, slot_size: usize, region_bytes: usize,
181) {
182 let inline_budget = slot_size - DESC_HEADER_BYTES;
183 unsafe {
184 let desc_base = std::mem::size_of::<FrameHeader>();
185 std::ptr::write_bytes(ptr, 0, desc_base + capacity * slot_size);
186 let hdr = ptr as *mut FrameHeader;
187 (*hdr).capacity = capacity as u64;
188 (*hdr).slot_size = slot_size as u64;
189 (*hdr).region_bytes = region_bytes as u64;
190 (*hdr).inline_budget = inline_budget as u64;
191 std::ptr::write_volatile(&raw mut (*hdr).magic, FRAME_MAGIC);
192 }
193}
194
195impl FrameRing {
196 pub fn create_anon(
200 capacity: usize, slot_size: usize, region_bytes: usize,
201 ) -> Result<Self, RingError> {
202 validate_params(capacity, slot_size, region_bytes)?;
203 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
204 let mut mmap = MmapOptions::new().len(total).map_anon()?;
205 unsafe { init_frame_layout_raw(mmap.as_mut_ptr(), capacity, slot_size, region_bytes) };
206 let raw_ptr = mmap.as_mut_ptr();
207 Ok(Self::from_parts(
208 FrameBacking::Anon(mmap), raw_ptr, capacity, slot_size, region_bytes,
209 ))
210 }
211
212 pub fn create(
219 path: impl AsRef<Path>, capacity: usize, slot_size: usize, region_bytes: usize,
220 ) -> Result<Self, RingError> {
221 validate_params(capacity, slot_size, region_bytes)?;
222 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
223 let (file, mut mmap) = crate::mmf_attach::create_or_attach(
224 path.as_ref(),
225 total,
226 |ptr| unsafe { init_frame_layout_raw(ptr, capacity, slot_size, region_bytes) },
227 |ptr| unsafe { (*(ptr as *const FrameHeader)).magic == FRAME_MAGIC },
228 )?;
229 Self::check_header(mmap.as_ptr(), capacity, slot_size, region_bytes)?;
230 let raw_ptr = mmap.as_mut_ptr();
231 Ok(Self::from_parts(
232 FrameBacking::File(file, mmap), raw_ptr, capacity, slot_size, region_bytes,
233 ))
234 }
235
236 pub fn reset(
240 path: impl AsRef<Path>, capacity: usize, slot_size: usize, region_bytes: usize,
241 ) -> Result<Self, RingError> {
242 validate_params(capacity, slot_size, region_bytes)?;
243 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
244 let (file, mut mmap) = crate::mmf_attach::reset(
245 path.as_ref(),
246 total,
247 |ptr| unsafe { init_frame_layout_raw(ptr, capacity, slot_size, region_bytes) },
248 )?;
249 let raw_ptr = mmap.as_mut_ptr();
250 Ok(Self::from_parts(
251 FrameBacking::File(file, mmap), raw_ptr, capacity, slot_size, region_bytes,
252 ))
253 }
254
255 pub fn open(
257 path: impl AsRef<Path>, capacity: usize, slot_size: usize, region_bytes: usize,
258 ) -> Result<Self, RingError> {
259 validate_params(capacity, slot_size, region_bytes)?;
260 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
261 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
262 if (file.metadata()?.len() as usize) < total {
263 return Err(RingError::LayoutMismatch);
264 }
265 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
266 Self::check_header(mmap.as_ptr(), capacity, slot_size, region_bytes)?;
267 let raw_ptr = mmap.as_mut_ptr();
268 Ok(Self::from_parts(
269 FrameBacking::File(file, mmap), raw_ptr, capacity, slot_size, region_bytes,
270 ))
271 }
272
273 pub fn create_from_shm(
276 mut shm: crate::shm_file::ShmFile,
277 capacity: usize, slot_size: usize, region_bytes: usize,
278 ) -> Result<Self, RingError> {
279 validate_params(capacity, slot_size, region_bytes)?;
280 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
281 if shm.len() < total {
282 return Err(RingError::LayoutMismatch);
283 }
284 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
285 unsafe { init_frame_layout_raw(raw_ptr, capacity, slot_size, region_bytes) };
286 Ok(Self::from_parts(
287 FrameBacking::Shm(shm), raw_ptr, capacity, slot_size, region_bytes,
288 ))
289 }
290
291 pub fn open_from_shm(
293 mut shm: crate::shm_file::ShmFile,
294 capacity: usize, slot_size: usize, region_bytes: usize,
295 ) -> Result<Self, RingError> {
296 validate_params(capacity, slot_size, region_bytes)?;
297 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
298 if shm.len() < total {
299 return Err(RingError::LayoutMismatch);
300 }
301 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
302 Self::check_header(raw_ptr, capacity, slot_size, region_bytes)?;
303 Ok(Self::from_parts(
304 FrameBacking::Shm(shm), raw_ptr, capacity, slot_size, region_bytes,
305 ))
306 }
307
308 fn from_parts(
309 backing: FrameBacking, raw_ptr: *mut u8,
310 capacity: usize, slot_size: usize, region_bytes: usize,
311 ) -> Self {
312 let desc_base = std::mem::size_of::<FrameHeader>();
313 let region_base = desc_base + capacity * slot_size;
314 Self {
315 _backing: backing, raw_ptr, capacity, slot_size, region_bytes,
316 inline_budget: slot_size - DESC_HEADER_BYTES,
317 desc_base, region_base,
318 }
319 }
320
321 fn check_header(
322 ptr: *const u8, capacity: usize, slot_size: usize, region_bytes: usize,
323 ) -> Result<(), RingError> {
324 let header = unsafe { &*(ptr as *const FrameHeader) };
325 if header.magic != FRAME_MAGIC
326 || header.capacity != capacity as u64
327 || header.slot_size != slot_size as u64
328 || header.region_bytes != region_bytes as u64
329 {
330 return Err(RingError::LayoutMismatch);
331 }
332 Ok(())
333 }
334
335 pub fn capacity(&self) -> usize { self.capacity }
337 pub fn slot_size(&self) -> usize { self.slot_size }
339 pub fn inline_budget(&self) -> usize { self.inline_budget }
341 pub fn region_bytes(&self) -> usize { self.region_bytes }
343 pub fn max_payload(&self) -> usize { self.region_bytes / 2 }
345
346 fn header(&self) -> &FrameHeader {
347 unsafe { &*(self.raw_ptr as *const FrameHeader) }
348 }
349
350 fn desc_slot_ptr(&self, idx: u64) -> *mut u8 {
351 let masked = (idx as usize) & (self.capacity - 1);
352 unsafe { self.raw_ptr.add(self.desc_base + masked * self.slot_size) }
353 }
354
355 fn region_ptr(&self) -> *mut u8 {
356 unsafe { self.raw_ptr.add(self.region_base) }
357 }
358
359 pub fn approx_len(&self) -> usize {
361 let h = self.header();
362 h.desc_head.load(Ordering::Acquire)
363 .saturating_sub(h.desc_tail.load(Ordering::Acquire)) as usize
364 }
365
366 pub fn send(&self, payload: &[u8]) -> Result<FrameClass, RingError> {
368 self.send_as(payload, LayoutHint::Auto)
369 }
370
371 pub fn send_as(&self, payload: &[u8], hint: LayoutHint)
374 -> Result<FrameClass, RingError>
375 {
376 let h = self.header();
377 let head = h.desc_head.load(Ordering::Relaxed);
378 let tail = h.desc_tail.load(Ordering::Acquire);
379 if head.wrapping_sub(tail) >= self.capacity as u64 {
380 return Err(RingError::Full);
381 }
382
383 let inline = match hint {
384 LayoutHint::ForceInline => {
385 if payload.len() > self.inline_budget {
386 return Err(RingError::PayloadTooLarge);
387 }
388 true
389 }
390 LayoutHint::ForceOffset => false,
391 LayoutHint::Auto => payload.len() <= self.inline_budget,
392 };
393
394 let slot = self.desc_slot_ptr(head);
395 let len = payload.len() as u32;
396
397 let class = if inline {
398 unsafe {
399 slot.write(FrameClass::Inline as u8);
400 std::ptr::copy_nonoverlapping(
401 len.to_le_bytes().as_ptr(), slot.add(4), 4,
402 );
403 std::ptr::copy_nonoverlapping(
404 payload.as_ptr(), slot.add(DESC_HEADER_BYTES), payload.len(),
405 );
406 }
407 FrameClass::Inline
408 } else {
409 if payload.len() > self.max_payload() {
410 return Err(RingError::PayloadTooLarge);
411 }
412 let rh = h.region_head.load(Ordering::Relaxed);
413 let rt = h.region_tail.load(Ordering::Acquire);
414 let rb = self.region_bytes as u64;
415 let phys = rh % rb;
416 let start = if phys + payload.len() as u64 > rb {
419 rh + (rb - phys)
420 } else {
421 rh
422 };
423 if start.wrapping_add(payload.len() as u64).wrapping_sub(rt) > rb {
424 return Err(RingError::Full);
425 }
426 let pstart = (start % rb) as usize;
427 unsafe {
428 std::ptr::copy_nonoverlapping(
429 payload.as_ptr(), self.region_ptr().add(pstart), payload.len(),
430 );
431 }
432 h.region_head.store(start + payload.len() as u64, Ordering::Release);
435 unsafe {
436 slot.write(FrameClass::Offset as u8);
437 std::ptr::copy_nonoverlapping(
438 len.to_le_bytes().as_ptr(), slot.add(4), 4,
439 );
440 std::ptr::copy_nonoverlapping(
441 start.to_le_bytes().as_ptr(), slot.add(DESC_HEADER_BYTES), 8,
442 );
443 }
444 FrameClass::Offset
445 };
446
447 h.desc_head.store(head + 1, Ordering::Release);
448 crate::cache_ops::cldemote(slot as *const u8);
449 Ok(class)
450 }
451
452 pub fn recv_into(&self, out: &mut Vec<u8>) -> Result<FrameClass, RingError> {
456 let h = self.header();
457 let tail = h.desc_tail.load(Ordering::Relaxed);
458 let head = h.desc_head.load(Ordering::Acquire);
459 if tail == head {
460 return Err(RingError::Empty);
461 }
462 let slot = self.desc_slot_ptr(tail);
463 let class_byte = unsafe { slot.read() };
464 let len = unsafe {
465 let mut b = [0u8; 4];
466 std::ptr::copy_nonoverlapping(slot.add(4), b.as_mut_ptr(), 4);
467 u32::from_le_bytes(b) as usize
468 };
469
470 out.clear();
471 out.reserve(len);
472 let class = if class_byte == FrameClass::Inline as u8 {
473 unsafe {
474 std::ptr::copy_nonoverlapping(
475 slot.add(DESC_HEADER_BYTES),
476 out.spare_capacity_mut().as_mut_ptr() as *mut u8,
477 len,
478 );
479 out.set_len(len);
480 }
481 FrameClass::Inline
482 } else {
483 let off = unsafe {
484 let mut b = [0u8; 8];
485 std::ptr::copy_nonoverlapping(slot.add(DESC_HEADER_BYTES), b.as_mut_ptr(), 8);
486 u64::from_le_bytes(b)
487 };
488 let pstart = (off % self.region_bytes as u64) as usize;
489 unsafe {
490 std::ptr::copy_nonoverlapping(
491 self.region_ptr().add(pstart),
492 out.spare_capacity_mut().as_mut_ptr() as *mut u8,
493 len,
494 );
495 out.set_len(len);
496 }
497 h.region_tail.store(off + len as u64, Ordering::Release);
499 FrameClass::Offset
500 };
501
502 h.desc_tail.store(tail + 1, Ordering::Release);
503 crate::cache_ops::cldemote(slot as *const u8);
504 Ok(class)
505 }
506
507 pub fn recv(&self) -> Result<Vec<u8>, RingError> {
509 let mut out = Vec::new();
510 self.recv_into(&mut out)?;
511 Ok(out)
512 }
513
514 pub fn flush(&self) -> Result<(), RingError> {
516 if let FrameBacking::File(_, mmap) = &self._backing {
517 mmap.flush()?;
518 }
519 Ok(())
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use std::sync::Arc;
527 use std::thread;
528
529 fn ring() -> FrameRing {
530 FrameRing::create_anon(16, 64, 1 << 16).unwrap()
532 }
533
534 #[test]
535 fn inline_round_trip() {
536 let r = ring();
537 let payload = b"small payload under the inline budget";
538 assert_eq!(r.send(payload).unwrap(), FrameClass::Inline);
539 let got = r.recv().unwrap();
540 assert_eq!(got, payload);
541 }
542
543 #[test]
544 fn offset_round_trip_large() {
545 let r = ring();
546 let payload = vec![0xABu8; 4096]; assert_eq!(r.send(&payload).unwrap(), FrameClass::Offset);
548 let got = r.recv().unwrap();
549 assert_eq!(got, payload);
550 }
551
552 #[test]
553 fn boundary_inline_vs_offset() {
554 let r = ring();
555 let at = vec![1u8; r.inline_budget()];
557 assert_eq!(r.send(&at).unwrap(), FrameClass::Inline);
558 assert_eq!(r.recv().unwrap(), at);
559 let over = vec![2u8; r.inline_budget() + 1];
561 assert_eq!(r.send(&over).unwrap(), FrameClass::Offset);
562 assert_eq!(r.recv().unwrap(), over);
563 }
564
565 #[test]
566 fn empty_payload_round_trip() {
567 let r = ring();
568 assert_eq!(r.send(&[]).unwrap(), FrameClass::Inline);
569 assert_eq!(r.recv().unwrap(), Vec::<u8>::new());
570 }
571
572 #[test]
573 fn force_offset_overrides_small() {
574 let r = ring();
575 assert_eq!(r.send_as(b"tiny", LayoutHint::ForceOffset).unwrap(),
576 FrameClass::Offset);
577 assert_eq!(r.recv().unwrap(), b"tiny");
578 }
579
580 #[test]
581 fn force_inline_rejects_oversize() {
582 let r = ring();
583 let big = vec![0u8; r.inline_budget() + 1];
584 assert_eq!(r.send_as(&big, LayoutHint::ForceInline).unwrap_err(),
585 RingError::PayloadTooLarge);
586 }
587
588 #[test]
589 fn payload_over_region_cap_rejected() {
590 let r = ring();
591 let too_big = vec![0u8; r.max_payload() + 1];
592 assert_eq!(r.send(&too_big).unwrap_err(), RingError::PayloadTooLarge);
593 }
594
595 #[test]
596 fn descriptor_full_then_drains() {
597 let r = FrameRing::create_anon(4, 64, 1 << 16).unwrap();
598 for i in 0..4u8 {
599 r.send(&[i; 8]).unwrap();
600 }
601 assert_eq!(r.send(&[9u8; 8]).unwrap_err(), RingError::Full);
602 assert_eq!(r.recv().unwrap(), &[0u8; 8]);
603 r.send(&[9u8; 8]).unwrap();
604 }
605
606 #[test]
607 fn region_wraps_with_skip_pad() {
608 let region = 1usize << 12; let r = FrameRing::create_anon(8, 64, region).unwrap();
613 for i in 0..200u32 {
614 let len = 600 + (i as usize % 700); let payload: Vec<u8> = (0..len).map(|k| (k as u32 ^ i) as u8).collect();
616 assert_eq!(r.send(&payload).unwrap(), FrameClass::Offset);
617 let got = r.recv().unwrap();
618 assert_eq!(got, payload, "record {i} survived the region wrap");
619 }
620 }
621
622 #[test]
623 fn mixed_inline_and_offset_fifo_order() {
624 let r = FrameRing::create_anon(64, 64, 1 << 16).unwrap();
625 let mut expected = Vec::new();
626 for i in 0..40u32 {
627 let len = if i % 2 == 0 { 16 } else { 500 };
629 let p: Vec<u8> = (0..len).map(|k| (k as u32 + i) as u8).collect();
630 r.send(&p).unwrap();
631 expected.push(p);
632 }
633 for want in expected {
634 assert_eq!(r.recv().unwrap(), want);
635 }
636 assert_eq!(r.recv().unwrap_err(), RingError::Empty);
637 }
638
639 #[test]
640 fn two_thread_mixed_size_stream() {
641 let r = Arc::new(FrameRing::create_anon(256, 64, 1 << 20).unwrap());
642 let rp = r.clone();
643 let rc = r.clone();
644 const N: u32 = 50_000;
645
646 let producer = thread::spawn(move || {
647 for i in 0..N {
648 let len = (i as usize % 300) + 1;
654 let p: Vec<u8> =
655 (0..len).map(|k| i.wrapping_add(k as u32) as u8).collect();
656 while rp.send(&p).is_err() {
657 std::hint::spin_loop();
658 }
659 }
660 });
661
662 let consumer = thread::spawn(move || {
663 let mut buf = Vec::new();
664 let mut got = 0u32;
665 while got < N {
666 if rc.recv_into(&mut buf).is_ok() {
667 let len = (got as usize % 300) + 1;
668 assert_eq!(buf.len(), len, "item {got} length");
669 for (k, &b) in buf.iter().enumerate() {
670 assert_eq!(b, got.wrapping_add(k as u32) as u8,
671 "item {got} byte {k}");
672 }
673 got += 1;
674 } else {
675 std::hint::spin_loop();
676 }
677 }
678 });
679
680 producer.join().unwrap();
681 consumer.join().unwrap();
682 }
683
684 #[test]
685 fn shm_cross_handle_visibility() {
686 use crate::shm_file::ShmFile;
687 let nonce = std::time::SystemTime::now()
688 .duration_since(std::time::UNIX_EPOCH)
689 .map(|d| d.as_nanos())
690 .unwrap_or(0);
691 let name = format!("frame_shm_{}_{}", std::process::id(), nonce);
692 let (cap, slot, region) = (16usize, 64usize, 1usize << 16);
693 let size = frame_ring_file_size(cap, slot, region);
694
695 let shm_a = ShmFile::create_or_open_named(&name, size).unwrap();
696 let producer = FrameRing::create_from_shm(shm_a, cap, slot, region).unwrap();
697 let shm_b = ShmFile::create_or_open_named(&name, size).unwrap();
698 let consumer = FrameRing::open_from_shm(shm_b, cap, slot, region).unwrap();
699
700 let small = b"inline across handles";
701 let large = vec![0x5Au8; 2000];
702 producer.send(small).unwrap();
703 producer.send(&large).unwrap();
704 assert_eq!(consumer.recv().unwrap(), small);
705 assert_eq!(consumer.recv().unwrap(), large);
706 }
707
708 #[test]
709 fn file_round_trips() {
710 let p = std::env::temp_dir().join(format!(
711 "subetha-frame-{}.bin", std::process::id(),
712 ));
713 std::fs::remove_file(&p).ok();
714 let (cap, slot, region) = (16usize, 128usize, 1usize << 16);
715 {
716 let r = FrameRing::create(&p, cap, slot, region).unwrap();
717 r.send(b"persisted inline").unwrap();
718 r.send(&vec![7u8; 3000]).unwrap();
719 r.flush().unwrap();
720 }
721 let r2 = FrameRing::open(&p, cap, slot, region).unwrap();
722 assert_eq!(r2.recv().unwrap(), b"persisted inline");
723 assert_eq!(r2.recv().unwrap(), vec![7u8; 3000]);
724 std::fs::remove_file(&p).ok();
725 }
726
727 #[test]
728 fn rejects_bad_params() {
729 assert!(matches!(FrameRing::create_anon(3, 64, 1 << 16),
730 Err(RingError::LayoutMismatch))); assert!(matches!(FrameRing::create_anon(16, 8, 1 << 16),
732 Err(RingError::LayoutMismatch))); assert!(matches!(FrameRing::create_anon(16, 64, 1000),
734 Err(RingError::LayoutMismatch))); }
736
737 #[test]
740 fn second_create_attaches_and_keeps_frames() {
741 let p = std::env::temp_dir().join(format!(
742 "subetha-frame-attach-{}.bin", std::process::id(),
743 ));
744 std::fs::remove_file(&p).ok();
745 let (cap, slot, region) = (16usize, 128usize, 1usize << 16);
746
747 let r = FrameRing::create(&p, cap, slot, region).unwrap();
748 r.send(b"inline survives attach").unwrap();
749 r.send(&vec![9u8; 3000]).unwrap();
750
751 let r2 = FrameRing::create(&p, cap, slot, region).unwrap();
752 assert_eq!(r2.recv().unwrap(), b"inline survives attach",
753 "attach lost a queued inline frame");
754 assert_eq!(r2.recv().unwrap(), vec![9u8; 3000],
755 "attach lost a queued region frame");
756 assert!(matches!(
757 FrameRing::create(&p, 8, slot, region),
758 Err(RingError::LayoutMismatch),
759 ));
760
761 drop(r);
764 drop(r2);
765 let fresh = FrameRing::reset(&p, cap, slot, region).unwrap();
766 assert_eq!(fresh.approx_len(), 0, "reset kept a queued frame");
767 drop(fresh);
768 std::fs::remove_file(&p).ok();
769 }
770}