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> {
73 let (file, mmap) = crate::mmf_attach::create_or_attach(
74 path.as_ref(),
75 ATOMIC_FILE_SIZE,
76 |ptr| unsafe { Self::init_region(ptr, init) },
77 |ptr| unsafe { (*(ptr as *const AtomicHeader)).magic == ATOMIC_MAGIC },
78 )?;
79 Self::from_region(file, mmap)
80 }
81
82 pub fn reset(path: impl AsRef<Path>, init: $native) -> Result<Self, SharedAtomicError> {
86 let (file, mmap) = crate::mmf_attach::reset(
87 path.as_ref(),
88 ATOMIC_FILE_SIZE,
89 |ptr| unsafe { Self::init_region(ptr, init) },
90 )?;
91 Self::from_region(file, mmap)
92 }
93
94 unsafe fn init_region(ptr: *mut u8, init: $native) {
101 let hdr = ptr as *mut AtomicHeader;
102 unsafe {
103 (*hdr).width = $width;
104 let payload = (&raw mut (*hdr).payload_u64) as *mut $atomic;
105 std::ptr::write(payload, <$atomic>::new(init));
106 std::ptr::write_volatile(&raw mut (*hdr).magic, ATOMIC_MAGIC);
107 }
108 }
109
110 fn from_region(file: File, mmap: MmapMut) -> Result<Self, SharedAtomicError> {
113 let hdr = unsafe { &*(mmap.as_ptr() as *const AtomicHeader) };
114 if hdr.magic != ATOMIC_MAGIC || hdr.width != $width {
115 return Err(SharedAtomicError::LayoutMismatch);
116 }
117 Ok(Self {
118 _file: file, mmap,
119 header_sidecar: subetha_core::HandshakeHeader::new(),
120 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
121 })
122 }
123
124 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedAtomicError> {
125 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
126 if file.metadata()?.len() < ATOMIC_FILE_SIZE as u64 {
127 return Err(SharedAtomicError::LayoutMismatch);
128 }
129 let mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
130 Self::from_region(file, mmap)
131 }
132
133 #[inline]
134 fn atomic(&self) -> &$atomic {
135 let base = unsafe {
136 self.mmap.as_ptr()
137 .add(std::mem::offset_of!(AtomicHeader, payload_u64))
138 };
139 unsafe { &*(base as *const $atomic) }
140 }
141
142 #[inline]
143 pub fn load(&self, ord: Ordering) -> $native {
144 let v = self.atomic().load(ord);
145 self.ring_sidecar
146 .push_op(crate::sidecar_ops::atomic::OP_LOAD, 0);
147 v
148 }
149
150 #[inline]
151 pub fn store(&self, v: $native, ord: Ordering) {
152 self.atomic().store(v, ord);
153 self.ring_sidecar
154 .push_op(crate::sidecar_ops::atomic::OP_STORE, 0);
155 }
156
157 #[inline]
158 pub fn fetch_add(&self, v: $native, ord: Ordering) -> $native {
159 let prev = self.atomic().fetch_add(v, ord);
160 self.ring_sidecar
161 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
162 prev
163 }
164
165 #[inline]
166 pub fn fetch_sub(&self, v: $native, ord: Ordering) -> $native {
167 let prev = self.atomic().fetch_sub(v, ord);
168 self.ring_sidecar
169 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
170 prev
171 }
172
173 #[inline]
174 pub fn fetch_or(&self, v: $native, ord: Ordering) -> $native {
175 let prev = self.atomic().fetch_or(v, ord);
176 self.ring_sidecar
177 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
178 prev
179 }
180
181 #[inline]
182 pub fn fetch_and(&self, v: $native, ord: Ordering) -> $native {
183 let prev = self.atomic().fetch_and(v, ord);
184 self.ring_sidecar
185 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
186 prev
187 }
188
189 #[inline]
190 pub fn fetch_xor(&self, v: $native, ord: Ordering) -> $native {
191 let prev = self.atomic().fetch_xor(v, ord);
192 self.ring_sidecar
193 .push_op(crate::sidecar_ops::atomic::OP_FETCH_ADD, 0);
194 prev
195 }
196
197 #[inline]
198 pub fn swap(&self, v: $native, ord: Ordering) -> $native {
199 let prev = self.atomic().swap(v, ord);
200 self.ring_sidecar
201 .push_op(crate::sidecar_ops::atomic::OP_CAS, 0);
202 prev
203 }
204
205 #[inline]
206 pub fn compare_exchange(
207 &self, current: $native, new: $native,
208 success: Ordering, failure: Ordering,
209 ) -> Result<$native, $native> {
210 let r = self.atomic().compare_exchange(current, new, success, failure);
211 self.ring_sidecar
212 .push_op(crate::sidecar_ops::atomic::OP_CAS, if r.is_err() { 1 } else { 0 });
213 r
214 }
215
216 pub fn flush(&self) -> Result<(), SharedAtomicError> {
217 self.mmap.flush()?;
218 Ok(())
219 }
220
221 pub fn flush_async(&self) -> Result<(), SharedAtomicError> {
226 self.mmap.flush_async()?;
227 Ok(())
228 }
229 }
230 };
231}
232
233shared_atomic_impl!(SharedAtomicU32, AtomicU32, u32, 4);
234shared_atomic_impl!(SharedAtomicU64, AtomicU64, u64, 8);
235
236pub struct SharedAtomicBool {
237 _file: File,
238 mmap: MmapMut,
239 header_sidecar: subetha_core::HandshakeHeader,
240 ring_sidecar: Box<subetha_core::ObservationRing>,
241}
242
243unsafe impl Send for SharedAtomicBool {}
244unsafe impl Sync for SharedAtomicBool {}
245
246impl subetha_sidecar::AdaptiveInstance for SharedAtomicBool {
247 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
248 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
249 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
250 Box::new(subetha_sidecar::NoMigrationPolicy)
251 }
252}
253
254impl SharedAtomicBool {
255 pub fn create(path: impl AsRef<Path>, init: bool) -> Result<Self, SharedAtomicError> {
256 let file = OpenOptions::new()
257 .read(true).write(true).create(true).truncate(true)
258 .open(path.as_ref())?;
259 file.set_len(ATOMIC_FILE_SIZE as u64)?;
260 let mut mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
261 let hdr = mmap.as_mut_ptr() as *mut AtomicHeader;
262 unsafe {
263 std::ptr::write(hdr, AtomicHeader {
264 magic: ATOMIC_MAGIC,
265 width: 1,
266 payload_u64: AtomicU64::new(0),
267 });
268 }
269 let s = Self {
270 _file: file, mmap,
271 header_sidecar: subetha_core::HandshakeHeader::new(),
272 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
273 };
274 s.atomic().store(init, Ordering::Release);
275 Ok(s)
276 }
277
278 pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedAtomicError> {
279 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
280 if file.metadata()?.len() < ATOMIC_FILE_SIZE as u64 {
281 return Err(SharedAtomicError::LayoutMismatch);
282 }
283 let mmap = unsafe { MmapOptions::new().len(ATOMIC_FILE_SIZE).map_mut(&file)? };
284 let hdr = unsafe { &*(mmap.as_ptr() as *const AtomicHeader) };
285 if hdr.magic != ATOMIC_MAGIC || hdr.width != 1 {
286 return Err(SharedAtomicError::LayoutMismatch);
287 }
288 Ok(Self {
289 _file: file, mmap,
290 header_sidecar: subetha_core::HandshakeHeader::new(),
291 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
292 })
293 }
294
295 fn atomic(&self) -> &AtomicBool {
296 let base = unsafe {
297 self.mmap.as_ptr().add(std::mem::offset_of!(AtomicHeader, payload_u64))
298 };
299 unsafe { &*(base as *const AtomicBool) }
300 }
301
302 pub fn load(&self, ord: Ordering) -> bool {
303 let v = self.atomic().load(ord);
304 self.ring_sidecar
305 .push_op(crate::sidecar_ops::atomic::OP_LOAD, 0);
306 v
307 }
308 pub fn store(&self, v: bool, ord: Ordering) {
309 self.atomic().store(v, ord);
310 self.ring_sidecar
311 .push_op(crate::sidecar_ops::atomic::OP_STORE, 0);
312 }
313 pub fn swap(&self, v: bool, ord: Ordering) -> bool {
314 let prev = self.atomic().swap(v, ord);
315 self.ring_sidecar
316 .push_op(crate::sidecar_ops::atomic::OP_CAS, 0);
317 prev
318 }
319
320 pub fn flush(&self) -> Result<(), SharedAtomicError> {
321 self.mmap.flush()?;
322 Ok(())
323 }
324
325 pub fn flush_async(&self) -> Result<(), SharedAtomicError> {
329 self.mmap.flush_async()?;
330 Ok(())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 fn tmp(name: &str) -> std::path::PathBuf {
339 let mut p = std::env::temp_dir();
340 let pid = std::process::id();
341 p.push(format!("subetha-atomic-{name}-{pid}.bin"));
342 p
343 }
344
345 #[test]
346 fn u64_load_store_round_trip() {
347 let p = tmp("u64-rt");
348 let a = SharedAtomicU64::create(&p, 42).unwrap();
349 assert_eq!(a.load(Ordering::Acquire), 42);
350 a.store(99, Ordering::Release);
351 assert_eq!(a.load(Ordering::Acquire), 99);
352 std::fs::remove_file(&p).ok();
353 }
354
355 #[test]
356 fn u32_fetch_add_increments() {
357 let p = tmp("u32-add");
358 let a = SharedAtomicU32::create(&p, 0).unwrap();
359 for _ in 0..100 { a.fetch_add(1, Ordering::AcqRel); }
360 assert_eq!(a.load(Ordering::Acquire), 100);
361 std::fs::remove_file(&p).ok();
362 }
363
364 #[test]
367 fn second_create_attaches_and_keeps_the_value() {
368 let p = tmp("attach");
369 std::fs::remove_file(&p).ok();
370 let a = SharedAtomicU64::create(&p, 42).unwrap();
371 a.store(777, Ordering::Release);
372
373 let b = SharedAtomicU64::create(&p, 0).unwrap();
374 assert_eq!(b.load(Ordering::Acquire), 777, "attach clobbered the value");
375
376 drop(a);
379 drop(b);
380 let fresh = SharedAtomicU64::reset(&p, 5).unwrap();
381 assert_eq!(fresh.load(Ordering::Acquire), 5, "reset did not re-seed");
382 drop(fresh);
383 std::fs::remove_file(&p).ok();
384 }
385
386 #[test]
388 fn create_refuses_a_mismatched_width() {
389 let p = tmp("mismatch");
390 std::fs::remove_file(&p).ok();
391 let a = SharedAtomicU64::create(&p, 1).unwrap();
392 assert!(matches!(
393 SharedAtomicU32::create(&p, 1),
394 Err(SharedAtomicError::LayoutMismatch),
395 ));
396 drop(a);
397 std::fs::remove_file(&p).ok();
398 }
399
400 #[test]
401 fn cross_handle_visibility() {
402 let p = tmp("cross-handle");
403 let writer = SharedAtomicU64::create(&p, 0).unwrap();
404 let reader = SharedAtomicU64::open(&p).unwrap();
405 writer.store(7777, Ordering::Release);
406 assert_eq!(reader.load(Ordering::Acquire), 7777);
407 std::fs::remove_file(&p).ok();
408 }
409
410 #[test]
411 fn concurrent_fetch_add_sums_correctly() {
412 use std::sync::Arc;
413 use std::thread;
414 let p = tmp("concurrent");
415 let a = Arc::new(SharedAtomicU64::create(&p, 0).unwrap());
416 let mut handles = vec![];
417 for _ in 0..8 {
418 let a = a.clone();
419 handles.push(thread::spawn(move || {
420 for _ in 0..1000 { a.fetch_add(1, Ordering::AcqRel); }
421 }));
422 }
423 for h in handles { h.join().unwrap(); }
424 assert_eq!(a.load(Ordering::Acquire), 8000);
425 std::fs::remove_file(&p).ok();
426 }
427
428 #[test]
429 fn compare_exchange_wins_once() {
430 let p = tmp("cas");
431 let a = SharedAtomicU64::create(&p, 5).unwrap();
432 let r1 = a.compare_exchange(5, 10, Ordering::AcqRel, Ordering::Acquire);
433 let r2 = a.compare_exchange(5, 20, Ordering::AcqRel, Ordering::Acquire);
434 assert_eq!(r1, Ok(5));
435 assert_eq!(r2, Err(10));
436 assert_eq!(a.load(Ordering::Acquire), 10);
437 std::fs::remove_file(&p).ok();
438 }
439
440 #[test]
441 fn bool_load_store_swap() {
442 let p = tmp("bool");
443 let b = SharedAtomicBool::create(&p, false).unwrap();
444 assert!(!b.load(Ordering::Acquire));
445 b.store(true, Ordering::Release);
446 assert!(b.load(Ordering::Acquire));
447 let prev = b.swap(false, Ordering::AcqRel);
448 assert!(prev);
449 assert!(!b.load(Ordering::Acquire));
450 std::fs::remove_file(&p).ok();
451 }
452
453 #[test]
454 fn disk_persistence_survives_reopen() {
455 let p = tmp("disk-persist");
456 {
457 let a = SharedAtomicU64::create(&p, 12345).unwrap();
458 a.flush().unwrap();
459 }
460 let a2 = SharedAtomicU64::open(&p).unwrap();
461 assert_eq!(a2.load(Ordering::Acquire), 12345);
462 std::fs::remove_file(&p).ok();
463 }
464
465 #[test]
466 fn open_rejects_wrong_width() {
467 let p = tmp("wrong-width");
468 let _a = SharedAtomicU64::create(&p, 0).unwrap();
469 match SharedAtomicU32::open(&p) {
470 Err(SharedAtomicError::LayoutMismatch) => {}
471 other => panic!("expected LayoutMismatch, got {:?}", other.as_ref().err()),
472 }
473 std::fs::remove_file(&p).ok();
474 }
475}