Skip to main content

subetha_cxc/
cross_process_waker.rs

1//! `CrossProcessWaker`: a futex-shaped wait/wake primitive sitting
2//! in shared memory (MMF or named-shm), portable across Linux /
3//! Windows / macOS / FreeBSD.
4//!
5//! # The problem
6//!
7//! SubEtha's bounded rings deliver bytes between threads / processes
8//! without any kernel involvement on the hot path. That's a win
9//! when the consumer can keep up - try_recv either returns an item
10//! or returns `Empty` and the caller decides what to do. The
11//! pattern breaks down when the consumer wants to BLOCK on an empty
12//! ring without spinning: there's no kernel-side handle to wait on,
13//! and a busy-wait burns one CPU per blocked consumer.
14//!
15//! `CrossProcessWaker` closes the gap. It's the userspace `futex`,
16//! ported to the substrate. The producer publishes a monotonic
17//! sequence atom on every push; a blocked consumer parks on a wake
18//! list in shared memory, registering the sequence it wants to be
19//! woken at. When the producer's sequence advances past that
20//! target, the producer's post-publish path fires a single
21//! syscall-level wake and the consumer's `wait` returns.
22//!
23//! # Cross-platform wake
24//!
25//! The primitive calls the platform's wait / wake syscalls
26//! directly (NOT via the `atomic-wait` crate, which hard-codes
27//! `FUTEX_PRIVATE_FLAG` on Linux and so cannot work across
28//! processes):
29//!
30//! - Linux / Android: `futex(FUTEX_WAIT)` / `futex(FUTEX_WAKE)`
31//!   without the PRIVATE flag - the kernel hashes by the page's
32//!   physical address so any process that mapped the same MMF
33//!   page joins the same wait queue.
34//! - FreeBSD: `_umtx_op(UMTX_OP_WAIT_UINT)` /
35//!   `_umtx_op(UMTX_OP_WAKE)` - the non-PRIVATE umtx ops, whose
36//!   sleep queues the kernel keys by PHYSICAL address exactly so
37//!   process-shared synchronization works (per `_umtx_op(2)`).
38//!   Same cross-process semantics as the Linux arm.
39//! - Windows: `WaitOnAddress` / `WakeByAddressSingle` for
40//!   process-private (anon-backed) wakers - those calls are
41//!   INTRA-PROCESS only per Microsoft's docs. Cross-process
42//!   (file / named-shm backed) wakers wait on the hardware
43//!   MONITOR tier instead (`crate::monitor_wait`): monitors are
44//!   physical-address based, so a store from another process to
45//!   the shared MMF line wakes the waiter - the platform's only
46//!   non-polling cross-process wake. On Windows hosts without
47//!   MONITORX/WAITPKG, cross-process waits fall back to the
48//!   wait-timeout + re-check recovery the blocking wrappers
49//!   already run.
50//! - macOS / other: polling fallback (correct, but wastes CPU
51//!   when idle).
52//!
53//! All shipping syscalls operate on a user-space address (no
54//! kernel handle bookkeeping per primitive), so the cross-process
55//! Linux case needs only that the waker's atomic lives in a
56//! mapping both processes have (named-shm or file-backed mmap).
57//! The kernel sees the same physical page from both sides and
58//! the wake reaches the parker.
59//!
60//! # Storage layout
61//!
62//! ```text
63//! +--------------------------------------+ offset 0
64//! | WakerHeader (64 bytes, one cache line)|
65//! |   magic: u64                          |
66//! |   capacity: u32                       |
67//! |   _pad                                |
68//! +--------------------------------------+ offset 64
69//! | WakerSlot[0] (64 bytes)              |
70//! |   state: AtomicU32 (FREE/PARKED/WOKEN)|
71//! |   _pad                                |
72//! |   target_seq: AtomicU64               |
73//! |   _pad                                |
74//! +--------------------------------------+
75//! | WakerSlot[1] ... WakerSlot[N-1]      |
76//! +--------------------------------------+
77//! ```
78//!
79//! Each slot is one cache line so producer's wake-scan and
80//! parker's state writes don't false-share across slots.
81//!
82//! # Wake protocol
83//!
84//! ## Consumer (parker) side
85//!
86//! 1. Scan slots for one with `state == FREE`.
87//! 2. CAS that slot's state from FREE to a transient RESERVED state.
88//! 3. Write `target_seq` (the sequence we want to be woken at).
89//! 4. Store state from RESERVED to PARKED with Release ordering -
90//!    this publishes the slot to producers and is the
91//!    happens-before edge for `target_seq`.
92//! 5. Call the platform's wait syscall on `&slot.state` with
93//!    expected = PARKED. The kernel verifies `state == PARKED`
94//!    before sleeping (Linux's futex_wait semantics; Windows'
95//!    WaitOnAddress likewise); if a producer's wake-CAS already
96//!    landed (state == WOKEN), wait returns immediately without
97//!    entering the kernel sleep path.
98//! 6. On return, store state back to FREE and release the slot.
99//!
100//! ## Producer (waker) side
101//!
102//! On every successful publish, call `wake_up_to(producer_seq)`.
103//! That scans slots:
104//!
105//! 1. Acquire-load `state`. If not PARKED, skip.
106//! 2. Relaxed-load `target_seq`. The Acquire on `state` acquired
107//!    the parker's Release-store, so prior writes (incl. target_seq)
108//!    are visible.
109//! 3. If `producer_seq >= target_seq`, CAS state from PARKED to
110//!    WOKEN. On CAS success, call the platform's wake-one syscall
111//!    on `&slot.state` and increment the wake counter.
112//!
113//! The CAS guards against a double-wake when multiple producers
114//! race to wake the same slot.
115//!
116//! # Wake-before-park race
117//!
118//! Between a blocked-recv's "try_recv returned Empty" check and
119//! its `try_park` call, a producer can publish AND call wake_up_to
120//! that finds zero parked slots. The standard recovery is the
121//! double-check in the blocking-recv wrapper: after parking,
122//! re-call try_recv before calling wait. If try_recv succeeds,
123//! release the token and return. Only if it still returns Empty
124//! does the consumer call wait.
125//!
126//! # Linux-futex-raw escape hatch
127//!
128//! The Cargo feature `linux-futex-raw` exposes the direct
129//! `libc::syscall(SYS_futex, ...)` surface to callers that need
130//! `FUTEX_WAIT_BITSET`, `FUTEX_REQUEUE`, or other ops the portable
131//! `atomic-wait` abstraction does not expose. Linux-only.
132
133use std::fs::{File, OpenOptions};
134use std::path::Path;
135use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
136use std::time::{Duration, Instant};
137
138use memmap2::{MmapMut, MmapOptions};
139
140/// Magic bytes identifying a CrossProcessWaker region. Used by
141/// `open` to reject the wrong kind of MMF.
142pub const WAKER_MAGIC: u64 = 0xE7E7_5742_4B45_5201; // "..WBKER.."
143
144/// Default slot capacity. Caller-overridable on construction.
145pub const MAX_WAITERS_DEFAULT: usize = 32;
146
147const STATE_FREE: u32 = 0;
148const STATE_RESERVED: u32 = 1;
149const STATE_PARKED: u32 = 2;
150const STATE_WOKEN: u32 = 3;
151
152#[repr(C, align(64))]
153struct WakerHeader {
154    magic: u64,
155    capacity: u32,
156    _pad0: [u8; 4],
157    /// One bit per slot index (< 64): set while the slot is
158    /// PARKED-ish. Producers' wake scans load this word first; a
159    /// zero mask makes the no-waiters case - the overwhelmingly
160    /// common one on a healthy ring - ONE cache line instead of
161    /// `capacity` slot lines. The bit is advisory: stale-set bits
162    /// are filtered by the per-slot state check, and a not-yet-set
163    /// bit is covered by the parker's pre-wait double-check, the
164    /// same race window the full scan always had. Capacities > 64
165    /// skip the mask and full-scan.
166    parked_mask: AtomicU64,
167    _pad: [u8; 64 - 24],
168}
169
170#[repr(C, align(64))]
171struct WakerSlot {
172    state: AtomicU32,
173    _pad1: [u8; 4],
174    target_seq: AtomicU64,
175    _pad2: [u8; 64 - 16],
176}
177
178const _: () = {
179    assert!(std::mem::size_of::<WakerHeader>() == 64);
180    assert!(std::mem::size_of::<WakerSlot>() == 64);
181};
182
183/// Errors returned by waker operations.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum WakerError {
186    /// All slots in use. Caller's fallback path: spin until the
187    /// thing they care about is ready (the same path they'd use
188    /// without a waker).
189    Full,
190    /// `wait()` returned because its timeout elapsed before any
191    /// producer fired a wake.
192    Timeout,
193    /// `open()` rejected the MMF because magic / capacity did
194    /// not match.
195    LayoutMismatch,
196    /// I/O error from the underlying mmap.
197    IoError(std::io::ErrorKind),
198}
199
200impl From<std::io::Error> for WakerError {
201    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
202}
203
204impl std::fmt::Display for WakerError {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            Self::Full => write!(f, "no free waker slot"),
208            Self::Timeout => write!(f, "wait timed out"),
209            Self::LayoutMismatch => write!(f, "waker layout mismatch on open"),
210            Self::IoError(k) => write!(f, "waker mmap io error: {k:?}"),
211        }
212    }
213}
214
215impl std::error::Error for WakerError {}
216
217/// Returned by `try_park`; identifies which slot the parker
218/// reserved. Pass back into `wait` and `release`.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct WakerToken {
221    slot: u32,
222}
223
224impl WakerToken {
225    /// The slot index this token is bound to. Useful for
226    /// debugging / instrumentation.
227    pub fn slot_index(&self) -> u32 { self.slot }
228}
229
230/// Bytes required for a waker region holding `capacity` slots.
231pub const fn waker_region_size(capacity: usize) -> usize {
232    std::mem::size_of::<WakerHeader>() + capacity * std::mem::size_of::<WakerSlot>()
233}
234
235/// Cross-process wake list. See module docs for the protocol.
236pub struct CrossProcessWaker {
237    _backing: WakerBacking,
238    raw_ptr: *mut u8,
239    capacity: usize,
240}
241
242unsafe impl Send for CrossProcessWaker {}
243unsafe impl Sync for CrossProcessWaker {}
244
245#[allow(dead_code)]
246enum WakerBacking {
247    Anon(MmapMut),
248    File(File, MmapMut),
249    Shm(crate::shm_file::ShmFile),
250}
251
252unsafe fn init_waker_layout_raw(ptr: *mut u8, capacity: usize) {
253    let hdr_ptr = ptr as *mut WakerHeader;
254    unsafe {
255        std::ptr::write_bytes(hdr_ptr as *mut u8, 0, std::mem::size_of::<WakerHeader>());
256        (*hdr_ptr).magic = WAKER_MAGIC;
257        (*hdr_ptr).capacity = capacity as u32;
258    }
259    let slots_base = unsafe { ptr.add(std::mem::size_of::<WakerHeader>()) };
260    for i in 0..capacity {
261        let slot_ptr = unsafe {
262            slots_base.add(i * std::mem::size_of::<WakerSlot>())
263        } as *mut WakerSlot;
264        unsafe {
265            std::ptr::write(slot_ptr, WakerSlot {
266                state: AtomicU32::new(STATE_FREE),
267                _pad1: [0; 4],
268                target_seq: AtomicU64::new(0),
269                _pad2: [0; 64 - 16],
270            });
271        }
272    }
273}
274
275impl CrossProcessWaker {
276    /// Anon (in-process) waker. Cross-thread only; for cross-
277    /// process use `create` (file) or `create_from_shm` (named).
278    pub fn create_anon(capacity: usize) -> Result<Self, WakerError> {
279        assert!(capacity >= 1, "capacity must be >= 1");
280        let total = waker_region_size(capacity);
281        let mut mmap = MmapOptions::new().len(total).map_anon()?;
282        let raw_ptr = mmap.as_mut_ptr();
283        unsafe { init_waker_layout_raw(raw_ptr, capacity); }
284        Ok(Self {
285            _backing: WakerBacking::Anon(mmap),
286            raw_ptr,
287            capacity,
288        })
289    }
290
291    /// File-backed waker, cross-process visible via the OS page cache.
292    /// Initialises the region only when `path` does not yet exist; otherwise
293    /// attaches, leaving parked waiters in place. A region built with a
294    /// different capacity is a `LayoutMismatch`. [`reset`](Self::reset)
295    /// reinitialises.
296    pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, WakerError> {
297        assert!(capacity >= 1, "capacity must be >= 1");
298        let total = waker_region_size(capacity);
299        let (file, mut mmap) = crate::mmf_attach::create_or_attach(
300            path.as_ref(),
301            total,
302            |ptr| unsafe { init_waker_layout_raw(ptr, capacity) },
303            |ptr| unsafe { (*(ptr as *const WakerHeader)).magic == WAKER_MAGIC },
304        )?;
305        let raw_ptr = mmap.as_mut_ptr();
306        let hdr = unsafe { &*(raw_ptr as *const WakerHeader) };
307        if hdr.capacity as usize != capacity {
308            return Err(WakerError::LayoutMismatch);
309        }
310        Ok(Self {
311            _backing: WakerBacking::File(file, mmap),
312            raw_ptr,
313            capacity,
314        })
315    }
316
317    /// Reinitialise the waker at `path`, discarding any parked waiters a live
318    /// peer holds. For a caller that knows it owns the path.
319    pub fn reset(path: impl AsRef<Path>, capacity: usize) -> Result<Self, WakerError> {
320        assert!(capacity >= 1, "capacity must be >= 1");
321        let total = waker_region_size(capacity);
322        let (file, mut mmap) = crate::mmf_attach::reset(path.as_ref(), total, |ptr| unsafe {
323            init_waker_layout_raw(ptr, capacity)
324        })?;
325        let raw_ptr = mmap.as_mut_ptr();
326        Ok(Self {
327            _backing: WakerBacking::File(file, mmap),
328            raw_ptr,
329            capacity,
330        })
331    }
332
333    /// Open an existing file-backed waker. Validates magic +
334    /// capacity.
335    pub fn open(
336        path: impl AsRef<Path>,
337        expected_capacity: usize,
338    ) -> Result<Self, WakerError> {
339        let total = waker_region_size(expected_capacity);
340        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
341        if (file.metadata()?.len() as usize) < total {
342            return Err(WakerError::LayoutMismatch);
343        }
344        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
345        let raw_ptr = mmap.as_mut_ptr();
346        let hdr = unsafe { &*(raw_ptr as *const WakerHeader) };
347        if hdr.magic != WAKER_MAGIC || hdr.capacity as usize != expected_capacity {
348            return Err(WakerError::LayoutMismatch);
349        }
350        Ok(Self {
351            _backing: WakerBacking::File(file, mmap),
352            raw_ptr,
353            capacity: expected_capacity,
354        })
355    }
356
357    /// Build a fresh waker on top of a named-shm region. Cross-
358    /// process visible via the `logical_name` of the underlying
359    /// [`ShmFile`](crate::shm_file::ShmFile); RAM-resident.
360    pub fn create_from_shm(
361        mut shm: crate::shm_file::ShmFile,
362        capacity: usize,
363    ) -> Result<Self, WakerError> {
364        assert!(capacity >= 1, "capacity must be >= 1");
365        let total = waker_region_size(capacity);
366        if shm.len() < total {
367            return Err(WakerError::LayoutMismatch);
368        }
369        let raw_ptr = shm.as_mut_slice().as_mut_ptr();
370        unsafe { init_waker_layout_raw(raw_ptr, capacity); }
371        Ok(Self {
372            _backing: WakerBacking::Shm(shm),
373            raw_ptr,
374            capacity,
375        })
376    }
377
378    /// Open an existing named-shm waker without re-initializing
379    /// the layout.
380    pub fn open_from_shm(
381        mut shm: crate::shm_file::ShmFile,
382        expected_capacity: usize,
383    ) -> Result<Self, WakerError> {
384        let total = waker_region_size(expected_capacity);
385        if shm.len() < total {
386            return Err(WakerError::LayoutMismatch);
387        }
388        let raw_ptr = shm.as_mut_slice().as_mut_ptr();
389        let hdr = unsafe { &*(raw_ptr as *const WakerHeader) };
390        if hdr.magic != WAKER_MAGIC || hdr.capacity as usize != expected_capacity {
391            return Err(WakerError::LayoutMismatch);
392        }
393        Ok(Self {
394            _backing: WakerBacking::Shm(shm),
395            raw_ptr,
396            capacity: expected_capacity,
397        })
398    }
399
400    /// Slot count fixed at construction.
401    pub fn capacity(&self) -> usize { self.capacity }
402
403    #[inline]
404    fn slot(&self, idx: usize) -> &WakerSlot {
405        let base = unsafe { self.raw_ptr.add(std::mem::size_of::<WakerHeader>()) };
406        unsafe { &*(base.add(idx * std::mem::size_of::<WakerSlot>()) as *const WakerSlot) }
407    }
408
409    /// Reserve a slot and park at `target_seq`. The caller will
410    /// be woken when some producer calls `wake_up_to(seq)` with
411    /// `seq >= target_seq`. Returns the token the caller passes
412    /// to `wait` / `release`.
413    ///
414    /// On `Err(Full)`, every slot is currently in use; the caller
415    /// falls back to spinning. This is the same fallback they'd
416    /// use without a waker at all.
417    pub fn try_park(&self, target_seq: u64) -> Result<WakerToken, WakerError> {
418        for idx in 0..self.capacity {
419            let slot = self.slot(idx);
420            // FREE -> RESERVED CAS. On success, this slot is
421            // ours; on failure, another parker beat us to it,
422            // try the next slot.
423            if slot
424                .state
425                .compare_exchange(
426                    STATE_FREE,
427                    STATE_RESERVED,
428                    Ordering::Acquire,
429                    Ordering::Relaxed,
430                )
431                .is_ok()
432            {
433                // Write target_seq before publishing as PARKED.
434                // The Release store on state below is the
435                // happens-before edge for this Relaxed store.
436                slot.target_seq.store(target_seq, Ordering::Relaxed);
437                // Publish the slot to producers.
438                slot.state.store(STATE_PARKED, Ordering::Release);
439                self.mask_set(idx);
440                return Ok(WakerToken { slot: idx as u32 });
441            }
442        }
443        Err(WakerError::Full)
444    }
445
446    /// Block until either some producer wakes this token or the
447    /// optional timeout elapses. After return (Ok or Err), the
448    /// token's slot is released; the caller does NOT need to
449    /// call `release` separately.
450    ///
451    /// If the slot's state was already transitioned to `WOKEN`
452    /// before the wait call entered the kernel, the wait returns
453    /// immediately (no kernel sleep).
454    /// Whether this waker's backing is reachable from other
455    /// processes (file / named-shm) rather than process-private
456    /// anonymous memory. Windows' wait ladder branches on this:
457    /// `WaitOnAddress` never receives a cross-process wake, so
458    /// cross-process-backed waits stay on the hardware monitor
459    /// tier (physical-address based) for their whole duration.
460    fn is_cross_process(&self) -> bool {
461        !matches!(self._backing, WakerBacking::Anon(_))
462    }
463
464    fn header(&self) -> &WakerHeader {
465        unsafe { &*(self.raw_ptr as *const WakerHeader) }
466    }
467
468    #[inline]
469    fn mask_set(&self, idx: usize) {
470        if idx < 64 {
471            self.header()
472                .parked_mask
473                .fetch_or(1u64 << idx, Ordering::Release);
474        }
475    }
476
477    #[inline]
478    fn mask_clear(&self, idx: usize) {
479        if idx < 64 {
480            self.header()
481                .parked_mask
482                .fetch_and(!(1u64 << idx), Ordering::Release);
483        }
484    }
485
486    /// Iterator over candidate slot indices for a wake scan: the
487    /// parked-mask bits when the mask covers every slot, else the
488    /// full range.
489    #[inline]
490    fn wake_candidates(&self) -> WakeCandidates {
491        if self.capacity <= 64 {
492            WakeCandidates::Mask(
493                self.header().parked_mask.load(Ordering::Acquire),
494            )
495        } else {
496            WakeCandidates::Range(0, self.capacity)
497        }
498    }
499
500    pub fn wait(
501        &self,
502        token: WakerToken,
503        timeout: Option<Duration>,
504    ) -> Result<(), WakerError> {
505        let slot = self.slot(token.slot as usize);
506        let cross_process = self.is_cross_process();
507        let result = match timeout {
508            None => {
509                loop {
510                    let cur = slot.state.load(Ordering::Acquire);
511                    if cur != STATE_PARKED {
512                        break Ok(());
513                    }
514                    platform_wait::wait_forever(
515                        &slot.state, STATE_PARKED, cross_process,
516                    );
517                }
518            }
519            Some(d) => {
520                // Deadline re-check loop. The wait syscall is only a
521                // hint: futex and the MONITOR/MWAIT tier may both wake
522                // SPURIOUSLY, so `state` - not the syscall's return -
523                // is the authority. A spurious wake re-loops and waits
524                // the remaining time; only a real producer transition
525                // (state != PARKED) returns Ok, and only an elapsed
526                // deadline returns Timeout. Without this loop a single
527                // spurious wake returned Ok, making waits end early and
528                // freeing the slot before a wake_all could see it.
529                let deadline = Instant::now() + d;
530                loop {
531                    if slot.state.load(Ordering::Acquire) != STATE_PARKED {
532                        break Ok(());
533                    }
534                    match deadline.checked_duration_since(Instant::now()) {
535                        Some(remaining) if !remaining.is_zero() => {
536                            platform_wait::wait_with_timeout(
537                                &slot.state, STATE_PARKED, remaining, cross_process,
538                            );
539                        }
540                        _ => {
541                            // Deadline reached; one last check catches a
542                            // wake that landed at the wire.
543                            break if slot.state.load(Ordering::Acquire) != STATE_PARKED {
544                                Ok(())
545                            } else {
546                                Err(WakerError::Timeout)
547                            };
548                        }
549                    }
550                }
551            }
552        };
553        slot.state.store(STATE_FREE, Ordering::Release);
554        self.mask_clear(token.slot as usize);
555        result
556    }
557
558    /// Release a parked slot without waiting. Used by the
559    /// blocking-recv wrapper's wake-before-park-race recovery:
560    /// after parking, the wrapper double-checks try_recv; if
561    /// that succeeds, it calls release to give the slot back
562    /// without entering the kernel.
563    pub fn release(&self, token: WakerToken) {
564        let slot = self.slot(token.slot as usize);
565        slot.state.store(STATE_FREE, Ordering::Release);
566        self.mask_clear(token.slot as usize);
567    }
568
569    /// Producer's post-publish wake call. Scans every slot;
570    /// for each PARKED slot whose `target_seq <= seq`, CASes
571    /// state to WOKEN and fires a single-slot wake. Returns the
572    /// number of consumers woken.
573    pub fn wake_up_to(&self, seq: u64) -> usize {
574        let mut count = 0usize;
575        for idx in self.wake_candidates() {
576            let slot = self.slot(idx);
577            let cur_state = slot.state.load(Ordering::Acquire);
578            if cur_state != STATE_PARKED {
579                continue;
580            }
581            // The Acquire on state acquires the parker's Release
582            // store, so target_seq is safe to read Relaxed.
583            let tgt = slot.target_seq.load(Ordering::Relaxed);
584            if seq < tgt {
585                continue;
586            }
587            // Try to claim the wake. If the CAS fails another
588            // producer already woke this slot (or the parker
589            // released it); skip.
590            if slot
591                .state
592                .compare_exchange(
593                    STATE_PARKED,
594                    STATE_WOKEN,
595                    Ordering::AcqRel,
596                    Ordering::Relaxed,
597                )
598                .is_ok()
599            {
600                platform_wait::wake_one(&slot.state, self.is_cross_process());
601                count += 1;
602            }
603        }
604        count
605    }
606
607    /// Wake AT MOST ONE PARKED slot whose `target_seq <= seq`. Used
608    /// by Mesa-style condvar `notify_one`: notifier bumps the
609    /// generation, then wakes exactly one waiter (if any) so the
610    /// other parked waiters stay parked.
611    ///
612    /// Returns 1 if a waiter was woken, 0 if none qualified.
613    pub fn wake_one_up_to(&self, seq: u64) -> usize {
614        for idx in self.wake_candidates() {
615            let slot = self.slot(idx);
616            let cur_state = slot.state.load(Ordering::Acquire);
617            if cur_state != STATE_PARKED {
618                continue;
619            }
620            let tgt = slot.target_seq.load(Ordering::Relaxed);
621            if seq < tgt {
622                continue;
623            }
624            if slot
625                .state
626                .compare_exchange(
627                    STATE_PARKED,
628                    STATE_WOKEN,
629                    Ordering::AcqRel,
630                    Ordering::Relaxed,
631                )
632                .is_ok()
633            {
634                platform_wait::wake_one(&slot.state, self.is_cross_process());
635                return 1;
636            }
637        }
638        0
639    }
640
641    /// Wake every PARKED slot regardless of `target_seq`. Used
642    /// during shutdown / drain so blocked consumers see the
643    /// terminate signal.
644    pub fn wake_all(&self) -> usize {
645        let mut count = 0usize;
646        for idx in self.wake_candidates() {
647            let slot = self.slot(idx);
648            if slot
649                .state
650                .compare_exchange(
651                    STATE_PARKED,
652                    STATE_WOKEN,
653                    Ordering::AcqRel,
654                    Ordering::Relaxed,
655                )
656                .is_ok()
657            {
658                platform_wait::wake_one(&slot.state, self.is_cross_process());
659                count += 1;
660            }
661        }
662        count
663    }
664}
665
666/// Wake-scan candidate indices: set bits of the parked mask, or a
667/// plain range when the capacity outgrows the 64-bit mask.
668enum WakeCandidates {
669    Mask(u64),
670    Range(usize, usize),
671}
672
673impl Iterator for WakeCandidates {
674    type Item = usize;
675    #[inline]
676    fn next(&mut self) -> Option<usize> {
677        match self {
678            WakeCandidates::Mask(m) => {
679                if *m == 0 {
680                    return None;
681                }
682                let idx = m.trailing_zeros() as usize;
683                *m &= *m - 1;
684                Some(idx)
685            }
686            WakeCandidates::Range(next, end) => {
687                if next < end {
688                    let idx = *next;
689                    *next += 1;
690                    Some(idx)
691                } else {
692                    None
693                }
694            }
695        }
696    }
697}
698
699// ============================================================================
700// Platform wait / wake. We do NOT use the atomic-wait crate
701// because that crate hard-codes FUTEX_PRIVATE_FLAG on Linux,
702// which restricts the futex to a single process and breaks the
703// cross-process wake claim. The waker calls the platform's
704// SHARED futex / WaitOnAddress / wake APIs directly.
705//
706// Every wait first runs the bounded MONITOR-class tier (see
707// crate::monitor_wait): MONITORX/MWAITX or UMONITOR/UMWAIT light
708// sleep on the slot's cache line for ~tens of microseconds, woken
709// for free by the producer's state store - cross-process included,
710// since hardware monitors are physical-address based. Only when
711// that budget expires does the wait escalate to the per-platform
712// kernel park below.
713//
714// Cross-process status per platform:
715// - Linux / Android: SHARED futex (no PRIVATE flag) works
716//   across processes when the atomic sits in a SHARED mmap.
717// - FreeBSD: _umtx_op with the non-PRIVATE UMTX_OP_WAIT_UINT /
718//   UMTX_OP_WAKE ops; the kernel keys those sleep queues by
719//   physical address ("same variable mapped multiple times will
720//   give one key value" - _umtx_op(2)), so waiters across
721//   processes sharing an MMF page join one queue.
722// - Windows: WaitOnAddress is INTRA-PROCESS only per the docs,
723//   so it serves anon-backed wakers; file / shm-backed wakers
724//   stay on the monitor tier for their whole wait (the
725//   cross_process flag below selects this), which IS
726//   cross-process because hardware monitors key on physical
727//   addresses. Hosts without MONITORX/WAITPKG fall back to the
728//   wait-timeout + re-check recovery in the blocking wrappers.
729// - macOS / others: polling fallback; correct but wastes CPU
730//   under heavy idle.
731// ============================================================================
732
733mod platform_wait {
734    use std::sync::atomic::AtomicU32;
735    use std::time::Duration;
736
737    /// macOS 14.4+ `os_sync_*` public-futex symbols resolved at RUNTIME via
738    /// `dlsym`, so the binary LINKS against an older SDK (e.g. 10.15, whose
739    /// libsystem has no `os_sync_*`) and degrades to the polling fallback there,
740    /// while taking the fast path on 14.4+. The flag constants are plain
741    /// integers (no link dependency), so only the three functions are resolved.
742    #[cfg(target_os = "macos")]
743    mod os_sync_dyn {
744        use std::ffi::c_void;
745        use std::sync::atomic::{AtomicUsize, Ordering};
746        pub type WaitFn = unsafe extern "C" fn(*mut c_void, u64, usize, u32) -> i32;
747        pub type WaitTimeoutFn =
748            unsafe extern "C" fn(*mut c_void, u64, usize, u32, u32, u64) -> i32;
749        pub type WakeFn = unsafe extern "C" fn(*mut c_void, usize, u32) -> i32;
750        // 1 = not yet probed, 0 = absent (older macOS), else = resolved fn ptr.
751        static WAIT: AtomicUsize = AtomicUsize::new(1);
752        static WAIT_TO: AtomicUsize = AtomicUsize::new(1);
753        static WAKE: AtomicUsize = AtomicUsize::new(1);
754        fn cached(slot: &AtomicUsize, name: &[u8]) -> usize {
755            let v = slot.load(Ordering::Relaxed);
756            if v != 1 {
757                return v;
758            }
759            // SAFETY: RTLD_DEFAULT lookup of a C symbol by NUL-terminated name.
760            let r = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr() as *const _) } as usize;
761            slot.store(r, Ordering::Relaxed);
762            r
763        }
764        pub fn wait() -> Option<WaitFn> {
765            match cached(&WAIT, b"os_sync_wait_on_address\0") {
766                0 => None,
767                // SAFETY: a non-null resolution of this symbol has this ABI.
768                p => Some(unsafe { std::mem::transmute::<usize, WaitFn>(p) }),
769            }
770        }
771        pub fn wait_timeout() -> Option<WaitTimeoutFn> {
772            match cached(&WAIT_TO, b"os_sync_wait_on_address_with_timeout\0") {
773                0 => None,
774                p => Some(unsafe { std::mem::transmute::<usize, WaitTimeoutFn>(p) }),
775            }
776        }
777        pub fn wake() -> Option<WakeFn> {
778            match cached(&WAKE, b"os_sync_wake_by_address_any\0") {
779                0 => None,
780                p => Some(unsafe { std::mem::transmute::<usize, WakeFn>(p) }),
781            }
782        }
783    }
784
785    /// The monitor tier: MONITORX/MWAITX (AMD) or UMONITOR/UMWAIT
786    /// (WAITPKG) light-sleep waiting for a bounded cycle budget
787    /// BEFORE the kernel park. Two wins when the wait resolves
788    /// inside the budget: the producer's wake is its existing
789    /// state-CAS (no syscall on either side), and - because
790    /// hardware monitors are physical-address based - the wake
791    /// crosses process boundaries on shared MMF pages, which on
792    /// Windows is the only non-polling cross-process wake the
793    /// platform offers (WaitOnAddress is intra-process). Returning
794    /// `false` (budget expired / unsupported CPU /
795    /// SUBETHA_NO_MONITOR_WAIT=1) falls through to the kernel
796    /// park, which re-checks the value itself - so the tier can
797    /// never lose a wake, only hand off.
798    #[inline]
799    fn monitor_tier(atomic: &AtomicU32, expected: u32) -> bool {
800        crate::monitor_wait::monitor_wait_u32(
801            atomic,
802            expected,
803            crate::monitor_wait::monitor_wait_budget_cycles(),
804        )
805    }
806
807    pub fn wait_forever(atomic: &AtomicU32, expected: u32, _cross_process: bool) {
808        if monitor_tier(atomic, expected) {
809            return;
810        }
811        // Windows + cross-process backing: WaitOnAddress never
812        // receives a wake from another process, so the monitor IS
813        // the wait - re-arm in budget-sized chunks until the value
814        // changes. The core holds C0.1 light sleep rather than
815        // releasing to the OS; that is the only non-polling
816        // cross-process wait the platform offers.
817        #[cfg(windows)]
818        if _cross_process
819            && crate::monitor_wait::monitor_wait_kind().is_some()
820        {
821            let budget = crate::monitor_wait::monitor_wait_budget_cycles();
822            while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
823                crate::monitor_wait::monitor_wait_u32(atomic, expected, budget);
824            }
825            return;
826        }
827        #[cfg(any(target_os = "linux", target_os = "android"))]
828        {
829            unsafe {
830                libc::syscall(
831                    libc::SYS_futex,
832                    atomic.as_ptr(),
833                    libc::FUTEX_WAIT,
834                    expected as libc::c_int,
835                    std::ptr::null::<libc::timespec>(),
836                );
837            }
838        }
839        #[cfg(target_os = "freebsd")]
840        {
841            // UMTX_OP_WAIT_UINT (the non-PRIVATE op): the kernel
842            // keys the sleep queue by the variable's PHYSICAL
843            // address, so waiters in any process that mapped the
844            // same MMF page share one queue - FreeBSD's native
845            // equivalent of the no-FUTEX_PRIVATE_FLAG Linux call.
846            // Sleeps only while *obj == val, exactly futex_wait.
847            unsafe {
848                libc::_umtx_op(
849                    atomic.as_ptr() as *mut libc::c_void,
850                    libc::UMTX_OP_WAIT_UINT,
851                    expected as libc::c_ulong,
852                    std::ptr::null_mut(),
853                    std::ptr::null_mut(),
854                );
855            }
856        }
857        #[cfg(windows)]
858        {
859            use windows_sys::Win32::System::Threading::{WaitOnAddress, INFINITE};
860            let expected_local = expected;
861            unsafe {
862                WaitOnAddress(
863                    atomic.as_ptr() as *const std::ffi::c_void,
864                    &expected_local as *const u32 as *const std::ffi::c_void,
865                    std::mem::size_of::<u32>(),
866                    INFINITE,
867                );
868            }
869        }
870        #[cfg(target_os = "macos")]
871        {
872            // The public futex (macOS 14.4+): compare-and-wait on the address;
873            // OS_SYNC_WAIT_ON_ADDRESS_SHARED keys the queue for a shared-memory
874            // address, allowing a futex wake from another process (the backing
875            // decides the flag). The symbol is resolved at runtime; on older
876            // macOS it is absent, so poll like the generic fallback below.
877            if let Some(wait_fn) = os_sync_dyn::wait() {
878                let flags = if _cross_process {
879                    libc::OS_SYNC_WAIT_ON_ADDRESS_SHARED
880                } else {
881                    libc::OS_SYNC_WAIT_ON_ADDRESS_NONE
882                };
883                unsafe {
884                    wait_fn(
885                        atomic.as_ptr() as *mut libc::c_void,
886                        expected as u64,
887                        std::mem::size_of::<u32>(),
888                        flags,
889                    );
890                }
891            } else {
892                std::thread::yield_now();
893                while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
894                    std::thread::sleep(Duration::from_millis(1));
895                }
896            }
897        }
898        #[cfg(not(any(target_os = "linux", target_os = "android",
899                      target_os = "freebsd", target_os = "macos", windows)))]
900        {
901            std::thread::yield_now();
902            while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
903                std::thread::sleep(Duration::from_millis(1));
904            }
905        }
906    }
907
908    pub fn wait_with_timeout(
909        atomic: &AtomicU32,
910        expected: u32,
911        timeout: Duration,
912        _cross_process: bool,
913    ) -> bool {
914        let monitor_start = std::time::Instant::now();
915        if monitor_tier(atomic, expected) {
916            return true;
917        }
918        // The monitor budget counts against the caller's timeout;
919        // the kernel park gets the remainder.
920        let timeout = match timeout.checked_sub(monitor_start.elapsed()) {
921            Some(rest) if !rest.is_zero() => rest,
922            _ => {
923                return atomic.load(std::sync::atomic::Ordering::Acquire)
924                    != expected;
925            }
926        };
927        // Windows + cross-process backing: stay on the monitor for
928        // the whole timeout (see wait_forever).
929        #[cfg(windows)]
930        if _cross_process
931            && crate::monitor_wait::monitor_wait_kind().is_some()
932        {
933            let deadline = std::time::Instant::now() + timeout;
934            let budget = crate::monitor_wait::monitor_wait_budget_cycles();
935            loop {
936                if crate::monitor_wait::monitor_wait_u32(atomic, expected, budget) {
937                    return true;
938                }
939                if std::time::Instant::now() >= deadline {
940                    return atomic.load(std::sync::atomic::Ordering::Acquire)
941                        != expected;
942                }
943            }
944        }
945        #[cfg(any(target_os = "linux", target_os = "android"))]
946        {
947            let ts = libc::timespec {
948                tv_sec: timeout.as_secs() as libc::time_t,
949                tv_nsec: timeout.subsec_nanos() as libc::c_long,
950            };
951            unsafe {
952                let rc = libc::syscall(
953                    libc::SYS_futex,
954                    atomic.as_ptr(),
955                    libc::FUTEX_WAIT,
956                    expected as libc::c_int,
957                    &ts as *const libc::timespec,
958                    std::ptr::null::<()>(),
959                    0u32,
960                );
961                if rc == -1 {
962                    let err = *libc::__errno_location();
963                    return err != libc::ETIMEDOUT;
964                }
965            }
966            true
967        }
968        #[cfg(target_os = "freebsd")]
969        {
970            // Relative timeout, monotonic clock by default: uaddr2
971            // points at the timespec and uaddr carries that
972            // structure's size, per _umtx_op(2).
973            let mut ts = libc::timespec {
974                tv_sec: timeout.as_secs() as libc::time_t,
975                tv_nsec: timeout.subsec_nanos() as libc::c_long,
976            };
977            let rc = unsafe {
978                libc::_umtx_op(
979                    atomic.as_ptr() as *mut libc::c_void,
980                    libc::UMTX_OP_WAIT_UINT,
981                    expected as libc::c_ulong,
982                    std::mem::size_of::<libc::timespec>() as *mut libc::c_void,
983                    &mut ts as *mut libc::timespec as *mut libc::c_void,
984                )
985            };
986            if rc == -1 {
987                let err = unsafe { *libc::__error() };
988                return err != libc::ETIMEDOUT;
989            }
990            true
991        }
992        #[cfg(windows)]
993        {
994            use windows_sys::Win32::System::Threading::WaitOnAddress;
995            let expected_local = expected;
996            let ms = timeout.as_millis().min(u32::MAX as u128) as u32;
997            let rc = unsafe {
998                WaitOnAddress(
999                    atomic.as_ptr() as *const std::ffi::c_void,
1000                    &expected_local as *const u32 as *const std::ffi::c_void,
1001                    std::mem::size_of::<u32>(),
1002                    ms,
1003                )
1004            };
1005            rc != 0
1006        }
1007        #[cfg(target_os = "macos")]
1008        {
1009            if let Some(wait_fn) = os_sync_dyn::wait_timeout() {
1010                let flags = if _cross_process {
1011                    libc::OS_SYNC_WAIT_ON_ADDRESS_SHARED
1012                } else {
1013                    libc::OS_SYNC_WAIT_ON_ADDRESS_NONE
1014                };
1015                let rc = unsafe {
1016                    wait_fn(
1017                        atomic.as_ptr() as *mut libc::c_void,
1018                        expected as u64,
1019                        std::mem::size_of::<u32>(),
1020                        flags,
1021                        libc::OS_CLOCK_MACH_ABSOLUTE_TIME,
1022                        timeout.as_nanos().min(u64::MAX as u128) as u64,
1023                    )
1024                };
1025                if rc < 0 {
1026                    return std::io::Error::last_os_error().raw_os_error()
1027                        != Some(libc::ETIMEDOUT);
1028                }
1029                true
1030            } else {
1031                // Older macOS (< 14.4): poll with a deadline, like the generic
1032                // fallback. Returns true if the value changed, false on timeout.
1033                let deadline = std::time::Instant::now() + timeout;
1034                loop {
1035                    if atomic.load(std::sync::atomic::Ordering::Acquire) != expected {
1036                        return true;
1037                    }
1038                    if std::time::Instant::now() >= deadline {
1039                        return atomic.load(std::sync::atomic::Ordering::Acquire) != expected;
1040                    }
1041                    std::thread::sleep(Duration::from_millis(2));
1042                }
1043            }
1044        }
1045        #[cfg(not(any(target_os = "linux", target_os = "android",
1046                      target_os = "freebsd", target_os = "macos", windows)))]
1047        {
1048            let deadline = std::time::Instant::now() + timeout;
1049            let step = Duration::from_millis(2);
1050            loop {
1051                if atomic.load(std::sync::atomic::Ordering::Acquire) != expected {
1052                    return true;
1053                }
1054                let now = std::time::Instant::now();
1055                if now >= deadline {
1056                    return false;
1057                }
1058                let remaining = deadline - now;
1059                std::thread::sleep(remaining.min(step));
1060            }
1061        }
1062    }
1063
1064    pub fn wake_one(atomic: &AtomicU32, _cross_process: bool) {
1065        #[cfg(any(target_os = "linux", target_os = "android"))]
1066        {
1067            unsafe {
1068                libc::syscall(
1069                    libc::SYS_futex,
1070                    atomic.as_ptr(),
1071                    libc::FUTEX_WAKE,
1072                    1i32,
1073                );
1074            }
1075        }
1076        #[cfg(target_os = "freebsd")]
1077        {
1078            // val = max threads to wake; same shared (non-PRIVATE)
1079            // physical-address-keyed queue the waiters parked on.
1080            unsafe {
1081                libc::_umtx_op(
1082                    atomic.as_ptr() as *mut libc::c_void,
1083                    libc::UMTX_OP_WAKE,
1084                    1 as libc::c_ulong,
1085                    std::ptr::null_mut(),
1086                    std::ptr::null_mut(),
1087                );
1088            }
1089        }
1090        #[cfg(windows)]
1091        {
1092            use windows_sys::Win32::System::Threading::WakeByAddressSingle;
1093            unsafe {
1094                WakeByAddressSingle(atomic.as_ptr() as *const std::ffi::c_void);
1095            }
1096        }
1097        #[cfg(target_os = "macos")]
1098        {
1099            // Mirror of the wait flag: SHARED wakes waiters in any process that
1100            // mapped the same region. On older macOS the symbol is absent and
1101            // the waiter polls, so no explicit wake is needed.
1102            if let Some(wake_fn) = os_sync_dyn::wake() {
1103                let flags = if _cross_process {
1104                    libc::OS_SYNC_WAKE_BY_ADDRESS_SHARED
1105                } else {
1106                    libc::OS_SYNC_WAKE_BY_ADDRESS_NONE
1107                };
1108                unsafe {
1109                    wake_fn(
1110                        atomic.as_ptr() as *mut libc::c_void,
1111                        std::mem::size_of::<u32>(),
1112                        flags,
1113                    );
1114                }
1115            }
1116        }
1117        #[cfg(not(any(target_os = "linux", target_os = "android",
1118                      target_os = "freebsd", target_os = "macos", windows)))]
1119        {
1120            drop(atomic);
1121        }
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128    use std::sync::Arc;
1129    use std::thread;
1130    use std::time::Instant;
1131
1132    #[test]
1133    fn anon_round_trip() {
1134        let waker = CrossProcessWaker::create_anon(4).expect("create");
1135        let token = waker.try_park(10).expect("park");
1136        let waker_c = Arc::new(waker);
1137        let waker_p = Arc::clone(&waker_c);
1138        let h = thread::spawn(move || {
1139            thread::sleep(Duration::from_millis(20));
1140            let n = waker_p.wake_up_to(15);
1141            assert!(n >= 1);
1142        });
1143        waker_c.wait(token, Some(Duration::from_secs(2))).expect("wake");
1144        h.join().unwrap();
1145    }
1146
1147    #[test]
1148    fn wait_returns_immediately_if_already_woken() {
1149        let waker = CrossProcessWaker::create_anon(2).expect("create");
1150        let token = waker.try_park(5).expect("park");
1151        assert_eq!(waker.wake_up_to(10), 1);
1152        let t0 = Instant::now();
1153        waker.wait(token, Some(Duration::from_secs(1))).expect("ok");
1154        assert!(t0.elapsed() < Duration::from_millis(50),
1155                "wait should return fast since wake fired before wait entered");
1156    }
1157
1158    #[test]
1159    fn timeout_works() {
1160        let waker = CrossProcessWaker::create_anon(2).expect("create");
1161        let token = waker.try_park(100).expect("park");
1162        let t0 = Instant::now();
1163        let err = waker.wait(token, Some(Duration::from_millis(80)));
1164        assert_eq!(err, Err(WakerError::Timeout));
1165        assert!(t0.elapsed() >= Duration::from_millis(70));
1166    }
1167
1168    #[test]
1169    fn full_when_all_slots_taken() {
1170        let waker = CrossProcessWaker::create_anon(2).expect("create");
1171        let _a = waker.try_park(1).expect("park 0");
1172        let _b = waker.try_park(2).expect("park 1");
1173        assert_eq!(waker.try_park(3), Err(WakerError::Full));
1174    }
1175
1176    #[test]
1177    fn release_lets_others_park() {
1178        let waker = CrossProcessWaker::create_anon(2).expect("create");
1179        let a = waker.try_park(1).expect("park 0");
1180        let _b = waker.try_park(2).expect("park 1");
1181        waker.release(a);
1182        let _c = waker.try_park(3).expect("re-park 0");
1183    }
1184
1185    #[test]
1186    fn wake_all_drains_blocked_consumers() {
1187        let waker = Arc::new(CrossProcessWaker::create_anon(4).expect("create"));
1188        let mut handles = Vec::new();
1189        for target in 0..4u64 {
1190            let w = Arc::clone(&waker);
1191            let token = w.try_park(target + 1000).expect("park");
1192            handles.push(thread::spawn(move || {
1193                w.wait(token, Some(Duration::from_secs(2))).expect("woken");
1194            }));
1195        }
1196        thread::sleep(Duration::from_millis(20));
1197        assert_eq!(waker.wake_all(), 4);
1198        for h in handles { h.join().unwrap(); }
1199    }
1200}