1use std::fs::{File, OpenOptions};
20use std::path::Path;
21use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
22
23use memmap2::{MmapMut, MmapOptions};
24
25pub const ATOMIC_MAGIC: u32 = 0x4150_5443;
26
27#[repr(C, align(64))]
28struct AtomicHeader {
29 magic: u32,
30 width: u32, payload_u64: AtomicU64, }
33
34const ATOMIC_FILE_SIZE: usize = std::mem::size_of::<AtomicHeader>();
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum SharedAtomicError {
38 LayoutMismatch,
39 IoError(std::io::ErrorKind),
40}
41
42impl From<std::io::Error> for SharedAtomicError {
43 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
44}
45
46macro_rules! shared_atomic_impl {
47 ($name:ident, $atomic:ty, $native:ty, $width:expr) => {
48 pub struct $name {
49 _file: File,
50 mmap: MmapMut,
51 header_sidecar: subetha_core::HandshakeHeader,
52 ring_sidecar: Box<subetha_core::ObservationRing>,
53 }
54
55 unsafe impl Send for $name {}
56 unsafe impl Sync for $name {}
57
58 impl subetha_sidecar::AdaptiveInstance for $name {
59 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
60 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
61 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
62 Box::new(subetha_sidecar::NoMigrationPolicy)
63 }
64 }
65
66 impl $name {
67 pub fn create(path: impl AsRef<Path>, init: $native) -> Result<Self, SharedAtomicError> {
68 let file = OpenOptions::new()
69 .read(true).write(true).create(true).truncate(true)
70 .open(path.as_ref())?;
71 file.set_len(ATOMIC_FILE_SIZE as u64)?;
72 let mut mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
73 let hdr = mmap.as_mut_ptr() as *mut AtomicHeader;
74 unsafe {
75 std::ptr::write(hdr, AtomicHeader {
76 magic: ATOMIC_MAGIC,
77 width: $width,
78 payload_u64: AtomicU64::new(0),
79 });
80 }
81 let s = Self {
82 _file: file, mmap,
83 header_sidecar: subetha_core::HandshakeHeader::new(),
84 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
85 };
86 s.atomic().store(init, Ordering::Release);
87 Ok(s)
88 }
89
90 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedAtomicError> {
91 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
92 if file.metadata()?.len() < ATOMIC_FILE_SIZE as u64 {
93 return Err(SharedAtomicError::LayoutMismatch);
94 }
95 let mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
96 let hdr = unsafe { &*(mmap.as_ptr() as *const AtomicHeader) };
97 if hdr.magic != ATOMIC_MAGIC || hdr.width != $width {
98 return Err(SharedAtomicError::LayoutMismatch);
99 }
100 Ok(Self {
101 _file: file, mmap,
102 header_sidecar: subetha_core::HandshakeHeader::new(),
103 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
104 })
105 }
106
107 #[inline]
108 fn atomic(&self) -> &$atomic {
109 let base = unsafe {
110 self.mmap.as_ptr()
111 .add(std::mem::offset_of!(AtomicHeader, payload_u64))
112 };
113 unsafe { &*(base as *const $atomic) }
114 }
115
116 #[inline]
117 pub fn load(&self, ord: Ordering) -> $native {
118 let v = self.atomic().load(ord);
119 self.ring_sidecar
120 .push_op(crate::sidecar_ops::atomic::OP_LOAD, 0);
121 v
122 }
123
124 #[inline]
125 pub fn store(&self, v: $native, ord: Ordering) {
126 self.atomic().store(v, ord);
127 self.ring_sidecar
128 .push_op(crate::sidecar_ops::atomic::OP_STORE, 0);
129 }
130
131 #[inline]
132 pub fn fetch_add(&self, v: $native, ord: Ordering) -> $native {
133 let prev = self.atomic().fetch_add(v, ord);
134 self.ring_sidecar
135 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
136 prev
137 }
138
139 #[inline]
140 pub fn fetch_sub(&self, v: $native, ord: Ordering) -> $native {
141 let prev = self.atomic().fetch_sub(v, ord);
142 self.ring_sidecar
143 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
144 prev
145 }
146
147 #[inline]
148 pub fn fetch_or(&self, v: $native, ord: Ordering) -> $native {
149 let prev = self.atomic().fetch_or(v, ord);
150 self.ring_sidecar
151 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
152 prev
153 }
154
155 #[inline]
156 pub fn fetch_and(&self, v: $native, ord: Ordering) -> $native {
157 let prev = self.atomic().fetch_and(v, ord);
158 self.ring_sidecar
159 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
160 prev
161 }
162
163 #[inline]
164 pub fn fetch_xor(&self, v: $native, ord: Ordering) -> $native {
165 let prev = self.atomic().fetch_xor(v, ord);
166 self.ring_sidecar
167 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
168 prev
169 }
170
171 #[inline]
172 pub fn swap(&self, v: $native, ord: Ordering) -> $native {
173 let prev = self.atomic().swap(v, ord);
174 self.ring_sidecar
175 .push_op(crate::sidecar_ops::atomic::OP_CAS, 0);
176 prev
177 }
178
179 #[inline]
180 pub fn compare_exchange(
181 &self, current: $native, new: $native,
182 success: Ordering, failure: Ordering,
183 ) -> Result<$native, $native> {
184 let r = self.atomic().compare_exchange(current, new, success, failure);
185 self.ring_sidecar
186 .push_op(crate::sidecar_ops::atomic::OP_CAS, if r.is_err() { 1 } else { 0 });
187 r
188 }
189
190 pub fn flush(&self) -> Result<(), SharedAtomicError> {
191 self.mmap.flush()?;
192 Ok(())
193 }
194
195 pub fn flush_async(&self) -> Result<(), SharedAtomicError> {
200 self.mmap.flush_async()?;
201 Ok(())
202 }
203 }
204 };
205}
206
207shared_atomic_impl!(SharedAtomicU32, AtomicU32, u32, 4);
208shared_atomic_impl!(SharedAtomicU64, AtomicU64, u64, 8);
209
210pub struct SharedAtomicBool {
211 _file: File,
212 mmap: MmapMut,
213 header_sidecar: subetha_core::HandshakeHeader,
214 ring_sidecar: Box<subetha_core::ObservationRing>,
215}
216
217unsafe impl Send for SharedAtomicBool {}
218unsafe impl Sync for SharedAtomicBool {}
219
220impl subetha_sidecar::AdaptiveInstance for SharedAtomicBool {
221 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
222 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
223 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
224 Box::new(subetha_sidecar::NoMigrationPolicy)
225 }
226}
227
228impl SharedAtomicBool {
229 pub fn create(path: impl AsRef<Path>, init: bool) -> Result<Self, SharedAtomicError> {
230 let file = OpenOptions::new()
231 .read(true).write(true).create(true).truncate(true)
232 .open(path.as_ref())?;
233 file.set_len(ATOMIC_FILE_SIZE as u64)?;
234 let mut mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
235 let hdr = mmap.as_mut_ptr() as *mut AtomicHeader;
236 unsafe {
237 std::ptr::write(hdr, AtomicHeader {
238 magic: ATOMIC_MAGIC,
239 width: 1,
240 payload_u64: AtomicU64::new(0),
241 });
242 }
243 let s = Self {
244 _file: file, mmap,
245 header_sidecar: subetha_core::HandshakeHeader::new(),
246 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
247 };
248 s.atomic().store(init, Ordering::Release);
249 Ok(s)
250 }
251
252 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedAtomicError> {
253 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
254 if file.metadata()?.len() < ATOMIC_FILE_SIZE as u64 {
255 return Err(SharedAtomicError::LayoutMismatch);
256 }
257 let mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
258 let hdr = unsafe { &*(mmap.as_ptr() as *const AtomicHeader) };
259 if hdr.magic != ATOMIC_MAGIC || hdr.width != 1 {
260 return Err(SharedAtomicError::LayoutMismatch);
261 }
262 Ok(Self {
263 _file: file, mmap,
264 header_sidecar: subetha_core::HandshakeHeader::new(),
265 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
266 })
267 }
268
269 fn atomic(&self) -> &AtomicBool {
270 let base = unsafe {
271 self.mmap.as_ptr().add(std::mem::offset_of!(AtomicHeader, payload_u64))
272 };
273 unsafe { &*(base as *const AtomicBool) }
274 }
275
276 pub fn load(&self, ord: Ordering) -> bool {
277 let v = self.atomic().load(ord);
278 self.ring_sidecar
279 .push_op(crate::sidecar_ops::atomic::OP_LOAD, 0);
280 v
281 }
282 pub fn store(&self, v: bool, ord: Ordering) {
283 self.atomic().store(v, ord);
284 self.ring_sidecar
285 .push_op(crate::sidecar_ops::atomic::OP_STORE, 0);
286 }
287 pub fn swap(&self, v: bool, ord: Ordering) -> bool {
288 let prev = self.atomic().swap(v, ord);
289 self.ring_sidecar
290 .push_op(crate::sidecar_ops::atomic::OP_CAS, 0);
291 prev
292 }
293
294 pub fn flush(&self) -> Result<(), SharedAtomicError> {
295 self.mmap.flush()?;
296 Ok(())
297 }
298
299 pub fn flush_async(&self) -> Result<(), SharedAtomicError> {
303 self.mmap.flush_async()?;
304 Ok(())
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn tmp(name: &str) -> std::path::PathBuf {
313 let mut p = std::env::temp_dir();
314 let pid = std::process::id();
315 p.push(format!("subetha-atomic-{name}-{pid}.bin"));
316 p
317 }
318
319 #[test]
320 fn u64_load_store_round_trip() {
321 let p = tmp("u64-rt");
322 let a = SharedAtomicU64::create(&p, 42).unwrap();
323 assert_eq!(a.load(Ordering::Acquire), 42);
324 a.store(99, Ordering::Release);
325 assert_eq!(a.load(Ordering::Acquire), 99);
326 std::fs::remove_file(&p).ok();
327 }
328
329 #[test]
330 fn u32_fetch_add_increments() {
331 let p = tmp("u32-add");
332 let a = SharedAtomicU32::create(&p, 0).unwrap();
333 for _ in 0..100 { a.fetch_add(1, Ordering::AcqRel); }
334 assert_eq!(a.load(Ordering::Acquire), 100);
335 std::fs::remove_file(&p).ok();
336 }
337
338 #[test]
339 fn cross_handle_visibility() {
340 let p = tmp("cross-handle");
341 let writer = SharedAtomicU64::create(&p, 0).unwrap();
342 let reader = SharedAtomicU64::open(&p).unwrap();
343 writer.store(7777, Ordering::Release);
344 assert_eq!(reader.load(Ordering::Acquire), 7777);
345 std::fs::remove_file(&p).ok();
346 }
347
348 #[test]
349 fn concurrent_fetch_add_sums_correctly() {
350 use std::sync::Arc;
351 use std::thread;
352 let p = tmp("concurrent");
353 let a = Arc::new(SharedAtomicU64::create(&p, 0).unwrap());
354 let mut handles = vec![];
355 for _ in 0..8 {
356 let a = a.clone();
357 handles.push(thread::spawn(move || {
358 for _ in 0..1000 { a.fetch_add(1, Ordering::AcqRel); }
359 }));
360 }
361 for h in handles { h.join().unwrap(); }
362 assert_eq!(a.load(Ordering::Acquire), 8000);
363 std::fs::remove_file(&p).ok();
364 }
365
366 #[test]
367 fn compare_exchange_wins_once() {
368 let p = tmp("cas");
369 let a = SharedAtomicU64::create(&p, 5).unwrap();
370 let r1 = a.compare_exchange(5, 10, Ordering::AcqRel, Ordering::Acquire);
371 let r2 = a.compare_exchange(5, 20, Ordering::AcqRel, Ordering::Acquire);
372 assert_eq!(r1, Ok(5));
373 assert_eq!(r2, Err(10));
374 assert_eq!(a.load(Ordering::Acquire), 10);
375 std::fs::remove_file(&p).ok();
376 }
377
378 #[test]
379 fn bool_load_store_swap() {
380 let p = tmp("bool");
381 let b = SharedAtomicBool::create(&p, false).unwrap();
382 assert!(!b.load(Ordering::Acquire));
383 b.store(true, Ordering::Release);
384 assert!(b.load(Ordering::Acquire));
385 let prev = b.swap(false, Ordering::AcqRel);
386 assert!(prev);
387 assert!(!b.load(Ordering::Acquire));
388 std::fs::remove_file(&p).ok();
389 }
390
391 #[test]
392 fn disk_persistence_survives_reopen() {
393 let p = tmp("disk-persist");
394 {
395 let a = SharedAtomicU64::create(&p, 12345).unwrap();
396 a.flush().unwrap();
397 }
398 let a2 = SharedAtomicU64::open(&p).unwrap();
399 assert_eq!(a2.load(Ordering::Acquire), 12345);
400 std::fs::remove_file(&p).ok();
401 }
402
403 #[test]
404 fn open_rejects_wrong_width() {
405 let p = tmp("wrong-width");
406 let _a = SharedAtomicU64::create(&p, 0).unwrap();
407 match SharedAtomicU32::open(&p) {
408 Err(SharedAtomicError::LayoutMismatch) => {}
409 other => panic!("expected LayoutMismatch, got {:?}", other.as_ref().err()),
410 }
411 std::fs::remove_file(&p).ok();
412 }
413}