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(
172 ptr: *mut u8, capacity: usize, slot_size: usize, region_bytes: usize,
173) {
174 let inline_budget = slot_size - DESC_HEADER_BYTES;
175 unsafe {
176 std::ptr::write(ptr as *mut FrameHeader, FrameHeader {
177 magic: FRAME_MAGIC,
178 capacity: capacity as u64,
179 slot_size: slot_size as u64,
180 region_bytes: region_bytes as u64,
181 inline_budget: inline_budget as u64,
182 _pad_meta: [0; 64 - 40],
183 desc_head: AtomicU64::new(0),
184 _pad_dh: [0; 64 - 8],
185 desc_tail: AtomicU64::new(0),
186 _pad_dt: [0; 64 - 8],
187 region_head: AtomicU64::new(0),
188 _pad_rh: [0; 64 - 8],
189 region_tail: AtomicU64::new(0),
190 _pad_rt: [0; 64 - 8],
191 });
192 let desc_base = std::mem::size_of::<FrameHeader>();
195 std::ptr::write_bytes(ptr.add(desc_base), 0, capacity * slot_size);
196 }
197}
198
199impl FrameRing {
200 pub fn create_anon(
204 capacity: usize, slot_size: usize, region_bytes: usize,
205 ) -> Result<Self, RingError> {
206 validate_params(capacity, slot_size, region_bytes)?;
207 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
208 let mut mmap = MmapOptions::new().len(total).map_anon()?;
209 unsafe { init_frame_layout_raw(mmap.as_mut_ptr(), capacity, slot_size, region_bytes) };
210 let raw_ptr = mmap.as_mut_ptr();
211 Ok(Self::from_parts(
212 FrameBacking::Anon(mmap), raw_ptr, capacity, slot_size, region_bytes,
213 ))
214 }
215
216 pub fn create(
218 path: impl AsRef<Path>, capacity: usize, slot_size: usize, region_bytes: usize,
219 ) -> Result<Self, RingError> {
220 validate_params(capacity, slot_size, region_bytes)?;
221 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
222 let file = OpenOptions::new()
223 .read(true).write(true).create(true).truncate(true)
224 .open(path.as_ref())?;
225 file.set_len(total as u64)?;
226 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
227 unsafe { init_frame_layout_raw(mmap.as_mut_ptr(), capacity, slot_size, region_bytes) };
228 let raw_ptr = mmap.as_mut_ptr();
229 Ok(Self::from_parts(
230 FrameBacking::File(file, mmap), raw_ptr, capacity, slot_size, region_bytes,
231 ))
232 }
233
234 pub fn open(
236 path: impl AsRef<Path>, capacity: usize, slot_size: usize, region_bytes: usize,
237 ) -> Result<Self, RingError> {
238 validate_params(capacity, slot_size, region_bytes)?;
239 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
240 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
241 if (file.metadata()?.len() as usize) < total {
242 return Err(RingError::LayoutMismatch);
243 }
244 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
245 Self::check_header(mmap.as_ptr(), capacity, slot_size, region_bytes)?;
246 let raw_ptr = mmap.as_mut_ptr();
247 Ok(Self::from_parts(
248 FrameBacking::File(file, mmap), raw_ptr, capacity, slot_size, region_bytes,
249 ))
250 }
251
252 pub fn create_from_shm(
255 mut shm: crate::shm_file::ShmFile,
256 capacity: usize, slot_size: usize, region_bytes: usize,
257 ) -> Result<Self, RingError> {
258 validate_params(capacity, slot_size, region_bytes)?;
259 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
260 if shm.len() < total {
261 return Err(RingError::LayoutMismatch);
262 }
263 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
264 unsafe { init_frame_layout_raw(raw_ptr, capacity, slot_size, region_bytes) };
265 Ok(Self::from_parts(
266 FrameBacking::Shm(shm), raw_ptr, capacity, slot_size, region_bytes,
267 ))
268 }
269
270 pub fn open_from_shm(
272 mut shm: crate::shm_file::ShmFile,
273 capacity: usize, slot_size: usize, region_bytes: usize,
274 ) -> Result<Self, RingError> {
275 validate_params(capacity, slot_size, region_bytes)?;
276 let total = frame_ring_file_size(capacity, slot_size, region_bytes);
277 if shm.len() < total {
278 return Err(RingError::LayoutMismatch);
279 }
280 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
281 Self::check_header(raw_ptr, capacity, slot_size, region_bytes)?;
282 Ok(Self::from_parts(
283 FrameBacking::Shm(shm), raw_ptr, capacity, slot_size, region_bytes,
284 ))
285 }
286
287 fn from_parts(
288 backing: FrameBacking, raw_ptr: *mut u8,
289 capacity: usize, slot_size: usize, region_bytes: usize,
290 ) -> Self {
291 let desc_base = std::mem::size_of::<FrameHeader>();
292 let region_base = desc_base + capacity * slot_size;
293 Self {
294 _backing: backing, raw_ptr, capacity, slot_size, region_bytes,
295 inline_budget: slot_size - DESC_HEADER_BYTES,
296 desc_base, region_base,
297 }
298 }
299
300 fn check_header(
301 ptr: *const u8, capacity: usize, slot_size: usize, region_bytes: usize,
302 ) -> Result<(), RingError> {
303 let header = unsafe { &*(ptr as *const FrameHeader) };
304 if header.magic != FRAME_MAGIC
305 || header.capacity != capacity as u64
306 || header.slot_size != slot_size as u64
307 || header.region_bytes != region_bytes as u64
308 {
309 return Err(RingError::LayoutMismatch);
310 }
311 Ok(())
312 }
313
314 pub fn capacity(&self) -> usize { self.capacity }
316 pub fn slot_size(&self) -> usize { self.slot_size }
318 pub fn inline_budget(&self) -> usize { self.inline_budget }
320 pub fn region_bytes(&self) -> usize { self.region_bytes }
322 pub fn max_payload(&self) -> usize { self.region_bytes / 2 }
324
325 fn header(&self) -> &FrameHeader {
326 unsafe { &*(self.raw_ptr as *const FrameHeader) }
327 }
328
329 fn desc_slot_ptr(&self, idx: u64) -> *mut u8 {
330 let masked = (idx as usize) & (self.capacity - 1);
331 unsafe { self.raw_ptr.add(self.desc_base + masked * self.slot_size) }
332 }
333
334 fn region_ptr(&self) -> *mut u8 {
335 unsafe { self.raw_ptr.add(self.region_base) }
336 }
337
338 pub fn approx_len(&self) -> usize {
340 let h = self.header();
341 h.desc_head.load(Ordering::Acquire)
342 .saturating_sub(h.desc_tail.load(Ordering::Acquire)) as usize
343 }
344
345 pub fn send(&self, payload: &[u8]) -> Result<FrameClass, RingError> {
347 self.send_as(payload, LayoutHint::Auto)
348 }
349
350 pub fn send_as(&self, payload: &[u8], hint: LayoutHint)
353 -> Result<FrameClass, RingError>
354 {
355 let h = self.header();
356 let head = h.desc_head.load(Ordering::Relaxed);
357 let tail = h.desc_tail.load(Ordering::Acquire);
358 if head.wrapping_sub(tail) >= self.capacity as u64 {
359 return Err(RingError::Full);
360 }
361
362 let inline = match hint {
363 LayoutHint::ForceInline => {
364 if payload.len() > self.inline_budget {
365 return Err(RingError::PayloadTooLarge);
366 }
367 true
368 }
369 LayoutHint::ForceOffset => false,
370 LayoutHint::Auto => payload.len() <= self.inline_budget,
371 };
372
373 let slot = self.desc_slot_ptr(head);
374 let len = payload.len() as u32;
375
376 let class = if inline {
377 unsafe {
378 slot.write(FrameClass::Inline as u8);
379 std::ptr::copy_nonoverlapping(
380 len.to_le_bytes().as_ptr(), slot.add(4), 4,
381 );
382 std::ptr::copy_nonoverlapping(
383 payload.as_ptr(), slot.add(DESC_HEADER_BYTES), payload.len(),
384 );
385 }
386 FrameClass::Inline
387 } else {
388 if payload.len() > self.max_payload() {
389 return Err(RingError::PayloadTooLarge);
390 }
391 let rh = h.region_head.load(Ordering::Relaxed);
392 let rt = h.region_tail.load(Ordering::Acquire);
393 let rb = self.region_bytes as u64;
394 let phys = rh % rb;
395 let start = if phys + payload.len() as u64 > rb {
398 rh + (rb - phys)
399 } else {
400 rh
401 };
402 if start.wrapping_add(payload.len() as u64).wrapping_sub(rt) > rb {
403 return Err(RingError::Full);
404 }
405 let pstart = (start % rb) as usize;
406 unsafe {
407 std::ptr::copy_nonoverlapping(
408 payload.as_ptr(), self.region_ptr().add(pstart), payload.len(),
409 );
410 }
411 h.region_head.store(start + payload.len() as u64, Ordering::Release);
414 unsafe {
415 slot.write(FrameClass::Offset as u8);
416 std::ptr::copy_nonoverlapping(
417 len.to_le_bytes().as_ptr(), slot.add(4), 4,
418 );
419 std::ptr::copy_nonoverlapping(
420 start.to_le_bytes().as_ptr(), slot.add(DESC_HEADER_BYTES), 8,
421 );
422 }
423 FrameClass::Offset
424 };
425
426 h.desc_head.store(head + 1, Ordering::Release);
427 crate::cache_ops::cldemote(slot as *const u8);
428 Ok(class)
429 }
430
431 pub fn recv_into(&self, out: &mut Vec<u8>) -> Result<FrameClass, RingError> {
435 let h = self.header();
436 let tail = h.desc_tail.load(Ordering::Relaxed);
437 let head = h.desc_head.load(Ordering::Acquire);
438 if tail == head {
439 return Err(RingError::Empty);
440 }
441 let slot = self.desc_slot_ptr(tail);
442 let class_byte = unsafe { slot.read() };
443 let len = unsafe {
444 let mut b = [0u8; 4];
445 std::ptr::copy_nonoverlapping(slot.add(4), b.as_mut_ptr(), 4);
446 u32::from_le_bytes(b) as usize
447 };
448
449 out.clear();
450 out.reserve(len);
451 let class = if class_byte == FrameClass::Inline as u8 {
452 unsafe {
453 std::ptr::copy_nonoverlapping(
454 slot.add(DESC_HEADER_BYTES),
455 out.spare_capacity_mut().as_mut_ptr() as *mut u8,
456 len,
457 );
458 out.set_len(len);
459 }
460 FrameClass::Inline
461 } else {
462 let off = unsafe {
463 let mut b = [0u8; 8];
464 std::ptr::copy_nonoverlapping(slot.add(DESC_HEADER_BYTES), b.as_mut_ptr(), 8);
465 u64::from_le_bytes(b)
466 };
467 let pstart = (off % self.region_bytes as u64) as usize;
468 unsafe {
469 std::ptr::copy_nonoverlapping(
470 self.region_ptr().add(pstart),
471 out.spare_capacity_mut().as_mut_ptr() as *mut u8,
472 len,
473 );
474 out.set_len(len);
475 }
476 h.region_tail.store(off + len as u64, Ordering::Release);
478 FrameClass::Offset
479 };
480
481 h.desc_tail.store(tail + 1, Ordering::Release);
482 crate::cache_ops::cldemote(slot as *const u8);
483 Ok(class)
484 }
485
486 pub fn recv(&self) -> Result<Vec<u8>, RingError> {
488 let mut out = Vec::new();
489 self.recv_into(&mut out)?;
490 Ok(out)
491 }
492
493 pub fn flush(&self) -> Result<(), RingError> {
495 if let FrameBacking::File(_, mmap) = &self._backing {
496 mmap.flush()?;
497 }
498 Ok(())
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use std::sync::Arc;
506 use std::thread;
507
508 fn ring() -> FrameRing {
509 FrameRing::create_anon(16, 64, 1 << 16).unwrap()
511 }
512
513 #[test]
514 fn inline_round_trip() {
515 let r = ring();
516 let payload = b"small payload under the inline budget";
517 assert_eq!(r.send(payload).unwrap(), FrameClass::Inline);
518 let got = r.recv().unwrap();
519 assert_eq!(got, payload);
520 }
521
522 #[test]
523 fn offset_round_trip_large() {
524 let r = ring();
525 let payload = vec![0xABu8; 4096]; assert_eq!(r.send(&payload).unwrap(), FrameClass::Offset);
527 let got = r.recv().unwrap();
528 assert_eq!(got, payload);
529 }
530
531 #[test]
532 fn boundary_inline_vs_offset() {
533 let r = ring();
534 let at = vec![1u8; r.inline_budget()];
536 assert_eq!(r.send(&at).unwrap(), FrameClass::Inline);
537 assert_eq!(r.recv().unwrap(), at);
538 let over = vec![2u8; r.inline_budget() + 1];
540 assert_eq!(r.send(&over).unwrap(), FrameClass::Offset);
541 assert_eq!(r.recv().unwrap(), over);
542 }
543
544 #[test]
545 fn empty_payload_round_trip() {
546 let r = ring();
547 assert_eq!(r.send(&[]).unwrap(), FrameClass::Inline);
548 assert_eq!(r.recv().unwrap(), Vec::<u8>::new());
549 }
550
551 #[test]
552 fn force_offset_overrides_small() {
553 let r = ring();
554 assert_eq!(r.send_as(b"tiny", LayoutHint::ForceOffset).unwrap(),
555 FrameClass::Offset);
556 assert_eq!(r.recv().unwrap(), b"tiny");
557 }
558
559 #[test]
560 fn force_inline_rejects_oversize() {
561 let r = ring();
562 let big = vec![0u8; r.inline_budget() + 1];
563 assert_eq!(r.send_as(&big, LayoutHint::ForceInline).unwrap_err(),
564 RingError::PayloadTooLarge);
565 }
566
567 #[test]
568 fn payload_over_region_cap_rejected() {
569 let r = ring();
570 let too_big = vec![0u8; r.max_payload() + 1];
571 assert_eq!(r.send(&too_big).unwrap_err(), RingError::PayloadTooLarge);
572 }
573
574 #[test]
575 fn descriptor_full_then_drains() {
576 let r = FrameRing::create_anon(4, 64, 1 << 16).unwrap();
577 for i in 0..4u8 {
578 r.send(&[i; 8]).unwrap();
579 }
580 assert_eq!(r.send(&[9u8; 8]).unwrap_err(), RingError::Full);
581 assert_eq!(r.recv().unwrap(), &[0u8; 8]);
582 r.send(&[9u8; 8]).unwrap();
583 }
584
585 #[test]
586 fn region_wraps_with_skip_pad() {
587 let region = 1usize << 12; let r = FrameRing::create_anon(8, 64, region).unwrap();
592 for i in 0..200u32 {
593 let len = 600 + (i as usize % 700); let payload: Vec<u8> = (0..len).map(|k| (k as u32 ^ i) as u8).collect();
595 assert_eq!(r.send(&payload).unwrap(), FrameClass::Offset);
596 let got = r.recv().unwrap();
597 assert_eq!(got, payload, "record {i} survived the region wrap");
598 }
599 }
600
601 #[test]
602 fn mixed_inline_and_offset_fifo_order() {
603 let r = FrameRing::create_anon(64, 64, 1 << 16).unwrap();
604 let mut expected = Vec::new();
605 for i in 0..40u32 {
606 let len = if i % 2 == 0 { 16 } else { 500 };
608 let p: Vec<u8> = (0..len).map(|k| (k as u32 + i) as u8).collect();
609 r.send(&p).unwrap();
610 expected.push(p);
611 }
612 for want in expected {
613 assert_eq!(r.recv().unwrap(), want);
614 }
615 assert_eq!(r.recv().unwrap_err(), RingError::Empty);
616 }
617
618 #[test]
619 fn two_thread_mixed_size_stream() {
620 let r = Arc::new(FrameRing::create_anon(256, 64, 1 << 20).unwrap());
621 let rp = r.clone();
622 let rc = r.clone();
623 const N: u32 = 50_000;
624
625 let producer = thread::spawn(move || {
626 for i in 0..N {
627 let len = (i as usize % 300) + 1;
633 let p: Vec<u8> =
634 (0..len).map(|k| i.wrapping_add(k as u32) as u8).collect();
635 while rp.send(&p).is_err() {
636 std::hint::spin_loop();
637 }
638 }
639 });
640
641 let consumer = thread::spawn(move || {
642 let mut buf = Vec::new();
643 let mut got = 0u32;
644 while got < N {
645 if rc.recv_into(&mut buf).is_ok() {
646 let len = (got as usize % 300) + 1;
647 assert_eq!(buf.len(), len, "item {got} length");
648 for (k, &b) in buf.iter().enumerate() {
649 assert_eq!(b, got.wrapping_add(k as u32) as u8,
650 "item {got} byte {k}");
651 }
652 got += 1;
653 } else {
654 std::hint::spin_loop();
655 }
656 }
657 });
658
659 producer.join().unwrap();
660 consumer.join().unwrap();
661 }
662
663 #[test]
664 fn shm_cross_handle_visibility() {
665 use crate::shm_file::ShmFile;
666 let nonce = std::time::SystemTime::now()
667 .duration_since(std::time::UNIX_EPOCH)
668 .map(|d| d.as_nanos())
669 .unwrap_or(0);
670 let name = format!("frame_shm_{}_{}", std::process::id(), nonce);
671 let (cap, slot, region) = (16usize, 64usize, 1usize << 16);
672 let size = frame_ring_file_size(cap, slot, region);
673
674 let shm_a = ShmFile::create_or_open_named(&name, size).unwrap();
675 let producer = FrameRing::create_from_shm(shm_a, cap, slot, region).unwrap();
676 let shm_b = ShmFile::create_or_open_named(&name, size).unwrap();
677 let consumer = FrameRing::open_from_shm(shm_b, cap, slot, region).unwrap();
678
679 let small = b"inline across handles";
680 let large = vec![0x5Au8; 2000];
681 producer.send(small).unwrap();
682 producer.send(&large).unwrap();
683 assert_eq!(consumer.recv().unwrap(), small);
684 assert_eq!(consumer.recv().unwrap(), large);
685 }
686
687 #[test]
688 fn file_round_trips() {
689 let p = std::env::temp_dir().join(format!(
690 "subetha-frame-{}.bin", std::process::id(),
691 ));
692 std::fs::remove_file(&p).ok();
693 let (cap, slot, region) = (16usize, 128usize, 1usize << 16);
694 {
695 let r = FrameRing::create(&p, cap, slot, region).unwrap();
696 r.send(b"persisted inline").unwrap();
697 r.send(&vec![7u8; 3000]).unwrap();
698 r.flush().unwrap();
699 }
700 let r2 = FrameRing::open(&p, cap, slot, region).unwrap();
701 assert_eq!(r2.recv().unwrap(), b"persisted inline");
702 assert_eq!(r2.recv().unwrap(), vec![7u8; 3000]);
703 std::fs::remove_file(&p).ok();
704 }
705
706 #[test]
707 fn rejects_bad_params() {
708 assert!(matches!(FrameRing::create_anon(3, 64, 1 << 16),
709 Err(RingError::LayoutMismatch))); assert!(matches!(FrameRing::create_anon(16, 8, 1 << 16),
711 Err(RingError::LayoutMismatch))); assert!(matches!(FrameRing::create_anon(16, 64, 1000),
713 Err(RingError::LayoutMismatch))); }
715}