1use std::fs::{File, OpenOptions};
21use std::path::Path;
22use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
23
24use memmap2::{MmapMut, MmapOptions};
25
26use crate::shared_ring::RingError;
27
28pub const FRAME_REGION_MAGIC: u64 = 0x4652_474e_0000_0001;
30
31const NIL: u32 = u32::MAX;
33
34pub const MIN_BLOCK_SIZE: usize = 8;
36
37#[inline]
38fn pack(counter: u32, index: u32) -> u64 {
39 ((counter as u64) << 32) | (index as u64)
40}
41#[inline]
42fn unpack(v: u64) -> (u32, u32) {
43 ((v >> 32) as u32, v as u32)
44}
45
46#[repr(C, align(64))]
50struct FrameRegionHeader {
51 magic: AtomicU64,
52 block_size: u64,
53 block_count: u64,
54 _pad_meta: [u8; 64 - 24],
55 bump_next: AtomicU32,
57 _pad_bump: [u8; 64 - 4],
58 free_head: AtomicU64,
60 _pad_free: [u8; 64 - 8],
61}
62
63pub const fn frame_region_file_size(block_size: usize, block_count: usize) -> usize {
65 std::mem::size_of::<FrameRegionHeader>() + block_size * block_count
66}
67
68#[allow(dead_code)]
69enum RegionBacking {
70 Anon(MmapMut),
71 File(File, MmapMut),
72 Shm(crate::shm_file::ShmFile),
73}
74
75pub struct FrameRegion {
78 _backing: RegionBacking,
79 raw_ptr: *mut u8,
80 block_size: usize,
81 block_count: usize,
82 blocks_base: usize,
83}
84
85unsafe impl Send for FrameRegion {}
86unsafe impl Sync for FrameRegion {}
87
88fn validate(block_size: usize, block_count: usize) -> Result<(), RingError> {
89 if block_size < MIN_BLOCK_SIZE || !block_size.is_multiple_of(8) {
90 return Err(RingError::LayoutMismatch);
91 }
92 if block_count < 1 || block_count >= NIL as usize {
93 return Err(RingError::LayoutMismatch);
94 }
95 Ok(())
96}
97
98unsafe fn init_region(ptr: *mut u8, block_size: usize, block_count: usize) {
101 unsafe {
102 std::ptr::write(ptr as *mut FrameRegionHeader, FrameRegionHeader {
103 magic: AtomicU64::new(0),
104 block_size: block_size as u64,
105 block_count: block_count as u64,
106 _pad_meta: [0; 64 - 24],
107 bump_next: AtomicU32::new(0),
108 _pad_bump: [0; 64 - 4],
109 free_head: AtomicU64::new(pack(0, NIL)),
110 _pad_free: [0; 64 - 8],
111 });
112 (*(ptr as *const FrameRegionHeader))
113 .magic
114 .store(FRAME_REGION_MAGIC, std::sync::atomic::Ordering::Release);
115 }
116}
117
118impl FrameRegion {
119 pub fn create_anon(block_size: usize, block_count: usize) -> Result<Self, RingError> {
121 validate(block_size, block_count)?;
122 let total = frame_region_file_size(block_size, block_count);
123 let mut mmap = MmapOptions::new().len(total).map_anon()?;
124 unsafe { init_region(mmap.as_mut_ptr(), block_size, block_count) };
125 let raw_ptr = mmap.as_mut_ptr();
126 Ok(Self::from_parts(RegionBacking::Anon(mmap), raw_ptr, block_size, block_count))
127 }
128
129 pub fn create(
136 path: impl AsRef<Path>, block_size: usize, block_count: usize,
137 ) -> Result<Self, RingError> {
138 Self::create_or_open_file(path, block_size, block_count)
139 }
140
141 pub fn reset(
145 path: impl AsRef<Path>, block_size: usize, block_count: usize,
146 ) -> Result<Self, RingError> {
147 validate(block_size, block_count)?;
148 let total = frame_region_file_size(block_size, block_count);
149 let file = OpenOptions::new()
150 .read(true).write(true).create(true).truncate(true)
151 .open(path.as_ref())?;
152 file.set_len(total as u64)?;
153 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
154 unsafe { init_region(mmap.as_mut_ptr(), block_size, block_count) };
155 let raw_ptr = mmap.as_mut_ptr();
156 Ok(Self::from_parts(RegionBacking::File(file, mmap), raw_ptr, block_size, block_count))
157 }
158
159 pub fn open(
161 path: impl AsRef<Path>, block_size: usize, block_count: usize,
162 ) -> Result<Self, RingError> {
163 validate(block_size, block_count)?;
164 let total = frame_region_file_size(block_size, block_count);
165 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
166 if (file.metadata()?.len() as usize) < total {
167 return Err(RingError::LayoutMismatch);
168 }
169 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
170 Self::check_header(mmap.as_ptr(), block_size, block_count)?;
171 let raw_ptr = mmap.as_mut_ptr();
172 Ok(Self::from_parts(RegionBacking::File(file, mmap), raw_ptr, block_size, block_count))
173 }
174
175 pub fn create_from_shm(
177 mut shm: crate::shm_file::ShmFile, block_size: usize, block_count: usize,
178 ) -> Result<Self, RingError> {
179 validate(block_size, block_count)?;
180 if shm.len() < frame_region_file_size(block_size, block_count) {
181 return Err(RingError::LayoutMismatch);
182 }
183 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
184 unsafe { init_region(raw_ptr, block_size, block_count) };
185 Ok(Self::from_parts(RegionBacking::Shm(shm), raw_ptr, block_size, block_count))
186 }
187
188 pub fn open_from_shm(
190 mut shm: crate::shm_file::ShmFile, block_size: usize, block_count: usize,
191 ) -> Result<Self, RingError> {
192 validate(block_size, block_count)?;
193 if shm.len() < frame_region_file_size(block_size, block_count) {
194 return Err(RingError::LayoutMismatch);
195 }
196 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
197 Self::check_header(raw_ptr, block_size, block_count)?;
198 Ok(Self::from_parts(RegionBacking::Shm(shm), raw_ptr, block_size, block_count))
199 }
200
201 pub fn create_or_open_shm(
211 name: &str, block_size: usize, block_count: usize,
212 ) -> Result<Self, RingError> {
213 validate(block_size, block_count)?;
214 let total = frame_region_file_size(block_size, block_count);
215 let mut shm = crate::shm_file::ShmFile::create_or_open_named(name, total)
216 .map_err(|_| RingError::LayoutMismatch)?;
217 if shm.len() < total {
218 return Err(RingError::LayoutMismatch);
219 }
220 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
221 Self::guarded_init_or_attach(raw_ptr, block_size, block_count)?;
222 Ok(Self::from_parts(RegionBacking::Shm(shm), raw_ptr, block_size, block_count))
223 }
224
225 pub fn create_or_open_file(
230 path: impl AsRef<Path>, block_size: usize, block_count: usize,
231 ) -> Result<Self, RingError> {
232 validate(block_size, block_count)?;
233 let total = frame_region_file_size(block_size, block_count);
234 let file = OpenOptions::new()
235 .read(true).write(true).create(true).truncate(false)
236 .open(path.as_ref())?;
237 if (file.metadata()?.len() as usize) < total {
238 file.set_len(total as u64)?;
239 }
240 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
241 let raw_ptr = mmap.as_mut_ptr();
242 Self::guarded_init_or_attach(raw_ptr, block_size, block_count)?;
243 Ok(Self::from_parts(RegionBacking::File(file, mmap), raw_ptr, block_size, block_count))
244 }
245
246 fn guarded_init_or_attach(
252 raw_ptr: *mut u8, block_size: usize, block_count: usize,
253 ) -> Result<(), RingError> {
254 const INIT_INPROGRESS: u64 = 1;
255 let h = unsafe { &*(raw_ptr as *const FrameRegionHeader) };
256 if h
257 .magic
258 .compare_exchange(0, INIT_INPROGRESS, Ordering::AcqRel, Ordering::Acquire)
259 .is_ok()
260 {
261 unsafe {
262 let hdr = raw_ptr as *mut FrameRegionHeader;
263 (*hdr).block_size = block_size as u64;
264 (*hdr).block_count = block_count as u64;
265 (*hdr).bump_next.store(0, Ordering::Relaxed);
266 (*hdr).free_head.store(pack(0, NIL), Ordering::Relaxed);
267 }
268 h.magic.store(FRAME_REGION_MAGIC, Ordering::Release);
269 } else {
270 let mut spins = 0u32;
271 while h.magic.load(Ordering::Acquire) != FRAME_REGION_MAGIC {
272 std::hint::spin_loop();
273 spins += 1;
274 if spins > 100_000_000 {
275 return Err(RingError::LayoutMismatch);
276 }
277 }
278 }
279 if h.block_size != block_size as u64 || h.block_count != block_count as u64 {
280 return Err(RingError::LayoutMismatch);
281 }
282 Ok(())
283 }
284
285 fn from_parts(
286 backing: RegionBacking, raw_ptr: *mut u8, block_size: usize, block_count: usize,
287 ) -> Self {
288 Self {
289 _backing: backing, raw_ptr, block_size, block_count,
290 blocks_base: std::mem::size_of::<FrameRegionHeader>(),
291 }
292 }
293
294 fn check_header(ptr: *const u8, block_size: usize, block_count: usize) -> Result<(), RingError> {
295 let h = unsafe { &*(ptr as *const FrameRegionHeader) };
296 if h.magic.load(Ordering::Acquire) != FRAME_REGION_MAGIC
297 || h.block_size != block_size as u64
298 || h.block_count != block_count as u64
299 {
300 return Err(RingError::LayoutMismatch);
301 }
302 Ok(())
303 }
304
305 pub fn block_size(&self) -> usize { self.block_size }
307 pub fn block_count(&self) -> usize { self.block_count }
309
310 fn header(&self) -> &FrameRegionHeader {
311 unsafe { &*(self.raw_ptr as *const FrameRegionHeader) }
312 }
313
314 fn block_ptr(&self, idx: u32) -> *mut u8 {
315 unsafe { self.raw_ptr.add(self.blocks_base + idx as usize * self.block_size) }
316 }
317
318 fn next_link(&self, idx: u32) -> &AtomicU32 {
321 unsafe { &*(self.block_ptr(idx) as *const AtomicU32) }
322 }
323
324 pub fn alloc(&self) -> Option<u32> {
326 loop {
327 let head = self.header().free_head.load(Ordering::Acquire);
328 let (counter, idx) = unpack(head);
329 if idx == NIL {
330 break;
331 }
332 let next = self.next_link(idx).load(Ordering::Acquire);
333 let new_head = pack(counter.wrapping_add(1), next);
334 if self.header().free_head.compare_exchange(
335 head, new_head, Ordering::AcqRel, Ordering::Acquire,
336 ).is_ok() {
337 return Some(idx);
338 }
339 }
340 let idx = self.header().bump_next.fetch_add(1, Ordering::AcqRel);
341 if idx >= self.block_count as u32 {
342 self.header().bump_next.fetch_sub(1, Ordering::AcqRel);
343 return None;
344 }
345 Some(idx)
346 }
347
348 pub fn free(&self, idx: u32) {
350 if idx as usize >= self.block_count {
351 return;
352 }
353 loop {
354 let head = self.header().free_head.load(Ordering::Acquire);
355 let (counter, old_top) = unpack(head);
356 self.next_link(idx).store(old_top, Ordering::Release);
357 let new_head = pack(counter.wrapping_add(1), idx);
358 if self.header().free_head.compare_exchange(
359 head, new_head, Ordering::AcqRel, Ordering::Acquire,
360 ).is_ok() {
361 return;
362 }
363 }
364 }
365
366 pub fn write_block(&self, idx: u32, payload: &[u8]) {
369 debug_assert!(payload.len() <= self.block_size);
370 unsafe {
371 std::ptr::copy_nonoverlapping(
372 payload.as_ptr(), self.block_ptr(idx), payload.len(),
373 );
374 }
375 }
376
377 pub fn read_block_into(&self, idx: u32, len: usize, out: &mut Vec<u8>) {
379 debug_assert!(len <= self.block_size);
380 out.reserve(len);
381 unsafe {
382 std::ptr::copy_nonoverlapping(
383 self.block_ptr(idx),
384 out.spare_capacity_mut().as_mut_ptr() as *mut u8,
385 len,
386 );
387 let new_len = out.len() + len;
388 out.set_len(new_len);
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use std::sync::Arc;
397 use std::sync::atomic::AtomicUsize;
398 use std::thread;
399
400 #[test]
401 fn alloc_write_read_free_cycle() {
402 let r = FrameRegion::create_anon(256, 8).unwrap();
403 let idx = r.alloc().unwrap();
404 let payload = vec![0xABu8; 200];
405 r.write_block(idx, &payload);
406 let mut out = Vec::new();
407 r.read_block_into(idx, 200, &mut out);
408 assert_eq!(out, payload);
409 r.free(idx);
410 let idx2 = r.alloc().unwrap();
412 assert_eq!(idx2, idx, "freed block returns to the stack");
413 }
414
415 #[test]
416 fn exhausts_then_full() {
417 let r = FrameRegion::create_anon(64, 4).unwrap();
418 let a: Vec<u32> = (0..4).map(|_| r.alloc().unwrap()).collect();
419 assert_eq!(a.len(), 4);
420 assert!(r.alloc().is_none(), "region full");
421 r.free(a[1]);
422 assert!(r.alloc().is_some(), "freeing reopens a block");
423 }
424
425 #[test]
426 fn concurrent_alloc_free_no_double_issue() {
427 let r = Arc::new(FrameRegion::create_anon(64, 64).unwrap());
430 let held: Arc<Vec<AtomicUsize>> =
431 Arc::new((0..64).map(|_| AtomicUsize::new(0)).collect());
432 let mut handles = Vec::new();
433 for _ in 0..8 {
434 let r = r.clone();
435 let held = held.clone();
436 handles.push(thread::spawn(move || {
437 for _ in 0..20_000 {
438 if let Some(idx) = r.alloc() {
439 let prev = held[idx as usize].fetch_add(1, Ordering::AcqRel);
440 assert_eq!(prev, 0, "block {idx} double-issued");
441 held[idx as usize].fetch_sub(1, Ordering::AcqRel);
442 r.free(idx);
443 }
444 }
445 }));
446 }
447 for h in handles {
448 h.join().unwrap();
449 }
450 }
451
452 #[test]
453 fn shm_cross_handle() {
454 use crate::shm_file::ShmFile;
455 let nonce = std::time::SystemTime::now()
456 .duration_since(std::time::UNIX_EPOCH)
457 .map(|d| d.as_nanos())
458 .unwrap_or(0);
459 let name = format!("frame_region_{}_{}", std::process::id(), nonce);
460 let (bs, bc) = (256usize, 8usize);
461 let size = frame_region_file_size(bs, bc);
462 let a = FrameRegion::create_from_shm(
463 ShmFile::create_or_open_named(&name, size).unwrap(), bs, bc).unwrap();
464 let b = FrameRegion::open_from_shm(
465 ShmFile::create_or_open_named(&name, size).unwrap(), bs, bc).unwrap();
466 let idx = a.alloc().unwrap();
467 a.write_block(idx, b"shared across handles");
468 let mut out = Vec::new();
469 b.read_block_into(idx, 21, &mut out);
470 assert_eq!(out, b"shared across handles");
471 }
472
473 #[test]
474 fn rejects_bad_params() {
475 assert!(matches!(FrameRegion::create_anon(7, 8), Err(RingError::LayoutMismatch)));
476 assert!(matches!(FrameRegion::create_anon(64, 0), Err(RingError::LayoutMismatch)));
477 }
478
479 #[test]
482 fn second_create_attaches_and_keeps_blocks() {
483 let p = std::env::temp_dir().join(format!(
484 "subetha-frame-region-attach-{}.bin", std::process::id(),
485 ));
486 std::fs::remove_file(&p).ok();
487 let (bs, bc) = (64usize, 8usize);
488
489 let r = FrameRegion::create(&p, bs, bc).unwrap();
490 let first = r.alloc().expect("alloc");
491
492 let r2 = FrameRegion::create(&p, bs, bc).unwrap();
493 assert_ne!(
494 r2.alloc().expect("second alloc"),
495 first,
496 "attach handed back a block the first handle already holds",
497 );
498 assert!(matches!(
499 FrameRegion::create(&p, bs, bc * 2),
500 Err(RingError::LayoutMismatch),
501 ));
502
503 drop(r);
506 drop(r2);
507 let fresh = FrameRegion::reset(&p, bs, bc).unwrap();
508 assert_eq!(fresh.alloc().expect("alloc after reset"), first,
509 "reset kept an allocation");
510 drop(fresh);
511 std::fs::remove_file(&p).ok();
512 }
513}