subetha_cxc/
shared_cell.rs1use std::fs::{File, OpenOptions};
33use std::marker::PhantomData;
34use std::mem::{align_of, size_of};
35use std::path::Path;
36use std::sync::atomic::{AtomicU32, Ordering};
37
38use memmap2::{MmapMut, MmapOptions};
39
40pub const CELL_MAGIC: u32 = 0x4350_4D46;
41pub const PAYLOAD_BYTES: usize = 52;
42
43#[repr(C, align(64))]
44pub struct CellHeader {
45 pub magic: u32,
46 pub size: u32,
47 pub version: AtomicU32,
48 pub _pad_to_payload: u32,
49 pub payload: [u8; PAYLOAD_BYTES],
50}
51
52pub const CELL_FILE_SIZE: usize = size_of::<CellHeader>();
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SharedCellError {
56 LayoutMismatch,
57 PayloadTooLarge,
58 NotInitialised,
59 IoError(std::io::ErrorKind),
60}
61
62impl From<std::io::Error> for SharedCellError {
63 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
64}
65
66pub struct SharedCell<T: Copy + 'static> {
67 _file: File,
68 mmap: MmapMut,
69 _phantom: PhantomData<T>,
70 header_sidecar: subetha_core::HandshakeHeader,
71 ring_sidecar: Box<subetha_core::ObservationRing>,
72}
73
74unsafe impl<T: Copy + Send + 'static> Send for SharedCell<T> {}
75unsafe impl<T: Copy + Sync + 'static> Sync for SharedCell<T> {}
76
77impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedCell<T> {
78 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
79 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
80 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
81 Box::new(subetha_sidecar::NoMigrationPolicy)
82 }
83}
84
85impl<T: Copy + 'static> SharedCell<T> {
86 pub fn create(path: impl AsRef<Path>) -> Result<Self, SharedCellError> {
92 Self::check_layout()?;
93 let (file, mmap) = crate::mmf_attach::create_or_attach(
94 path.as_ref(),
95 CELL_FILE_SIZE,
96 |ptr| unsafe { Self::init_region(ptr) },
97 |ptr| unsafe { (*(ptr as *const CellHeader)).magic == CELL_MAGIC },
98 )?;
99 Self::from_region(file, mmap)
100 }
101
102 pub fn reset(path: impl AsRef<Path>) -> Result<Self, SharedCellError> {
106 Self::check_layout()?;
107 let (file, mmap) = crate::mmf_attach::reset(path.as_ref(), CELL_FILE_SIZE, |ptr| unsafe {
108 Self::init_region(ptr)
109 })?;
110 Self::from_region(file, mmap)
111 }
112
113 unsafe fn init_region(ptr: *mut u8) {
121 let hdr = ptr as *mut CellHeader;
122 unsafe {
123 (*hdr).size = size_of::<T>() as u32;
124 std::ptr::write_volatile(&raw mut (*hdr).magic, CELL_MAGIC);
125 }
126 }
127
128 fn from_region(file: File, mmap: MmapMut) -> Result<Self, SharedCellError> {
131 let header = unsafe { &*(mmap.as_ptr() as *const CellHeader) };
132 if header.magic != CELL_MAGIC || header.size as usize != size_of::<T>() {
133 return Err(SharedCellError::LayoutMismatch);
134 }
135 Ok(Self {
136 _file: file, mmap, _phantom: PhantomData,
137 header_sidecar: subetha_core::HandshakeHeader::new(),
138 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
139 })
140 }
141
142 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedCellError> {
143 Self::check_layout()?;
144 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
145 if file.metadata()?.len() < CELL_FILE_SIZE as u64 {
146 return Err(SharedCellError::LayoutMismatch);
147 }
148 let mmap = unsafe { MmapOptions::new().len(CELL_FILE_SIZE).map_mut(&file)? };
149 Self::from_region(file, mmap)
150 }
151
152 fn check_layout() -> Result<(), SharedCellError> {
153 if size_of::<T>() > PAYLOAD_BYTES {
154 return Err(SharedCellError::PayloadTooLarge);
155 }
156 if align_of::<T>() > 8 {
157 return Err(SharedCellError::PayloadTooLarge);
158 }
159 Ok(())
160 }
161
162 fn header(&self) -> &CellHeader {
163 unsafe { &*(self.mmap.as_ptr() as *const CellHeader) }
164 }
165
166 pub fn set(&self, value: T) {
169 let header = self.header();
170 let v_old = header.version.fetch_add(1, Ordering::AcqRel);
172 debug_assert!(v_old & 1 == 0, "concurrent writers not supported on SharedCell");
173 unsafe {
176 let dst = header.payload.as_ptr() as *mut T;
177 std::ptr::write_unaligned(dst, value);
178 }
179 header.version.fetch_add(1, Ordering::Release);
181 self.ring_sidecar
182 .push_op(crate::sidecar_ops::cell::OP_SET, 0);
183 }
184
185 pub fn get(&self) -> T {
188 let header = self.header();
189 let mut retries: u32 = 0;
190 loop {
191 let v1 = header.version.load(Ordering::Acquire);
192 if v1 & 1 != 0 {
193 retries = retries.saturating_add(1);
194 std::hint::spin_loop();
195 continue;
196 }
197 let value: T = unsafe {
200 let src = header.payload.as_ptr() as *const T;
201 std::ptr::read_unaligned(src)
202 };
203 let v2 = header.version.load(Ordering::Acquire);
204 if v1 == v2 {
205 self.ring_sidecar.push_op(
206 crate::sidecar_ops::cell::OP_GET,
207 if retries > 0 { 1 } else { 0 },
208 );
209 return value;
210 }
211 retries = retries.saturating_add(1);
213 std::hint::spin_loop();
214 }
215 }
216
217 pub fn version(&self) -> u32 {
218 self.header().version.load(Ordering::Acquire)
219 }
220
221 pub fn flush_async(&self) -> Result<(), SharedCellError> {
225 self.mmap.flush_async()?;
226 Ok(())
227 }
228
229 pub fn flush(&self) -> Result<(), SharedCellError> {
230 self.mmap.flush()?;
231 Ok(())
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 fn tmp(name: &str) -> std::path::PathBuf {
240 let mut p = std::env::temp_dir();
241 let pid = std::process::id();
242 p.push(format!("subetha-cell-{name}-{pid}.bin"));
243 p
244 }
245
246 #[test]
247 fn round_trip_simple_payload() {
248 let p = tmp("round-trip");
249 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
250 c.set(42);
251 assert_eq!(c.get(), 42);
252 c.set(99);
253 assert_eq!(c.get(), 99);
254 std::fs::remove_file(&p).ok();
255 }
256
257 #[test]
260 fn second_create_attaches_and_keeps_the_value() {
261 let p = tmp("attach");
262 std::fs::remove_file(&p).ok();
263 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
264 c.set(777);
265
266 let c2: SharedCell<u64> = SharedCell::create(&p).unwrap();
267 assert_eq!(c2.get(), 777, "attach clobbered the value");
268
269 drop(c);
272 drop(c2);
273 let fresh: SharedCell<u64> = SharedCell::reset(&p).unwrap();
274 assert_eq!(fresh.get(), 0, "reset left a value behind");
275 drop(fresh);
276 std::fs::remove_file(&p).ok();
277 }
278
279 #[test]
281 fn create_refuses_a_mismatched_region() {
282 let p = tmp("mismatch");
283 std::fs::remove_file(&p).ok();
284 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
285 assert!(matches!(
286 SharedCell::<u32>::create(&p),
287 Err(SharedCellError::LayoutMismatch),
288 ));
289 drop(c);
290 std::fs::remove_file(&p).ok();
291 }
292
293 #[test]
294 fn cross_handle_visibility() {
295 let p = tmp("cross-handle");
296 let writer: SharedCell<u64> = SharedCell::create(&p).unwrap();
297 let reader: SharedCell<u64> = SharedCell::open(&p).unwrap();
298 writer.set(0xDEAD_BEEF);
299 assert_eq!(reader.get(), 0xDEAD_BEEF);
300 std::fs::remove_file(&p).ok();
301 }
302
303 #[test]
304 fn version_advances_on_each_set() {
305 let p = tmp("version");
306 let c: SharedCell<u32> = SharedCell::create(&p).unwrap();
307 let v0 = c.version();
308 c.set(1);
309 let v1 = c.version();
310 c.set(2);
311 let v2 = c.version();
312 assert_eq!(v1, v0 + 2);
314 assert_eq!(v2, v0 + 4);
315 std::fs::remove_file(&p).ok();
316 }
317
318 #[test]
319 fn disk_persistence_survives_reopen() {
320 let p = tmp("disk-persist");
321 {
322 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
323 c.set(7777);
324 c.flush().unwrap();
325 }
326 let c2: SharedCell<u64> = SharedCell::open(&p).unwrap();
327 assert_eq!(c2.get(), 7777);
328 std::fs::remove_file(&p).ok();
329 }
330
331 #[test]
332 fn open_rejects_wrong_payload_size() {
333 let p = tmp("wrong-size");
334 let _c: SharedCell<u64> = SharedCell::create(&p).unwrap();
335 match SharedCell::<u32>::open(&p) {
336 Err(SharedCellError::LayoutMismatch) => {}
337 other => panic!("expected LayoutMismatch, got {:?}", other.as_ref().err()),
338 }
339 std::fs::remove_file(&p).ok();
340 }
341
342 #[test]
343 fn struct_payload_round_trip() {
344 #[derive(Clone, Copy, Debug, PartialEq)]
345 #[repr(C)]
346 struct Point { x: f64, y: f64, z: f64 }
347 let p = tmp("struct");
348 let c: SharedCell<Point> = SharedCell::create(&p).unwrap();
349 let pt = Point { x: 1.0, y: 2.0, z: 3.0 };
350 c.set(pt);
351 assert_eq!(c.get(), pt);
352 std::fs::remove_file(&p).ok();
353 }
354
355 #[test]
356 fn concurrent_readers_during_writes() {
357 use std::sync::Arc;
358 use std::thread;
359 let p = tmp("concurrent-rw");
360 let c: Arc<SharedCell<u64>> = Arc::new(SharedCell::create(&p).unwrap());
361 c.set(0);
362 let writer_c = c.clone();
363 let writer = thread::spawn(move || {
364 for i in 1..1000u64 {
365 writer_c.set(i);
366 }
367 999u64
368 });
369 let mut handles = vec![];
370 for _ in 0..4 {
371 let reader_c = c.clone();
372 handles.push(thread::spawn(move || {
373 let mut last = 0u64;
374 for _ in 0..1000 {
375 let v = reader_c.get();
376 assert!(v >= last, "torn read detected: v={v} last={last}");
378 last = v;
379 }
380 }));
381 }
382 let final_w = writer.join().unwrap();
383 for h in handles { h.join().unwrap(); }
384 assert!(c.get() >= final_w);
385 std::fs::remove_file(&p).ok();
386 }
387
388 #[test]
389 fn payload_too_large_at_create() {
390 #[allow(dead_code)] struct Big([u8; PAYLOAD_BYTES + 1]);
392 impl Copy for Big {}
393 impl Clone for Big { fn clone(&self) -> Self { *self } }
394 let p = tmp("too-large");
395 match SharedCell::<Big>::create(&p) {
396 Err(SharedCellError::PayloadTooLarge) => {}
397 other => panic!("expected PayloadTooLarge, got {:?}", other.as_ref().err()),
398 }
399 std::fs::remove_file(&p).ok();
400 }
401}