1use std::fs::{File, OpenOptions};
62use std::marker::PhantomData;
63use std::mem::size_of;
64use std::path::Path;
65use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
66
67use memmap2::{MmapMut, MmapOptions};
68
69pub const REGION_MAGIC: u32 = 0x4150_5247;
70pub const NIL_INDEX: u32 = u32::MAX;
71
72#[repr(C, align(64))]
73pub struct RegionHeader {
74 pub magic: u32,
75 pub capacity: u32,
76 pub slot_size: u32,
77 _pad1: u32,
78 pub bump_next: AtomicU64,
79 pub free_head: AtomicU64,
80 _pad2: [u8; 32],
81}
82
83const _: () = {
84 assert!(size_of::<RegionHeader>() == 64);
85};
86
87pub const fn region_file_size(capacity: usize, slot_size: usize) -> usize {
88 size_of::<RegionHeader>()
89 + capacity * size_of::<AtomicU32>() + capacity * slot_size }
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum RegionError {
95 Full,
96 InvalidPtr,
97 PayloadTooLarge,
98 LayoutMismatch,
99 IoError(std::io::ErrorKind),
100}
101
102impl From<std::io::Error> for RegionError {
103 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
104}
105
106#[derive(Debug)]
111#[repr(C)]
112pub struct OffsetPtr<T> {
113 pub index: u32,
114 _phantom: PhantomData<T>,
115}
116
117impl<T> Clone for OffsetPtr<T> {
118 fn clone(&self) -> Self { *self }
119}
120impl<T> Copy for OffsetPtr<T> {}
121impl<T> PartialEq for OffsetPtr<T> {
122 fn eq(&self, other: &Self) -> bool { self.index == other.index }
123}
124impl<T> Eq for OffsetPtr<T> {}
125impl<T> std::hash::Hash for OffsetPtr<T> {
126 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
127 self.index.hash(state);
128 }
129}
130
131impl<T> OffsetPtr<T> {
132 pub const NIL: Self = Self { index: NIL_INDEX, _phantom: PhantomData };
133
134 #[inline]
135 pub fn new(index: u32) -> Self {
136 Self { index, _phantom: PhantomData }
137 }
138
139 #[inline]
140 pub fn is_nil(self) -> bool { self.index == NIL_INDEX }
141}
142
143#[inline]
144fn pack(counter: u32, index: u32) -> u64 {
145 ((counter as u64) << 32) | (index as u64)
146}
147#[inline]
148fn unpack(v: u64) -> (u32, u32) {
149 ((v >> 32) as u32, v as u32)
150}
151
152pub struct SharedRegion<T: Copy + 'static> {
153 _file: File,
154 mmap: MmapMut,
155 capacity: usize,
156 next_offset: usize,
157 slots_offset: usize,
158 _phantom: PhantomData<T>,
159 header_sidecar: subetha_core::HandshakeHeader,
160 ring_sidecar: Box<subetha_core::ObservationRing>,
161}
162
163unsafe impl<T: Copy + Send + 'static> Send for SharedRegion<T> {}
164unsafe impl<T: Copy + Sync + 'static> Sync for SharedRegion<T> {}
165
166impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedRegion<T> {
167 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
168 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
169 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
170 Box::new(subetha_sidecar::NoMigrationPolicy)
171 }
172}
173
174impl<T: Copy + 'static> SharedRegion<T> {
175 pub fn create(
182 path: impl AsRef<Path>, capacity: usize,
183 ) -> Result<Self, RegionError> {
184 assert!(capacity >= 1);
185 assert!(capacity < NIL_INDEX as usize, "capacity must be < u32::MAX");
186 let (file, mut mmap) = crate::mmf_attach::create_or_attach(
187 path.as_ref(),
188 region_file_size(capacity, size_of::<T>()),
189 |ptr| unsafe { Self::init_region(ptr, capacity) },
190 |ptr| unsafe { (*(ptr as *const RegionHeader)).magic == REGION_MAGIC },
191 )?;
192 crate::mmf_warm::warm_mmap(&mut mmap);
193 Self::from_region(file, mmap, capacity)
194 }
195
196 pub fn reset(
200 path: impl AsRef<Path>, capacity: usize,
201 ) -> Result<Self, RegionError> {
202 assert!(capacity >= 1);
203 assert!(capacity < NIL_INDEX as usize, "capacity must be < u32::MAX");
204 let (file, mut mmap) = crate::mmf_attach::reset(
205 path.as_ref(),
206 region_file_size(capacity, size_of::<T>()),
207 |ptr| unsafe { Self::init_region(ptr, capacity) },
208 )?;
209 crate::mmf_warm::warm_mmap(&mut mmap);
210 Self::from_region(file, mmap, capacity)
211 }
212
213 unsafe fn init_region(ptr: *mut u8, capacity: usize) {
221 let hdr = ptr as *mut RegionHeader;
222 unsafe {
223 (*hdr).capacity = capacity as u32;
224 (*hdr).slot_size = size_of::<T>() as u32;
225 std::ptr::write(&raw mut (*hdr).free_head, AtomicU64::new(pack(0, NIL_INDEX)));
226 std::ptr::write_volatile(&raw mut (*hdr).magic, REGION_MAGIC);
227 }
228 }
229
230 fn from_region(
233 file: File,
234 mmap: MmapMut,
235 capacity: usize,
236 ) -> Result<Self, RegionError> {
237 let hdr = unsafe { &*(mmap.as_ptr() as *const RegionHeader) };
238 if hdr.magic != REGION_MAGIC
239 || hdr.capacity != capacity as u32
240 || hdr.slot_size != size_of::<T>() as u32
241 {
242 return Err(RegionError::LayoutMismatch);
243 }
244 let next_offset = size_of::<RegionHeader>();
245 let slots_offset = next_offset + capacity * size_of::<AtomicU32>();
246 Ok(Self {
247 _file: file, mmap, capacity, next_offset, slots_offset,
248 _phantom: PhantomData,
249 header_sidecar: subetha_core::HandshakeHeader::new(),
250 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
251 })
252 }
253
254 pub fn open(
255 path: impl AsRef<Path>, expected_capacity: usize,
256 ) -> Result<Self, RegionError> {
257 let total = region_file_size(expected_capacity, size_of::<T>());
258 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
259 if file.metadata()?.len() < total as u64 {
260 return Err(RegionError::LayoutMismatch);
261 }
262 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
263 crate::mmf_warm::warm_mmap(&mut mmap);
264 Self::from_region(file, mmap, expected_capacity)
265 }
266
267 #[inline]
268 pub fn capacity(&self) -> usize { self.capacity }
269
270 #[inline]
275 pub fn mmap_ptr(&self) -> *const u8 { self.mmap.as_ptr() }
276
277 fn header(&self) -> &RegionHeader {
278 unsafe { &*(self.mmap.as_ptr() as *const RegionHeader) }
279 }
280
281 fn next_link(&self, idx: usize) -> &AtomicU32 {
282 let base = unsafe { self.mmap.as_ptr().add(self.next_offset) };
283 unsafe { &*(base.add(idx * size_of::<AtomicU32>()) as *const AtomicU32) }
284 }
285
286 fn slot_ptr(&self, idx: usize) -> *mut T {
287 let base = unsafe { self.mmap.as_ptr().add(self.slots_offset) };
288 unsafe { base.add(idx * size_of::<T>()) as *mut T }
289 }
290
291 pub fn len(&self) -> usize {
295 let bump = self.header().bump_next.load(Ordering::Acquire) as usize;
296 bump.saturating_sub(self.free_count())
297 }
298
299 pub fn is_empty(&self) -> bool { self.len() == 0 }
300
301 pub fn free_count(&self) -> usize {
304 let mut count = 0usize;
305 let (_, mut idx) = unpack(self.header().free_head.load(Ordering::Acquire));
306 let mut visited = 0;
307 while idx != NIL_INDEX && (visited as usize) < self.capacity {
308 count += 1;
309 visited += 1;
310 idx = self.next_link(idx as usize).load(Ordering::Acquire);
311 }
312 count
313 }
314
315 pub fn allocate(&self, value: T) -> Result<OffsetPtr<T>, RegionError> {
318 loop {
320 let head = self.header().free_head.load(Ordering::Acquire);
321 let (counter, idx) = unpack(head);
322 if idx == NIL_INDEX { break; } let next_idx = self.next_link(idx as usize).load(Ordering::Acquire);
324 let new_head = pack(counter.wrapping_add(1), next_idx);
325 if self.header().free_head.compare_exchange(
326 head, new_head, Ordering::AcqRel, Ordering::Acquire,
327 ).is_ok() {
328 unsafe { std::ptr::write(self.slot_ptr(idx as usize), value); }
329 self.ring_sidecar
330 .push_op(crate::sidecar_ops::region::OP_ALLOCATE, 0);
331 return Ok(OffsetPtr::new(idx));
332 }
333 }
335 let idx = self.header().bump_next.fetch_add(1, Ordering::AcqRel);
337 if idx >= self.capacity as u64 {
338 self.header().bump_next.fetch_sub(1, Ordering::AcqRel);
339 self.ring_sidecar
340 .push_op(crate::sidecar_ops::region::OP_ALLOCATE, 1); return Err(RegionError::Full);
342 }
343 let idx = idx as u32;
344 unsafe { std::ptr::write(self.slot_ptr(idx as usize), value); }
345 self.ring_sidecar
346 .push_op(crate::sidecar_ops::region::OP_ALLOCATE, 0);
347 Ok(OffsetPtr::new(idx))
348 }
349
350 pub fn free(&self, ptr: OffsetPtr<T>) -> Result<T, RegionError> {
353 if ptr.is_nil() || (ptr.index as usize) >= self.capacity {
354 self.ring_sidecar
355 .push_op(crate::sidecar_ops::region::OP_FREE, 1); return Err(RegionError::InvalidPtr);
357 }
358 let value = unsafe { std::ptr::read(self.slot_ptr(ptr.index as usize)) };
359 loop {
360 let head = self.header().free_head.load(Ordering::Acquire);
361 let (counter, old_top) = unpack(head);
362 self.next_link(ptr.index as usize).store(old_top, Ordering::Release);
363 let new_head = pack(counter.wrapping_add(1), ptr.index);
364 if self.header().free_head.compare_exchange(
365 head, new_head, Ordering::AcqRel, Ordering::Acquire,
366 ).is_ok() {
367 self.ring_sidecar
368 .push_op(crate::sidecar_ops::region::OP_FREE, 0);
369 return Ok(value);
370 }
371 }
373 }
374
375 pub fn get(&self, ptr: OffsetPtr<T>) -> Result<T, RegionError> {
380 if ptr.is_nil() || (ptr.index as usize) >= self.capacity {
381 self.ring_sidecar
382 .push_op(crate::sidecar_ops::region::OP_GET, 1); return Err(RegionError::InvalidPtr);
384 }
385 let v = unsafe { std::ptr::read(self.slot_ptr(ptr.index as usize)) };
386 self.ring_sidecar
387 .push_op(crate::sidecar_ops::region::OP_GET, 0);
388 Ok(v)
389 }
390
391 pub fn set(&self, ptr: OffsetPtr<T>, value: T) -> Result<(), RegionError> {
394 if ptr.is_nil() || (ptr.index as usize) >= self.capacity {
395 self.ring_sidecar
396 .push_op(crate::sidecar_ops::region::OP_SET, 1); return Err(RegionError::InvalidPtr);
398 }
399 unsafe { std::ptr::write(self.slot_ptr(ptr.index as usize), value); }
400 self.ring_sidecar
401 .push_op(crate::sidecar_ops::region::OP_SET, 0);
402 Ok(())
403 }
404
405 pub fn clear(&self) {
414 self.header().bump_next.store(0, Ordering::Release);
415 self.header().free_head.store(pack(0, NIL_INDEX), Ordering::Release);
416 }
417
418 pub fn flush(&self) -> Result<(), RegionError> {
419 self.mmap.flush()?;
420 Ok(())
421 }
422
423 pub fn flush_async(&self) -> Result<(), RegionError> {
427 self.mmap.flush_async()?;
428 Ok(())
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435 use std::sync::Arc;
436 use std::thread;
437
438 fn tmp(name: &str) -> std::path::PathBuf {
439 let mut p = std::env::temp_dir();
440 let pid = std::process::id();
441 p.push(format!("subetha-region-{name}-{pid}.bin"));
442 p
443 }
444
445 #[test]
446 fn create_initial_state_is_empty() {
447 let p = tmp("init");
448 let r: SharedRegion<u64> = SharedRegion::create(&p, 16).unwrap();
449 assert_eq!(r.capacity(), 16);
450 assert_eq!(r.len(), 0);
451 assert!(r.is_empty());
452 assert_eq!(r.free_count(), 0);
453 std::fs::remove_file(&p).ok();
454 }
455
456 #[test]
459 fn second_create_attaches_and_keeps_allocations() {
460 let p = tmp("attach");
461 std::fs::remove_file(&p).ok();
462 let r: SharedRegion<u64> = SharedRegion::create(&p, 16).unwrap();
463 let ptr = r.allocate(777).unwrap();
464
465 let r2: SharedRegion<u64> = SharedRegion::create(&p, 16).unwrap();
466 assert_eq!(r2.get(ptr), Ok(777), "attach lost an allocation");
467 assert!(matches!(
468 SharedRegion::<u64>::create(&p, 8),
469 Err(RegionError::LayoutMismatch),
470 ));
471
472 drop(r);
475 drop(r2);
476 let fresh: SharedRegion<u64> = SharedRegion::reset(&p, 16).unwrap();
477 assert!(fresh.is_empty(), "reset kept an allocation");
478 drop(fresh);
479 std::fs::remove_file(&p).ok();
480 }
481
482 #[test]
483 fn allocate_and_get_round_trip() {
484 let p = tmp("rt");
485 let r: SharedRegion<u64> = SharedRegion::create(&p, 8).unwrap();
486 let p1 = r.allocate(100).unwrap();
487 let p2 = r.allocate(200).unwrap();
488 let p3 = r.allocate(300).unwrap();
489 assert_eq!(p1.index, 0);
490 assert_eq!(p2.index, 1);
491 assert_eq!(p3.index, 2);
492 assert_eq!(r.get(p1).unwrap(), 100);
493 assert_eq!(r.get(p2).unwrap(), 200);
494 assert_eq!(r.get(p3).unwrap(), 300);
495 assert_eq!(r.len(), 3);
496 std::fs::remove_file(&p).ok();
497 }
498
499 #[test]
500 fn full_region_returns_error_on_bump() {
501 let p = tmp("full");
502 let r: SharedRegion<u32> = SharedRegion::create(&p, 4).unwrap();
503 for i in 0..4u32 { r.allocate(i).unwrap(); }
504 assert_eq!(r.allocate(99).err(), Some(RegionError::Full));
505 assert_eq!(r.len(), 4); std::fs::remove_file(&p).ok();
507 }
508
509 #[test]
510 fn free_returns_value_and_decrements_len() {
511 let p = tmp("free");
512 let r: SharedRegion<u64> = SharedRegion::create(&p, 8).unwrap();
513 let ptr1 = r.allocate(111).unwrap();
514 let ptr2 = r.allocate(222).unwrap();
515 assert_eq!(r.len(), 2);
516 let v = r.free(ptr1).unwrap();
517 assert_eq!(v, 111);
518 assert_eq!(r.len(), 1);
519 assert_eq!(r.free_count(), 1);
520 assert_eq!(r.get(ptr2).unwrap(), 222);
522 std::fs::remove_file(&p).ok();
523 }
524
525 #[test]
526 fn free_then_allocate_reuses_slot() {
527 let p = tmp("reuse");
528 let r: SharedRegion<u64> = SharedRegion::create(&p, 8).unwrap();
529 let p1 = r.allocate(100).unwrap();
530 let _p2 = r.allocate(200).unwrap();
531 r.free(p1).unwrap();
532 let p3 = r.allocate(999).unwrap();
534 assert_eq!(p3.index, p1.index, "free list should have returned slot 0");
535 assert_eq!(r.get(p3).unwrap(), 999);
536 std::fs::remove_file(&p).ok();
537 }
538
539 #[test]
540 fn free_invalid_ptr_returns_error() {
541 let p = tmp("invalid");
542 let r: SharedRegion<u64> = SharedRegion::create(&p, 4).unwrap();
543 assert_eq!(r.free(OffsetPtr::NIL).err(), Some(RegionError::InvalidPtr));
544 let oob = OffsetPtr::<u64>::new(100);
545 assert_eq!(r.free(oob).err(), Some(RegionError::InvalidPtr));
546 std::fs::remove_file(&p).ok();
547 }
548
549 #[test]
550 fn set_overwrites_value_in_place() {
551 let p = tmp("set");
552 let r: SharedRegion<u64> = SharedRegion::create(&p, 4).unwrap();
553 let ptr = r.allocate(42).unwrap();
554 r.set(ptr, 999).unwrap();
555 assert_eq!(r.get(ptr).unwrap(), 999);
556 std::fs::remove_file(&p).ok();
557 }
558
559 #[test]
560 fn cross_handle_visibility() {
561 let p = tmp("cross-handle");
562 let writer: SharedRegion<u64> = SharedRegion::create(&p, 16).unwrap();
563 let reader: SharedRegion<u64> = SharedRegion::open(&p, 16).unwrap();
564 let ptr = writer.allocate(7777).unwrap();
565 assert_eq!(reader.get(ptr).unwrap(), 7777);
566 writer.set(ptr, 8888).unwrap();
567 assert_eq!(reader.get(ptr).unwrap(), 8888);
568 std::fs::remove_file(&p).ok();
569 }
570
571 #[test]
572 fn concurrent_allocations_get_distinct_indices() {
573 let p = tmp("concurrent");
574 let r: Arc<SharedRegion<u64>> = Arc::new(SharedRegion::create(&p, 1024).unwrap());
575 let n_threads = 4;
576 let per_thread = 100;
577 let mut handles = vec![];
578 for t in 0..n_threads as u64 {
579 let r = r.clone();
580 handles.push(thread::spawn(move || {
581 let mut ptrs = vec![];
582 for i in 0..per_thread as u64 {
583 let v = t * 1000 + i;
584 let ptr = r.allocate(v).unwrap();
585 ptrs.push((v, ptr));
586 }
587 ptrs
588 }));
589 }
590 let all: Vec<(u64, OffsetPtr<u64>)> = handles.into_iter()
591 .flat_map(|h| h.join().unwrap()).collect();
592
593 let mut indices: Vec<u32> = all.iter().map(|(_, p)| p.index).collect();
595 indices.sort();
596 for w in indices.windows(2) {
597 assert_ne!(w[0], w[1], "two threads got the same slot index");
598 }
599 for (expected, ptr) in &all {
601 assert_eq!(r.get(*ptr).unwrap(), *expected);
602 }
603 std::fs::remove_file(&p).ok();
604 }
605
606 #[test]
607 fn concurrent_free_and_realloc_no_corruption() {
608 let p = tmp("free-realloc");
609 let r: Arc<SharedRegion<u64>> = Arc::new(SharedRegion::create(&p, 64).unwrap());
610 let initial: Vec<OffsetPtr<u64>> = (0..64u64)
612 .map(|i| r.allocate(i * 10).unwrap()).collect();
613 let r_a = r.clone();
614 let _init = initial.clone();
615 let freer = thread::spawn(move || {
617 for (i, ptr) in _init.iter().enumerate() {
618 if i % 2 == 0 { r_a.free(*ptr).ok(); }
619 }
620 });
621 let r_b = r.clone();
623 let alloc = thread::spawn(move || {
624 let mut new_ptrs = vec![];
625 for i in 100u64..132 {
626 if let Ok(p) = r_b.allocate(i) { new_ptrs.push((i, p)); }
627 }
628 new_ptrs
629 });
630 freer.join().unwrap();
631 let new_ptrs = alloc.join().unwrap();
632 for (val, ptr) in new_ptrs {
634 assert_eq!(r.get(ptr).unwrap(), val);
635 }
636 std::fs::remove_file(&p).ok();
637 }
638
639 #[test]
640 fn offset_ptr_is_position_independent() {
641 let p = tmp("position-indep");
644 let writer: SharedRegion<u64> = SharedRegion::create(&p, 4).unwrap();
645 let ptr = writer.allocate(0xCAFE_BABE).unwrap();
646 let reader: SharedRegion<u64> = SharedRegion::open(&p, 4).unwrap();
647 let same_ptr = OffsetPtr::<u64>::new(ptr.index);
649 assert_eq!(reader.get(same_ptr).unwrap(), 0xCAFE_BABE);
650 std::fs::remove_file(&p).ok();
651 }
652
653 #[test]
654 fn struct_payload_round_trip() {
655 #[derive(Clone, Copy, Debug, PartialEq)]
656 #[repr(C)]
657 struct Node { left: u32, right: u32, key: u64 }
658 let p = tmp("struct");
659 let r: SharedRegion<Node> = SharedRegion::create(&p, 16).unwrap();
660 let n = Node { left: 1, right: 2, key: 42 };
661 let ptr = r.allocate(n).unwrap();
662 assert_eq!(r.get(ptr).unwrap(), n);
663 std::fs::remove_file(&p).ok();
664 }
665
666 #[test]
667 fn nil_ptr_round_trips() {
668 let p: OffsetPtr<u64> = OffsetPtr::NIL;
669 assert!(p.is_nil());
670 assert_eq!(p.index, NIL_INDEX);
671 }
672
673 #[test]
674 fn offset_ptr_equality_and_hash() {
675 use std::collections::HashSet;
676 let a: OffsetPtr<u64> = OffsetPtr::new(5);
677 let b: OffsetPtr<u64> = OffsetPtr::new(5);
678 let c: OffsetPtr<u64> = OffsetPtr::new(6);
679 assert_eq!(a, b);
680 assert_ne!(a, c);
681 let mut s = HashSet::new();
682 s.insert(a);
683 assert!(s.contains(&b));
684 assert!(!s.contains(&c));
685 }
686
687 #[test]
688 fn disk_persistence_survives_reopen() {
689 let p = tmp("disk");
690 let saved_ptr_index;
691 {
692 let r: SharedRegion<u64> = SharedRegion::create(&p, 16).unwrap();
693 let p1 = r.allocate(1111).unwrap();
694 let _p2 = r.allocate(2222).unwrap();
695 r.flush().unwrap();
696 saved_ptr_index = p1.index;
697 }
698 let r2: SharedRegion<u64> = SharedRegion::open(&p, 16).unwrap();
699 let restored = OffsetPtr::<u64>::new(saved_ptr_index);
700 assert_eq!(r2.get(restored).unwrap(), 1111);
701 let p3 = r2.allocate(3333).unwrap();
703 assert_eq!(p3.index, 2);
704 assert_eq!(r2.get(p3).unwrap(), 3333);
705 std::fs::remove_file(&p).ok();
706 }
707}