Skip to main content

subetha_cxc/
shared_atomic.rs

1//! `SharedAtomic<T>` - cross-process atomic counter / flag.
2//!
3//! Backed by an MMF cell whose payload is interpreted directly as
4//! `AtomicU8 / AtomicU16 / AtomicU32 / AtomicU64`. The atomic ops
5//! are cross-process safe on every modern CPU because hardware
6//! cache coherence guarantees the atomic semantics across address
7//! spaces; the only requirement is that both processes map the
8//! same physical page (which the OS guarantees when they open the
9//! same MMF file).
10//!
11//! Three concrete types:
12//! - `SharedAtomicU32`
13//! - `SharedAtomicU64`
14//! - `SharedAtomicBool` (one byte, but enforced bool semantics)
15//!
16//! Type-erased layout: header + payload region the size of the
17//! native atomic, aligned naturally.
18
19use 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,  // 1, 4, or 8 bytes
31    payload_u64: AtomicU64,  // also covers u32, u8 via punning
32}
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            /// Obtain the atomic at `path`, initializing it to `init` if
68            /// the path does not yet exist and attaching to it if it
69            /// does. Attaching leaves the live value in place; `init` is
70            /// then unused. A region built for a different width is a
71            /// `LayoutMismatch`. [`reset`](Self::reset) reinitializes.
72            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            /// Truncate the atomic at `path` and initialize it to
83            /// `init`, discarding the value a live peer holds. For a
84            /// caller that knows it owns the path.
85            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            /// Lay out a fresh atomic: width and value first, magic
95            /// last, because attachers spin on it.
96            ///
97            /// # Safety
98            /// `ptr` addresses at least `ATOMIC_FILE_SIZE` writable
99            /// zeroed bytes.
100            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            /// Wrap an initialized region, refusing one built for a
111            /// different width.
112            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            /// Non-blocking flush: schedules a writeback via the OS
222            /// (msync(MS_ASYNC) on Linux; FlushViewOfFile without
223            /// FlushFileBuffers on Windows). Note: Windows is only
224            /// partially async (sync to page cache, not to disk).
225            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    /// Non-blocking flush: schedules a writeback via the OS.
326    /// Note: Windows is only partially async (sync to page cache,
327    /// not to disk).
328    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    /// A second create attaches with the live value in place, ignoring
365    /// its init argument; reset is what re-seeds.
366    #[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        // Windows refuses to truncate a mapped file, so every handle goes
377        // before the reset.
378        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    /// Attaching with a different width is refused.
387    #[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}