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> {
87 Self::check_layout()?;
88 let file = OpenOptions::new()
89 .read(true).write(true).create(true).truncate(true)
90 .open(path.as_ref())?;
91 file.set_len(CELL_FILE_SIZE as u64)?;
92 let mut mmap = unsafe { MmapOptions::new().len(CELL_FILE_SIZE).map_mut(&file)? };
93 let ptr = mmap.as_mut_ptr() as *mut CellHeader;
94 unsafe {
95 std::ptr::write(ptr, CellHeader {
96 magic: CELL_MAGIC,
97 size: size_of::<T>() as u32,
98 version: AtomicU32::new(0),
99 _pad_to_payload: 0,
100 payload: [0; PAYLOAD_BYTES],
101 });
102 }
103 Ok(Self {
104 _file: file, mmap, _phantom: PhantomData,
105 header_sidecar: subetha_core::HandshakeHeader::new(),
106 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
107 })
108 }
109
110 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedCellError> {
111 Self::check_layout()?;
112 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
113 if file.metadata()?.len() < CELL_FILE_SIZE as u64 {
114 return Err(SharedCellError::LayoutMismatch);
115 }
116 let mmap = unsafe { MmapOptions::new().len(CELL_FILE_SIZE).map_mut(&file)? };
117 let header = unsafe { &*(mmap.as_ptr() as *const CellHeader) };
118 if header.magic != CELL_MAGIC || header.size as usize != size_of::<T>() {
119 return Err(SharedCellError::LayoutMismatch);
120 }
121 Ok(Self {
122 _file: file, mmap, _phantom: PhantomData,
123 header_sidecar: subetha_core::HandshakeHeader::new(),
124 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
125 })
126 }
127
128 fn check_layout() -> Result<(), SharedCellError> {
129 if size_of::<T>() > PAYLOAD_BYTES {
130 return Err(SharedCellError::PayloadTooLarge);
131 }
132 if align_of::<T>() > 8 {
133 return Err(SharedCellError::PayloadTooLarge);
134 }
135 Ok(())
136 }
137
138 fn header(&self) -> &CellHeader {
139 unsafe { &*(self.mmap.as_ptr() as *const CellHeader) }
140 }
141
142 pub fn set(&self, value: T) {
145 let header = self.header();
146 let v_old = header.version.fetch_add(1, Ordering::AcqRel);
148 debug_assert!(v_old & 1 == 0, "concurrent writers not supported on SharedCell");
149 unsafe {
152 let dst = header.payload.as_ptr() as *mut T;
153 std::ptr::write_unaligned(dst, value);
154 }
155 header.version.fetch_add(1, Ordering::Release);
157 self.ring_sidecar
158 .push_op(crate::sidecar_ops::cell::OP_SET, 0);
159 }
160
161 pub fn get(&self) -> T {
164 let header = self.header();
165 let mut retries: u32 = 0;
166 loop {
167 let v1 = header.version.load(Ordering::Acquire);
168 if v1 & 1 != 0 {
169 retries = retries.saturating_add(1);
170 std::hint::spin_loop();
171 continue;
172 }
173 let value: T = unsafe {
176 let src = header.payload.as_ptr() as *const T;
177 std::ptr::read_unaligned(src)
178 };
179 let v2 = header.version.load(Ordering::Acquire);
180 if v1 == v2 {
181 self.ring_sidecar.push_op(
182 crate::sidecar_ops::cell::OP_GET,
183 if retries > 0 { 1 } else { 0 },
184 );
185 return value;
186 }
187 retries = retries.saturating_add(1);
189 std::hint::spin_loop();
190 }
191 }
192
193 pub fn version(&self) -> u32 {
194 self.header().version.load(Ordering::Acquire)
195 }
196
197 pub fn flush_async(&self) -> Result<(), SharedCellError> {
201 self.mmap.flush_async()?;
202 Ok(())
203 }
204
205 pub fn flush(&self) -> Result<(), SharedCellError> {
206 self.mmap.flush()?;
207 Ok(())
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 fn tmp(name: &str) -> std::path::PathBuf {
216 let mut p = std::env::temp_dir();
217 let pid = std::process::id();
218 p.push(format!("subetha-cell-{name}-{pid}.bin"));
219 p
220 }
221
222 #[test]
223 fn round_trip_simple_payload() {
224 let p = tmp("round-trip");
225 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
226 c.set(42);
227 assert_eq!(c.get(), 42);
228 c.set(99);
229 assert_eq!(c.get(), 99);
230 std::fs::remove_file(&p).ok();
231 }
232
233 #[test]
234 fn cross_handle_visibility() {
235 let p = tmp("cross-handle");
236 let writer: SharedCell<u64> = SharedCell::create(&p).unwrap();
237 let reader: SharedCell<u64> = SharedCell::open(&p).unwrap();
238 writer.set(0xDEAD_BEEF);
239 assert_eq!(reader.get(), 0xDEAD_BEEF);
240 std::fs::remove_file(&p).ok();
241 }
242
243 #[test]
244 fn version_advances_on_each_set() {
245 let p = tmp("version");
246 let c: SharedCell<u32> = SharedCell::create(&p).unwrap();
247 let v0 = c.version();
248 c.set(1);
249 let v1 = c.version();
250 c.set(2);
251 let v2 = c.version();
252 assert_eq!(v1, v0 + 2);
254 assert_eq!(v2, v0 + 4);
255 std::fs::remove_file(&p).ok();
256 }
257
258 #[test]
259 fn disk_persistence_survives_reopen() {
260 let p = tmp("disk-persist");
261 {
262 let c: SharedCell<u64> = SharedCell::create(&p).unwrap();
263 c.set(7777);
264 c.flush().unwrap();
265 }
266 let c2: SharedCell<u64> = SharedCell::open(&p).unwrap();
267 assert_eq!(c2.get(), 7777);
268 std::fs::remove_file(&p).ok();
269 }
270
271 #[test]
272 fn open_rejects_wrong_payload_size() {
273 let p = tmp("wrong-size");
274 let _c: SharedCell<u64> = SharedCell::create(&p).unwrap();
275 match SharedCell::<u32>::open(&p) {
276 Err(SharedCellError::LayoutMismatch) => {}
277 other => panic!("expected LayoutMismatch, got {:?}", other.as_ref().err()),
278 }
279 std::fs::remove_file(&p).ok();
280 }
281
282 #[test]
283 fn struct_payload_round_trip() {
284 #[derive(Clone, Copy, Debug, PartialEq)]
285 #[repr(C)]
286 struct Point { x: f64, y: f64, z: f64 }
287 let p = tmp("struct");
288 let c: SharedCell<Point> = SharedCell::create(&p).unwrap();
289 let pt = Point { x: 1.0, y: 2.0, z: 3.0 };
290 c.set(pt);
291 assert_eq!(c.get(), pt);
292 std::fs::remove_file(&p).ok();
293 }
294
295 #[test]
296 fn concurrent_readers_during_writes() {
297 use std::sync::Arc;
298 use std::thread;
299 let p = tmp("concurrent-rw");
300 let c: Arc<SharedCell<u64>> = Arc::new(SharedCell::create(&p).unwrap());
301 c.set(0);
302 let writer_c = c.clone();
303 let writer = thread::spawn(move || {
304 for i in 1..1000u64 {
305 writer_c.set(i);
306 }
307 999u64
308 });
309 let mut handles = vec![];
310 for _ in 0..4 {
311 let reader_c = c.clone();
312 handles.push(thread::spawn(move || {
313 let mut last = 0u64;
314 for _ in 0..1000 {
315 let v = reader_c.get();
316 assert!(v >= last, "torn read detected: v={v} last={last}");
318 last = v;
319 }
320 }));
321 }
322 let final_w = writer.join().unwrap();
323 for h in handles { h.join().unwrap(); }
324 assert!(c.get() >= final_w);
325 std::fs::remove_file(&p).ok();
326 }
327
328 #[test]
329 fn payload_too_large_at_create() {
330 #[allow(dead_code)] struct Big([u8; PAYLOAD_BYTES + 1]);
332 impl Copy for Big {}
333 impl Clone for Big { fn clone(&self) -> Self { *self } }
334 let p = tmp("too-large");
335 match SharedCell::<Big>::create(&p) {
336 Err(SharedCellError::PayloadTooLarge) => {}
337 other => panic!("expected PayloadTooLarge, got {:?}", other.as_ref().err()),
338 }
339 std::fs::remove_file(&p).ok();
340 }
341}