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
292    /// cache.
293    pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, WakerError> {
294        assert!(capacity >= 1, "capacity must be >= 1");
295        let total = waker_region_size(capacity);
296        let file = OpenOptions::new()
297            .read(true).write(true).create(true).truncate(true)
298            .open(path.as_ref())?;
299        file.set_len(total as u64)?;
300        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
301        let raw_ptr = mmap.as_mut_ptr();
302        unsafe { init_waker_layout_raw(raw_ptr, capacity); }
303        Ok(Self {
304            _backing: WakerBacking::File(file, mmap),
305            raw_ptr,
306            capacity,
307        })
308    }
309
310    /// Open an existing file-backed waker. Validates magic +
311    /// capacity.
312    pub fn open(
313        path: impl AsRef<Path>,
314        expected_capacity: usize,
315    ) -> Result<Self, WakerError> {
316        let total = waker_region_size(expected_capacity);
317        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
318        if (file.metadata()?.len() as usize) < total {
319            return Err(WakerError::LayoutMismatch);
320        }
321        let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
322        let raw_ptr = mmap.as_mut_ptr();
323        let hdr = unsafe { &*(raw_ptr as *const WakerHeader) };
324        if hdr.magic != WAKER_MAGIC || hdr.capacity as usize != expected_capacity {
325            return Err(WakerError::LayoutMismatch);
326        }
327        Ok(Self {
328            _backing: WakerBacking::File(file, mmap),
329            raw_ptr,
330            capacity: expected_capacity,
331        })
332    }
333
334    /// Build a fresh waker on top of a named-shm region. Cross-
335    /// process visible via the `logical_name` of the underlying
336    /// [`ShmFile`](crate::shm_file::ShmFile); RAM-resident.
337    pub fn create_from_shm(
338        mut shm: crate::shm_file::ShmFile,
339        capacity: usize,
340    ) -> Result<Self, WakerError> {
341        assert!(capacity >= 1, "capacity must be >= 1");
342        let total = waker_region_size(capacity);
343        if shm.len() < total {
344            return Err(WakerError::LayoutMismatch);
345        }
346        let raw_ptr = shm.as_mut_slice().as_mut_ptr();
347        unsafe { init_waker_layout_raw(raw_ptr, capacity); }
348        Ok(Self {
349            _backing: WakerBacking::Shm(shm),
350            raw_ptr,
351            capacity,
352        })
353    }
354
355    /// Open an existing named-shm waker without re-initialising
356    /// the layout.
357    pub fn open_from_shm(
358        mut shm: crate::shm_file::ShmFile,
359        expected_capacity: usize,
360    ) -> Result<Self, WakerError> {
361        let total = waker_region_size(expected_capacity);
362        if shm.len() < total {
363            return Err(WakerError::LayoutMismatch);
364        }
365        let raw_ptr = shm.as_mut_slice().as_mut_ptr();
366        let hdr = unsafe { &*(raw_ptr as *const WakerHeader) };
367        if hdr.magic != WAKER_MAGIC || hdr.capacity as usize != expected_capacity {
368            return Err(WakerError::LayoutMismatch);
369        }
370        Ok(Self {
371            _backing: WakerBacking::Shm(shm),
372            raw_ptr,
373            capacity: expected_capacity,
374        })
375    }
376
377    /// Slot count fixed at construction.
378    pub fn capacity(&self) -> usize { self.capacity }
379
380    #[inline]
381    fn slot(&self, idx: usize) -> &WakerSlot {
382        let base = unsafe { self.raw_ptr.add(std::mem::size_of::<WakerHeader>()) };
383        unsafe { &*(base.add(idx * std::mem::size_of::<WakerSlot>()) as *const WakerSlot) }
384    }
385
386    /// Reserve a slot and park at `target_seq`. The caller will
387    /// be woken when some producer calls `wake_up_to(seq)` with
388    /// `seq >= target_seq`. Returns the token the caller passes
389    /// to `wait` / `release`.
390    ///
391    /// On `Err(Full)`, every slot is currently in use; the caller
392    /// falls back to spinning. This is the same fallback they'd
393    /// use without a waker at all.
394    pub fn try_park(&self, target_seq: u64) -> Result<WakerToken, WakerError> {
395        for idx in 0..self.capacity {
396            let slot = self.slot(idx);
397            // FREE -> RESERVED CAS. On success, this slot is
398            // ours; on failure, another parker beat us to it,
399            // try the next slot.
400            if slot
401                .state
402                .compare_exchange(
403                    STATE_FREE,
404                    STATE_RESERVED,
405                    Ordering::Acquire,
406                    Ordering::Relaxed,
407                )
408                .is_ok()
409            {
410                // Write target_seq before publishing as PARKED.
411                // The Release store on state below is the
412                // happens-before edge for this Relaxed store.
413                slot.target_seq.store(target_seq, Ordering::Relaxed);
414                // Publish the slot to producers.
415                slot.state.store(STATE_PARKED, Ordering::Release);
416                self.mask_set(idx);
417                return Ok(WakerToken { slot: idx as u32 });
418            }
419        }
420        Err(WakerError::Full)
421    }
422
423    /// Block until either some producer wakes this token or the
424    /// optional timeout elapses. After return (Ok or Err), the
425    /// token's slot is released; the caller does NOT need to
426    /// call `release` separately.
427    ///
428    /// If the slot's state was already transitioned to `WOKEN`
429    /// before the wait call entered the kernel, the wait returns
430    /// immediately (no kernel sleep).
431    /// Whether this waker's backing is reachable from other
432    /// processes (file / named-shm) rather than process-private
433    /// anonymous memory. Windows' wait ladder branches on this:
434    /// `WaitOnAddress` never receives a cross-process wake, so
435    /// cross-process-backed waits stay on the hardware monitor
436    /// tier (physical-address based) for their whole duration.
437    fn is_cross_process(&self) -> bool {
438        !matches!(self._backing, WakerBacking::Anon(_))
439    }
440
441    fn header(&self) -> &WakerHeader {
442        unsafe { &*(self.raw_ptr as *const WakerHeader) }
443    }
444
445    #[inline]
446    fn mask_set(&self, idx: usize) {
447        if idx < 64 {
448            self.header()
449                .parked_mask
450                .fetch_or(1u64 << idx, Ordering::Release);
451        }
452    }
453
454    #[inline]
455    fn mask_clear(&self, idx: usize) {
456        if idx < 64 {
457            self.header()
458                .parked_mask
459                .fetch_and(!(1u64 << idx), Ordering::Release);
460        }
461    }
462
463    /// Iterator over candidate slot indices for a wake scan: the
464    /// parked-mask bits when the mask covers every slot, else the
465    /// full range.
466    #[inline]
467    fn wake_candidates(&self) -> WakeCandidates {
468        if self.capacity <= 64 {
469            WakeCandidates::Mask(
470                self.header().parked_mask.load(Ordering::Acquire),
471            )
472        } else {
473            WakeCandidates::Range(0, self.capacity)
474        }
475    }
476
477    pub fn wait(
478        &self,
479        token: WakerToken,
480        timeout: Option<Duration>,
481    ) -> Result<(), WakerError> {
482        let slot = self.slot(token.slot as usize);
483        let cross_process = self.is_cross_process();
484        let result = match timeout {
485            None => {
486                loop {
487                    let cur = slot.state.load(Ordering::Acquire);
488                    if cur != STATE_PARKED {
489                        break Ok(());
490                    }
491                    platform_wait::wait_forever(
492                        &slot.state, STATE_PARKED, cross_process,
493                    );
494                }
495            }
496            Some(d) => {
497                // Deadline re-check loop. The wait syscall is only a
498                // hint: futex and the MONITOR/MWAIT tier may both wake
499                // SPURIOUSLY, so `state` - not the syscall's return -
500                // is the authority. A spurious wake re-loops and waits
501                // the remaining time; only a real producer transition
502                // (state != PARKED) returns Ok, and only an elapsed
503                // deadline returns Timeout. Without this loop a single
504                // spurious wake returned Ok, making waits end early and
505                // freeing the slot before a wake_all could see it.
506                let deadline = Instant::now() + d;
507                loop {
508                    if slot.state.load(Ordering::Acquire) != STATE_PARKED {
509                        break Ok(());
510                    }
511                    match deadline.checked_duration_since(Instant::now()) {
512                        Some(remaining) if !remaining.is_zero() => {
513                            platform_wait::wait_with_timeout(
514                                &slot.state, STATE_PARKED, remaining, cross_process,
515                            );
516                        }
517                        _ => {
518                            // Deadline reached; one last check catches a
519                            // wake that landed at the wire.
520                            break if slot.state.load(Ordering::Acquire) != STATE_PARKED {
521                                Ok(())
522                            } else {
523                                Err(WakerError::Timeout)
524                            };
525                        }
526                    }
527                }
528            }
529        };
530        slot.state.store(STATE_FREE, Ordering::Release);
531        self.mask_clear(token.slot as usize);
532        result
533    }
534
535    /// Release a parked slot without waiting. Used by the
536    /// blocking-recv wrapper's wake-before-park-race recovery:
537    /// after parking, the wrapper double-checks try_recv; if
538    /// that succeeds, it calls release to give the slot back
539    /// without entering the kernel.
540    pub fn release(&self, token: WakerToken) {
541        let slot = self.slot(token.slot as usize);
542        slot.state.store(STATE_FREE, Ordering::Release);
543        self.mask_clear(token.slot as usize);
544    }
545
546    /// Producer's post-publish wake call. Scans every slot;
547    /// for each PARKED slot whose `target_seq <= seq`, CASes
548    /// state to WOKEN and fires a single-slot wake. Returns the
549    /// number of consumers woken.
550    pub fn wake_up_to(&self, seq: u64) -> usize {
551        let mut count = 0usize;
552        for idx in self.wake_candidates() {
553            let slot = self.slot(idx);
554            let cur_state = slot.state.load(Ordering::Acquire);
555            if cur_state != STATE_PARKED {
556                continue;
557            }
558            // The Acquire on state acquires the parker's Release
559            // store, so target_seq is safe to read Relaxed.
560            let tgt = slot.target_seq.load(Ordering::Relaxed);
561            if seq < tgt {
562                continue;
563            }
564            // Try to claim the wake. If the CAS fails another
565            // producer already woke this slot (or the parker
566            // released it); skip.
567            if slot
568                .state
569                .compare_exchange(
570                    STATE_PARKED,
571                    STATE_WOKEN,
572                    Ordering::AcqRel,
573                    Ordering::Relaxed,
574                )
575                .is_ok()
576            {
577                platform_wait::wake_one(&slot.state, self.is_cross_process());
578                count += 1;
579            }
580        }
581        count
582    }
583
584    /// Wake AT MOST ONE PARKED slot whose `target_seq <= seq`. Used
585    /// by Mesa-style condvar `notify_one`: notifier bumps the
586    /// generation, then wakes exactly one waiter (if any) so the
587    /// other parked waiters stay parked.
588    ///
589    /// Returns 1 if a waiter was woken, 0 if none qualified.
590    pub fn wake_one_up_to(&self, seq: u64) -> usize {
591        for idx in self.wake_candidates() {
592            let slot = self.slot(idx);
593            let cur_state = slot.state.load(Ordering::Acquire);
594            if cur_state != STATE_PARKED {
595                continue;
596            }
597            let tgt = slot.target_seq.load(Ordering::Relaxed);
598            if seq < tgt {
599                continue;
600            }
601            if slot
602                .state
603                .compare_exchange(
604                    STATE_PARKED,
605                    STATE_WOKEN,
606                    Ordering::AcqRel,
607                    Ordering::Relaxed,
608                )
609                .is_ok()
610            {
611                platform_wait::wake_one(&slot.state, self.is_cross_process());
612                return 1;
613            }
614        }
615        0
616    }
617
618    /// Wake every PARKED slot regardless of `target_seq`. Used
619    /// during shutdown / drain so blocked consumers see the
620    /// terminate signal.
621    pub fn wake_all(&self) -> usize {
622        let mut count = 0usize;
623        for idx in self.wake_candidates() {
624            let slot = self.slot(idx);
625            if slot
626                .state
627                .compare_exchange(
628                    STATE_PARKED,
629                    STATE_WOKEN,
630                    Ordering::AcqRel,
631                    Ordering::Relaxed,
632                )
633                .is_ok()
634            {
635                platform_wait::wake_one(&slot.state, self.is_cross_process());
636                count += 1;
637            }
638        }
639        count
640    }
641}
642
643/// Wake-scan candidate indices: set bits of the parked mask, or a
644/// plain range when the capacity outgrows the 64-bit mask.
645enum WakeCandidates {
646    Mask(u64),
647    Range(usize, usize),
648}
649
650impl Iterator for WakeCandidates {
651    type Item = usize;
652    #[inline]
653    fn next(&mut self) -> Option<usize> {
654        match self {
655            WakeCandidates::Mask(m) => {
656                if *m == 0 {
657                    return None;
658                }
659                let idx = m.trailing_zeros() as usize;
660                *m &= *m - 1;
661                Some(idx)
662            }
663            WakeCandidates::Range(next, end) => {
664                if next < end {
665                    let idx = *next;
666                    *next += 1;
667                    Some(idx)
668                } else {
669                    None
670                }
671            }
672        }
673    }
674}
675
676// ============================================================================
677// Platform wait / wake. We do NOT use the atomic-wait crate
678// because that crate hard-codes FUTEX_PRIVATE_FLAG on Linux,
679// which restricts the futex to a single process and breaks the
680// cross-process wake claim. The waker calls the platform's
681// SHARED futex / WaitOnAddress / wake APIs directly.
682//
683// Every wait first runs the bounded MONITOR-class tier (see
684// crate::monitor_wait): MONITORX/MWAITX or UMONITOR/UMWAIT light
685// sleep on the slot's cache line for ~tens of microseconds, woken
686// for free by the producer's state store - cross-process included,
687// since hardware monitors are physical-address based. Only when
688// that budget expires does the wait escalate to the per-platform
689// kernel park below.
690//
691// Cross-process status per platform:
692// - Linux / Android: SHARED futex (no PRIVATE flag) works
693//   across processes when the atomic sits in a SHARED mmap.
694// - FreeBSD: _umtx_op with the non-PRIVATE UMTX_OP_WAIT_UINT /
695//   UMTX_OP_WAKE ops; the kernel keys those sleep queues by
696//   physical address ("same variable mapped multiple times will
697//   give one key value" - _umtx_op(2)), so waiters across
698//   processes sharing an MMF page join one queue.
699// - Windows: WaitOnAddress is INTRA-PROCESS only per the docs,
700//   so it serves anon-backed wakers; file / shm-backed wakers
701//   stay on the monitor tier for their whole wait (the
702//   cross_process flag below selects this), which IS
703//   cross-process because hardware monitors key on physical
704//   addresses. Hosts without MONITORX/WAITPKG fall back to the
705//   wait-timeout + re-check recovery in the blocking wrappers.
706// - macOS / others: polling fallback; correct but wastes CPU
707//   under heavy idle.
708// ============================================================================
709
710mod platform_wait {
711    use std::sync::atomic::AtomicU32;
712    use std::time::Duration;
713
714    /// macOS 14.4+ `os_sync_*` public-futex symbols resolved at RUNTIME via
715    /// `dlsym`, so the binary LINKS against an older SDK (e.g. 10.15, whose
716    /// libsystem has no `os_sync_*`) and degrades to the polling fallback there,
717    /// while taking the fast path on 14.4+. The flag constants are plain
718    /// integers (no link dependency), so only the three functions are resolved.
719    #[cfg(target_os = "macos")]
720    mod os_sync_dyn {
721        use std::ffi::c_void;
722        use std::sync::atomic::{AtomicUsize, Ordering};
723        pub type WaitFn = unsafe extern "C" fn(*mut c_void, u64, usize, u32) -> i32;
724        pub type WaitTimeoutFn =
725            unsafe extern "C" fn(*mut c_void, u64, usize, u32, u32, u64) -> i32;
726        pub type WakeFn = unsafe extern "C" fn(*mut c_void, usize, u32) -> i32;
727        // 1 = not yet probed, 0 = absent (older macOS), else = resolved fn ptr.
728        static WAIT: AtomicUsize = AtomicUsize::new(1);
729        static WAIT_TO: AtomicUsize = AtomicUsize::new(1);
730        static WAKE: AtomicUsize = AtomicUsize::new(1);
731        fn cached(slot: &AtomicUsize, name: &[u8]) -> usize {
732            let v = slot.load(Ordering::Relaxed);
733            if v != 1 {
734                return v;
735            }
736            // SAFETY: RTLD_DEFAULT lookup of a C symbol by NUL-terminated name.
737            let r = unsafe { libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr() as *const _) } as usize;
738            slot.store(r, Ordering::Relaxed);
739            r
740        }
741        pub fn wait() -> Option<WaitFn> {
742            match cached(&WAIT, b"os_sync_wait_on_address\0") {
743                0 => None,
744                // SAFETY: a non-null resolution of this symbol has this ABI.
745                p => Some(unsafe { std::mem::transmute::<usize, WaitFn>(p) }),
746            }
747        }
748        pub fn wait_timeout() -> Option<WaitTimeoutFn> {
749            match cached(&WAIT_TO, b"os_sync_wait_on_address_with_timeout\0") {
750                0 => None,
751                p => Some(unsafe { std::mem::transmute::<usize, WaitTimeoutFn>(p) }),
752            }
753        }
754        pub fn wake() -> Option<WakeFn> {
755            match cached(&WAKE, b"os_sync_wake_by_address_any\0") {
756                0 => None,
757                p => Some(unsafe { std::mem::transmute::<usize, WakeFn>(p) }),
758            }
759        }
760    }
761
762    /// The monitor tier: MONITORX/MWAITX (AMD) or UMONITOR/UMWAIT
763    /// (WAITPKG) light-sleep waiting for a bounded cycle budget
764    /// BEFORE the kernel park. Two wins when the wait resolves
765    /// inside the budget: the producer's wake is its existing
766    /// state-CAS (no syscall on either side), and - because
767    /// hardware monitors are physical-address based - the wake
768    /// crosses process boundaries on shared MMF pages, which on
769    /// Windows is the only non-polling cross-process wake the
770    /// platform offers (WaitOnAddress is intra-process). Returning
771    /// `false` (budget expired / unsupported CPU /
772    /// SUBETHA_NO_MONITOR_WAIT=1) falls through to the kernel
773    /// park, which re-checks the value itself - so the tier can
774    /// never lose a wake, only hand off.
775    #[inline]
776    fn monitor_tier(atomic: &AtomicU32, expected: u32) -> bool {
777        crate::monitor_wait::monitor_wait_u32(
778            atomic,
779            expected,
780            crate::monitor_wait::monitor_wait_budget_cycles(),
781        )
782    }
783
784    pub fn wait_forever(atomic: &AtomicU32, expected: u32, _cross_process: bool) {
785        if monitor_tier(atomic, expected) {
786            return;
787        }
788        // Windows + cross-process backing: WaitOnAddress never
789        // receives a wake from another process, so the monitor IS
790        // the wait - re-arm in budget-sized chunks until the value
791        // changes. The core holds C0.1 light sleep rather than
792        // releasing to the OS; that is the only non-polling
793        // cross-process wait the platform offers.
794        #[cfg(windows)]
795        if _cross_process
796            && crate::monitor_wait::monitor_wait_kind().is_some()
797        {
798            let budget = crate::monitor_wait::monitor_wait_budget_cycles();
799            while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
800                crate::monitor_wait::monitor_wait_u32(atomic, expected, budget);
801            }
802            return;
803        }
804        #[cfg(any(target_os = "linux", target_os = "android"))]
805        {
806            unsafe {
807                libc::syscall(
808                    libc::SYS_futex,
809                    atomic.as_ptr(),
810                    libc::FUTEX_WAIT,
811                    expected as libc::c_int,
812                    std::ptr::null::<libc::timespec>(),
813                );
814            }
815        }
816        #[cfg(target_os = "freebsd")]
817        {
818            // UMTX_OP_WAIT_UINT (the non-PRIVATE op): the kernel
819            // keys the sleep queue by the variable's PHYSICAL
820            // address, so waiters in any process that mapped the
821            // same MMF page share one queue - FreeBSD's native
822            // equivalent of the no-FUTEX_PRIVATE_FLAG Linux call.
823            // Sleeps only while *obj == val, exactly futex_wait.
824            unsafe {
825                libc::_umtx_op(
826                    atomic.as_ptr() as *mut libc::c_void,
827                    libc::UMTX_OP_WAIT_UINT,
828                    expected as libc::c_ulong,
829                    std::ptr::null_mut(),
830                    std::ptr::null_mut(),
831                );
832            }
833        }
834        #[cfg(windows)]
835        {
836            use windows_sys::Win32::System::Threading::{WaitOnAddress, INFINITE};
837            let expected_local = expected;
838            unsafe {
839                WaitOnAddress(
840                    atomic.as_ptr() as *const std::ffi::c_void,
841                    &expected_local as *const u32 as *const std::ffi::c_void,
842                    std::mem::size_of::<u32>(),
843                    INFINITE,
844                );
845            }
846        }
847        #[cfg(target_os = "macos")]
848        {
849            // The public futex (macOS 14.4+): compare-and-wait on the address;
850            // OS_SYNC_WAIT_ON_ADDRESS_SHARED keys the queue for a shared-memory
851            // address, allowing a futex wake from another process (the backing
852            // decides the flag). The symbol is resolved at runtime; on older
853            // macOS it is absent, so poll like the generic fallback below.
854            if let Some(wait_fn) = os_sync_dyn::wait() {
855                let flags = if _cross_process {
856                    libc::OS_SYNC_WAIT_ON_ADDRESS_SHARED
857                } else {
858                    libc::OS_SYNC_WAIT_ON_ADDRESS_NONE
859                };
860                unsafe {
861                    wait_fn(
862                        atomic.as_ptr() as *mut libc::c_void,
863                        expected as u64,
864                        std::mem::size_of::<u32>(),
865                        flags,
866                    );
867                }
868            } else {
869                std::thread::yield_now();
870                while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
871                    std::thread::sleep(Duration::from_millis(1));
872                }
873            }
874        }
875        #[cfg(not(any(target_os = "linux", target_os = "android",
876                      target_os = "freebsd", target_os = "macos", windows)))]
877        {
878            std::thread::yield_now();
879            while atomic.load(std::sync::atomic::Ordering::Acquire) == expected {
880                std::thread::sleep(Duration::from_millis(1));
881            }
882        }
883    }
884
885    pub fn wait_with_timeout(
886        atomic: &AtomicU32,
887        expected: u32,
888        timeout: Duration,
889        _cross_process: bool,
890    ) -> bool {
891        let monitor_start = std::time::Instant::now();
892        if monitor_tier(atomic, expected) {
893            return true;
894        }
895        // The monitor budget counts against the caller's timeout;
896        // the kernel park gets the remainder.
897        let timeout = match timeout.checked_sub(monitor_start.elapsed()) {
898            Some(rest) if !rest.is_zero() => rest,
899            _ => {
900                return atomic.load(std::sync::atomic::Ordering::Acquire)
901                    != expected;
902            }
903        };
904        // Windows + cross-process backing: stay on the monitor for
905        // the whole timeout (see wait_forever).
906        #[cfg(windows)]
907        if _cross_process
908            && crate::monitor_wait::monitor_wait_kind().is_some()
909        {
910            let deadline = std::time::Instant::now() + timeout;
911            let budget = crate::monitor_wait::monitor_wait_budget_cycles();
912            loop {
913                if crate::monitor_wait::monitor_wait_u32(atomic, expected, budget) {
914                    return true;
915                }
916                if std::time::Instant::now() >= deadline {
917                    return atomic.load(std::sync::atomic::Ordering::Acquire)
918                        != expected;
919                }
920            }
921        }
922        #[cfg(any(target_os = "linux", target_os = "android"))]
923        {
924            let ts = libc::timespec {
925                tv_sec: timeout.as_secs() as libc::time_t,
926                tv_nsec: timeout.subsec_nanos() as libc::c_long,
927            };
928            unsafe {
929                let rc = libc::syscall(
930                    libc::SYS_futex,
931                    atomic.as_ptr(),
932                    libc::FUTEX_WAIT,
933                    expected as libc::c_int,
934                    &ts as *const libc::timespec,
935                    std::ptr::null::<()>(),
936                    0u32,
937                );
938                if rc == -1 {
939                    let err = *libc::__errno_location();
940                    return err != libc::ETIMEDOUT;
941                }
942            }
943            true
944        }
945        #[cfg(target_os = "freebsd")]
946        {
947            // Relative timeout, monotonic clock by default: uaddr2
948            // points at the timespec and uaddr carries that
949            // structure's size, per _umtx_op(2).
950            let mut ts = libc::timespec {
951                tv_sec: timeout.as_secs() as libc::time_t,
952                tv_nsec: timeout.subsec_nanos() as libc::c_long,
953            };
954            let rc = unsafe {
955                libc::_umtx_op(
956                    atomic.as_ptr() as *mut libc::c_void,
957                    libc::UMTX_OP_WAIT_UINT,
958                    expected as libc::c_ulong,
959                    std::mem::size_of::<libc::timespec>() as *mut libc::c_void,
960                    &mut ts as *mut libc::timespec as *mut libc::c_void,
961                )
962            };
963            if rc == -1 {
964                let err = unsafe { *libc::__error() };
965                return err != libc::ETIMEDOUT;
966            }
967            true
968        }
969        #[cfg(windows)]
970        {
971            use windows_sys::Win32::System::Threading::WaitOnAddress;
972            let expected_local = expected;
973            let ms = timeout.as_millis().min(u32::MAX as u128) as u32;
974            let rc = unsafe {
975                WaitOnAddress(
976                    atomic.as_ptr() as *const std::ffi::c_void,
977                    &expected_local as *const u32 as *const std::ffi::c_void,
978                    std::mem::size_of::<u32>(),
979                    ms,
980                )
981            };
982            rc != 0
983        }
984        #[cfg(target_os = "macos")]
985        {
986            if let Some(wait_fn) = os_sync_dyn::wait_timeout() {
987                let flags = if _cross_process {
988                    libc::OS_SYNC_WAIT_ON_ADDRESS_SHARED
989                } else {
990                    libc::OS_SYNC_WAIT_ON_ADDRESS_NONE
991                };
992                let rc = unsafe {
993                    wait_fn(
994                        atomic.as_ptr() as *mut libc::c_void,
995                        expected as u64,
996                        std::mem::size_of::<u32>(),
997                        flags,
998                        libc::OS_CLOCK_MACH_ABSOLUTE_TIME,
999                        timeout.as_nanos().min(u64::MAX as u128) as u64,
1000                    )
1001                };
1002                if rc < 0 {
1003                    return std::io::Error::last_os_error().raw_os_error()
1004                        != Some(libc::ETIMEDOUT);
1005                }
1006                true
1007            } else {
1008                // Older macOS (< 14.4): poll with a deadline, like the generic
1009                // fallback. Returns true if the value changed, false on timeout.
1010                let deadline = std::time::Instant::now() + timeout;
1011                loop {
1012                    if atomic.load(std::sync::atomic::Ordering::Acquire) != expected {
1013                        return true;
1014                    }
1015                    if std::time::Instant::now() >= deadline {
1016                        return atomic.load(std::sync::atomic::Ordering::Acquire) != expected;
1017                    }
1018                    std::thread::sleep(Duration::from_millis(2));
1019                }
1020            }
1021        }
1022        #[cfg(not(any(target_os = "linux", target_os = "android",
1023                      target_os = "freebsd", target_os = "macos", windows)))]
1024        {
1025            let deadline = std::time::Instant::now() + timeout;
1026            let step = Duration::from_millis(2);
1027            loop {
1028                if atomic.load(std::sync::atomic::Ordering::Acquire) != expected {
1029                    return true;
1030                }
1031                let now = std::time::Instant::now();
1032                if now >= deadline {
1033                    return false;
1034                }
1035                let remaining = deadline - now;
1036                std::thread::sleep(remaining.min(step));
1037            }
1038        }
1039    }
1040
1041    pub fn wake_one(atomic: &AtomicU32, _cross_process: bool) {
1042        #[cfg(any(target_os = "linux", target_os = "android"))]
1043        {
1044            unsafe {
1045                libc::syscall(
1046                    libc::SYS_futex,
1047                    atomic.as_ptr(),
1048                    libc::FUTEX_WAKE,
1049                    1i32,
1050                );
1051            }
1052        }
1053        #[cfg(target_os = "freebsd")]
1054        {
1055            // val = max threads to wake; same shared (non-PRIVATE)
1056            // physical-address-keyed queue the waiters parked on.
1057            unsafe {
1058                libc::_umtx_op(
1059                    atomic.as_ptr() as *mut libc::c_void,
1060                    libc::UMTX_OP_WAKE,
1061                    1 as libc::c_ulong,
1062                    std::ptr::null_mut(),
1063                    std::ptr::null_mut(),
1064                );
1065            }
1066        }
1067        #[cfg(windows)]
1068        {
1069            use windows_sys::Win32::System::Threading::WakeByAddressSingle;
1070            unsafe {
1071                WakeByAddressSingle(atomic.as_ptr() as *const std::ffi::c_void);
1072            }
1073        }
1074        #[cfg(target_os = "macos")]
1075        {
1076            // Mirror of the wait flag: SHARED wakes waiters in any process that
1077            // mapped the same region. On older macOS the symbol is absent and
1078            // the waiter polls, so no explicit wake is needed.
1079            if let Some(wake_fn) = os_sync_dyn::wake() {
1080                let flags = if _cross_process {
1081                    libc::OS_SYNC_WAKE_BY_ADDRESS_SHARED
1082                } else {
1083                    libc::OS_SYNC_WAKE_BY_ADDRESS_NONE
1084                };
1085                unsafe {
1086                    wake_fn(
1087                        atomic.as_ptr() as *mut libc::c_void,
1088                        std::mem::size_of::<u32>(),
1089                        flags,
1090                    );
1091                }
1092            }
1093        }
1094        #[cfg(not(any(target_os = "linux", target_os = "android",
1095                      target_os = "freebsd", target_os = "macos", windows)))]
1096        {
1097            drop(atomic);
1098        }
1099    }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use super::*;
1105    use std::sync::Arc;
1106    use std::thread;
1107    use std::time::Instant;
1108
1109    #[test]
1110    fn anon_round_trip() {
1111        let waker = CrossProcessWaker::create_anon(4).expect("create");
1112        let token = waker.try_park(10).expect("park");
1113        let waker_c = Arc::new(waker);
1114        let waker_p = Arc::clone(&waker_c);
1115        let h = thread::spawn(move || {
1116            thread::sleep(Duration::from_millis(20));
1117            let n = waker_p.wake_up_to(15);
1118            assert!(n >= 1);
1119        });
1120        waker_c.wait(token, Some(Duration::from_secs(2))).expect("wake");
1121        h.join().unwrap();
1122    }
1123
1124    #[test]
1125    fn wait_returns_immediately_if_already_woken() {
1126        let waker = CrossProcessWaker::create_anon(2).expect("create");
1127        let token = waker.try_park(5).expect("park");
1128        assert_eq!(waker.wake_up_to(10), 1);
1129        let t0 = Instant::now();
1130        waker.wait(token, Some(Duration::from_secs(1))).expect("ok");
1131        assert!(t0.elapsed() < Duration::from_millis(50),
1132                "wait should return fast since wake fired before wait entered");
1133    }
1134
1135    #[test]
1136    fn timeout_works() {
1137        let waker = CrossProcessWaker::create_anon(2).expect("create");
1138        let token = waker.try_park(100).expect("park");
1139        let t0 = Instant::now();
1140        let err = waker.wait(token, Some(Duration::from_millis(80)));
1141        assert_eq!(err, Err(WakerError::Timeout));
1142        assert!(t0.elapsed() >= Duration::from_millis(70));
1143    }
1144
1145    #[test]
1146    fn full_when_all_slots_taken() {
1147        let waker = CrossProcessWaker::create_anon(2).expect("create");
1148        let _a = waker.try_park(1).expect("park 0");
1149        let _b = waker.try_park(2).expect("park 1");
1150        assert_eq!(waker.try_park(3), Err(WakerError::Full));
1151    }
1152
1153    #[test]
1154    fn release_lets_others_park() {
1155        let waker = CrossProcessWaker::create_anon(2).expect("create");
1156        let a = waker.try_park(1).expect("park 0");
1157        let _b = waker.try_park(2).expect("park 1");
1158        waker.release(a);
1159        let _c = waker.try_park(3).expect("re-park 0");
1160    }
1161
1162    #[test]
1163    fn wake_all_drains_blocked_consumers() {
1164        let waker = Arc::new(CrossProcessWaker::create_anon(4).expect("create"));
1165        let mut handles = Vec::new();
1166        for target in 0..4u64 {
1167            let w = Arc::clone(&waker);
1168            let token = w.try_park(target + 1000).expect("park");
1169            handles.push(thread::spawn(move || {
1170                w.wait(token, Some(Duration::from_secs(2))).expect("woken");
1171            }));
1172        }
1173        thread::sleep(Duration::from_millis(20));
1174        assert_eq!(waker.wake_all(), 4);
1175        for h in handles { h.join().unwrap(); }
1176    }
1177}