Skip to main content

epics_libcom_rs/runtime/
sync.rs

1// Re-export tokio sync primitives through the runtime facade.
2pub use std::sync::Arc;
3pub use tokio::sync::{Mutex, Notify, RwLock, broadcast, mpsc, oneshot};
4
5/// Priority-inheritance mutex for real-time builds (epics-base 7-E
6/// `5a8b6e41` "epicsMutex priority inheritance"). On Linux with the
7/// `linux-rt` Cargo feature enabled this wraps a `pthread_mutex_t`
8/// configured with `PTHREAD_PRIO_INHERIT` so a low-priority holder
9/// inherits the priority of the highest-priority waiter — preventing
10/// the classic priority-inversion deadlock that bit C epicsMutex on
11/// PREEMPT_RT kernels.
12///
13/// RTEMS gets the same `pthread_mutex_t` construction on its own arm
14/// below, unconditionally. On every other target — including a default
15/// (non-`linux-rt`) Linux build — this is a transparent type alias for
16/// [`parking_lot::Mutex`], which is
17/// already non-poisoning, smaller, and faster than `std::sync::Mutex`
18/// for typical IOC workloads. Callers see the same API surface — the
19/// PI variant only matters when the OS scheduler can preempt a thread
20/// holding the lock, i.e. when the runtime is configured for
21/// real-time scheduling.
22///
23/// Note: tokio's async `Mutex` is unaffected — async tasks don't have
24/// OS-level priorities to invert. `PriorityInheritanceMutex` is for
25/// the rare blocking-Sync code paths (device-support callbacks, some
26/// tracing sinks) where we hold a mutex while running with
27/// SCHED_FIFO / SCHED_RR.
28#[cfg(all(target_os = "linux", feature = "linux-rt"))]
29type MutexBackend<T> = pi_mutex::PiMutex<T>;
30
31/// The RTEMS arm, and it is **not** behind a Cargo feature.
32///
33/// `linux-rt` exists because a desktop Linux build must opt in: the request
34/// can fail on a box without `CAP_SYS_NICE`, and a runaway RT band on a box
35/// that grants it wedges a developer's machine. Neither failure mode exists on
36/// RTEMS, which is the same reasoning that makes
37/// [`crate::runtime::task::DEFAULT_POLICY`] `AllowRealtime` there
38/// (`task.rs:591-603`) — the target has no way to set an env var and no
39/// privilege gate to fail.
40///
41/// The parity target is a POSIX PI mutex, not an RTEMS classic-API semaphore:
42/// base-on-RTEMS-6 compiles the POSIX arm (`configure/toolchain.c:31-35`
43/// selects `OS_API = posix` for `__RTEMS_MAJOR__ >= 5`, and
44/// `os/RTEMS-posix/osdMutex.c:8` is a single `#include "../posix/osdMutex.c"`).
45/// So this is the same construction on the same API as C — including C's
46/// probe-and-fall-back, see `pi_mutex::protocol` below.
47#[cfg(target_os = "rtems")]
48type MutexBackend<T> = pi_mutex::PiMutex<T>;
49
50/// Non-RT fallback — uses `parking_lot::Mutex` for the common case.
51/// PI semantics are not needed because the scheduler does not
52/// preempt the lock holder by priority.
53#[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
54type MutexBackend<T> = parking_lot::Mutex<T>;
55
56/// C `epicsMutexId` — the mutex every EPICS lock in this port is built from,
57/// and the only one `epicsMutexShowAll` can see.
58///
59/// A newtype over `MutexBackend` rather than a bare alias to it, for the
60/// same reason C wraps a `pthread_mutex_t` in an `epicsMutexParm`: the mutex
61/// has to be *findable*. C `calloc`s a node carrying the creation site,
62/// `ellAdd`s it to a process-global `mutexList` under `epicsMutexGlobalLock`
63/// (`epicsMutex.cpp:72-93`), and `ellDelete`s it in `epicsMutexDestroy`
64/// (`:105-113`); `epicsMutexShowAll` then walks that list, and — this is what
65/// forces the shape — *try-locks each entry* to answer `onlyLocked`
66/// (`:129-146`).
67///
68/// Try-locking from the list means the list must hold something addressable,
69/// so the backend goes in a `Box` allocated before registration and freed
70/// after deregistration. That is C's node, exactly: a value that never moves
71/// for its whole life. The public type moving is then a pointer move, which
72/// is why the twenty call sites — struct fields, `Box<[_]>` elements,
73/// `Box::leak`ed gates — need no change and cannot invalidate an entry.
74/// Registering the value itself instead would leave a dangling address the
75/// first time a caller moved one, and `PvDatabase::new` moves two into an
76/// `Arc` at IOC init.
77///
78/// The creation site is [`std::panic::Location::caller`], which is C's
79/// `__FILE__`/`__LINE__` reached without a macro: `epicsMutexCreate` is
80/// `epicsMutexOsiCreate(__FILE__, __LINE__)` and `#[track_caller]` records the
81/// same two facts at the same place.
82pub type PriorityInheritanceMutex<T> = epics_mutex::EpicsMutex<T>;
83
84/// The guard `PriorityInheritanceMutex::lock` hands out, nameable so a
85/// caller can *store* one — the per-record write gate
86/// (`server::database::record_lock`) holds a `'static` guard in a struct
87/// field rather than a local.
88///
89/// `!Send` on every arm, and on the PI arms that is a correctness
90/// requirement rather than a lint: POSIX requires a mutex to be unlocked by
91/// the thread that locked it, so a guard that could migrate between threads
92/// would eventually call `pthread_mutex_unlock` from a non-owner. It is also
93/// what makes "no `.await` inside a lock window" a build error at every
94/// spawn site instead of a review convention.
95#[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
96pub type PriorityInheritanceMutexGuard<'a, T> = pi_mutex::PiMutexGuard<'a, T>;
97
98/// See [`PriorityInheritanceMutexGuard`] — `parking_lot`'s guard is already
99/// `!Send` for the same reason.
100#[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
101pub type PriorityInheritanceMutexGuard<'a, T> = parking_lot::MutexGuard<'a, T>;
102
103/// Diagnostic — `true` when the PI variant is active in this build.
104///
105/// On RTEMS this is **not** a `cfg!`: PI there is a probe, exactly as it is in
106/// C (`posix/osdMutex.c:77-85`), so the answer is what the probe obtained
107/// rather than what the build selected. See `pi_mutex::protocol` below.
108pub fn is_pi_mutex_active() -> bool {
109    #[cfg(target_os = "rtems")]
110    {
111        pi_mutex::protocol() == rtems_pi::PTHREAD_PRIO_INHERIT
112    }
113    #[cfg(not(target_os = "rtems"))]
114    {
115        cfg!(all(target_os = "linux", feature = "linux-rt"))
116    }
117}
118
119/// Print, once at boot, which lock protocol this process actually obtained.
120///
121/// C's counterpart is `epicsMutexShowAll`, which reports "PI is/is not
122/// enabled" (`os/posix/osdMutex.c:199-205`) — but that is an iocsh command,
123/// and the RTEMS target has no iocsh. So this is an `eprintln!` from the boot
124/// path, deliberately not a `tracing` event: a subscriber can be absent,
125/// installed late, or filtered, and a diagnostic that reports whether a
126/// guarantee exists must not itself depend on one.
127///
128/// **Two facts on one line, because either alone is misleading.** Priority
129/// inheritance on the record gate needs *both* the probe to have returned
130/// `PTHREAD_PRIO_INHERIT` ([`is_pi_mutex_active`]) *and* the contending
131/// threads to carry distinct scheduling priorities, which is
132/// [`RtPolicy::AllowRealtime`](crate::runtime::task::RtPolicy::AllowRealtime)
133/// — with the RT switch off every thread is one priority and PI has nothing
134/// to inherit (`server::database::record_lock`'s module doc states the same
135/// pair). Printing "PI enabled" beside a disabled RT policy would claim an
136/// ordering the process does not have.
137pub fn report_lock_protocol() {
138    let pi = if is_pi_mutex_active() {
139        "PI is enabled"
140    } else {
141        "PI is not enabled"
142    };
143    eprintln!(
144        "epics-rs: lock protocol: {pi}, RT scheduling {:?}",
145        crate::runtime::task::RtPolicy::current()
146    );
147}
148
149/// The RTEMS priority-protocol surface, declared here rather than taken from
150/// `libc`.
151///
152/// `libc` (the pinned fork, `Cargo.toml`'s `[patch.crates-io]`) declares
153/// `pthread_mutexattr_setprotocol` only for cygwin/qurt/teeos/aix and the
154/// musl/glibc/uclibc modules, and defines `PTHREAD_PRIO_NONE`/`_INHERIT`/
155/// `_PROTECT` only for aix/vxworks/l4re/qurt/apple/hurd/linux — there is no
156/// newlib/rtems arm for either. Both exist on the target: the prototype in
157/// newlib's `pthread.h:189-206`, the constants in `sys/_pthreadtypes.h:81-83`,
158/// and `sys/features.h:394-395` turns `_POSIX_THREAD_PRIO_INHERIT` on
159/// unconditionally for `__rtems__`. So the symbols are there at link time and
160/// only the Rust binding is missing — the identical situation
161/// [`crate::runtime::task`]'s `rtems_sched` block (`task.rs:987-1056`) already
162/// solved for `pthread_setschedparam`.
163///
164/// Deliberately **not** redeclared here: `pthread_mutex_t`,
165/// `pthread_mutexattr_t`, and the `pthread_mutex_*` / `pthread_mutexattr_init`
166/// / `_destroy` functions. `libc` carries all of them for RTEMS at the
167/// target's own widths (`__SIZEOF_PTHREAD_MUTEX_T = 64`,
168/// `__SIZEOF_PTHREAD_MUTEXATTR_T = 24`, `src/unix/newlib/mod.rs:283`, `:329`,
169/// `:392-393`). Restating a struct layout locally is how the `timespec` and
170/// `sockaddr` defects happened; this block declares a function and two
171/// integers and no layout at all.
172#[cfg(target_os = "rtems")]
173mod rtems_pi {
174    use std::ffi::c_int;
175
176    /// `sys/_pthreadtypes.h:81-83` on the arm-rtems6 toolchain.
177    pub const PTHREAD_PRIO_NONE: c_int = 0;
178    /// `sys/_pthreadtypes.h:81-83` on the arm-rtems6 toolchain.
179    pub const PTHREAD_PRIO_INHERIT: c_int = 1;
180
181    unsafe extern "C" {
182        /// `pthread.h:189-206`; implemented in
183        /// `cpukit/posix/src/mutexattrsetprotocol.c`. Absent from `libc`'s
184        /// `newlib/rtems` module.
185        pub fn pthread_mutexattr_setprotocol(
186            attr: *mut libc::pthread_mutexattr_t,
187            protocol: c_int,
188        ) -> c_int;
189    }
190}
191
192/// `pthread_mutex_t` with the target's priority protocol — one implementation
193/// for both PI targets.
194///
195/// Linux and RTEMS differ in exactly one thing — where the protocol constant
196/// comes from (`protocol` below) — so they share the mutex, the guard and the
197/// `unsafe` rather than carrying two copies that must be fixed twice.
198#[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
199mod pi_mutex {
200    use std::cell::UnsafeCell;
201    use std::ffi::c_int;
202    use std::ops::{Deref, DerefMut};
203
204    #[cfg(target_os = "rtems")]
205    use super::rtems_pi;
206    #[cfg(target_os = "rtems")]
207    use super::rtems_pi::pthread_mutexattr_setprotocol;
208    #[cfg(not(target_os = "rtems"))]
209    use libc::pthread_mutexattr_setprotocol;
210
211    /// The protocol every mutex in this process is built with.
212    ///
213    /// On Linux this is `PTHREAD_PRIO_INHERIT` unconditionally: the caller
214    /// asked for `linux-rt`, glibc supports the protocol, and a failure is a
215    /// misconfiguration worth panicking on.
216    #[cfg(not(target_os = "rtems"))]
217    pub fn protocol() -> c_int {
218        libc::PTHREAD_PRIO_INHERIT
219    }
220
221    /// The protocol this process actually obtained, probed once.
222    ///
223    /// C does not assert PI on POSIX — it *probes* it. `globalAttrInit`
224    /// (`posix/osdMutex.c:71-88`) sets `PTHREAD_PRIO_INHERIT` on the global
225    /// attributes, builds one temporary mutex with them, and on failure
226    /// silently downgrades both attributes to `PTHREAD_PRIO_NONE`
227    /// (`:81-85`). `epicsMutexShowAll` then reports which it got
228    /// (`:199-205`).
229    ///
230    /// We match that rather than the Linux arm's `assert_eq!`, and the reason
231    /// is target-specific: the RTEMS IOC installs no `tracing` subscriber and
232    /// has no iocsh, so a panic in a lock constructor during boot is the worst
233    /// available failure mode — it is both fatal and silent. Degrading to a
234    /// plain mutex loses priority inheritance and says so through
235    /// [`is_pi_mutex_active`](super::is_pi_mutex_active); panicking loses the
236    /// IOC.
237    ///
238    /// Probed once per process, so the answer is one fact and not a per-mutex
239    /// race — the `OnceLock` is this file's analogue of C's `pthread_once`.
240    #[cfg(target_os = "rtems")]
241    pub fn protocol() -> c_int {
242        static PROTOCOL: std::sync::OnceLock<c_int> = std::sync::OnceLock::new();
243        *PROTOCOL.get_or_init(|| {
244            // SAFETY: `attr` and `probe` are stack locals of libc's own RTEMS
245            // widths, handed only to the pthread calls that own them, and each
246            // is destroyed on every path that initialised it.
247            unsafe {
248                let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
249                if libc::pthread_mutexattr_init(&mut attr) != 0 {
250                    return rtems_pi::PTHREAD_PRIO_NONE;
251                }
252                let mut obtained = rtems_pi::PTHREAD_PRIO_NONE;
253                // C probes only when `setprotocol` itself succeeded
254                // (`osdMutex.c:76`), and treats a failed temporary
255                // `pthread_mutex_init` as "PI does not work here" (`:81`).
256                if pthread_mutexattr_setprotocol(&mut attr, rtems_pi::PTHREAD_PRIO_INHERIT) == 0 {
257                    let mut probe: libc::pthread_mutex_t = std::mem::zeroed();
258                    if libc::pthread_mutex_init(&mut probe, &attr) == 0 {
259                        libc::pthread_mutex_destroy(&mut probe);
260                        obtained = rtems_pi::PTHREAD_PRIO_INHERIT;
261                    }
262                }
263                libc::pthread_mutexattr_destroy(&mut attr);
264                obtained
265            }
266        })
267    }
268
269    /// The `pthread_mutex_t` lives behind a `Box`, and that is a correctness
270    /// requirement on RTEMS rather than a layout preference.
271    ///
272    /// RTEMS 6 binds a POSIX mutex to **its own address**: `pthread_mutex_init`
273    /// stores `flags = ((uintptr_t) mutex ^ POSIX_MUTEX_MAGIC) | protocol`, and
274    /// every later operation recomputes that from the address it is handed —
275    /// `POSIX_MUTEX_VALIDATE_OBJECT` (`rtems/posix/muteximpl.h:445-459` (`rtems_6`),
276    /// magic at `:64`) returns **`EINVAL`** when they disagree. So a
277    /// `pthread_mutex_t` that is *relocated* after being initialised is dead:
278    /// not slow, not unordered — every `lock` fails.
279    ///
280    /// Held inline, that is exactly what happened. `new` initialised the mutex
281    /// in a stack local and then moved the struct out by value (and callers
282    /// move it again — `record_lock`'s gates are `Box::leak(Box::new(…))`), so
283    /// on target the first `lock()` returned 22 and the assertion below took
284    /// the IOC down at boot. Measured; invisible on Linux because glibc's
285    /// mutex carries no address in its state and survives the move.
286    ///
287    /// Boxing makes the invariant hold **by construction**: the mutex is
288    /// allocated first and initialised at the address it will keep for its
289    /// whole life, and moving a `PiMutex` moves a pointer. There is no move to
290    /// forbid, so there is no runtime check, no `Pin` in the public API and
291    /// nothing for a future call site to get wrong. It is also what `std` does
292    /// for the same reason on every platform whose `pthread_mutex_t` cannot be
293    /// relocated.
294    ///
295    /// **Not** fixed by zeroing and letting RTEMS auto-initialise: the
296    /// auto-initialisation arm of that macro exists for
297    /// `PTHREAD_MUTEX_INITIALIZER`, and it produces a *default* mutex — no
298    /// protocol, i.e. no priority inheritance. That path would report success
299    /// while silently giving up the property this type exists for.
300    pub struct PiMutex<T> {
301        inner: Box<UnsafeCell<libc::pthread_mutex_t>>,
302        data: UnsafeCell<T>,
303    }
304
305    unsafe impl<T: Send> Send for PiMutex<T> {}
306    unsafe impl<T: Send> Sync for PiMutex<T> {}
307
308    impl<T> PiMutex<T> {
309        pub fn new(value: T) -> Self {
310            // Allocated before it is initialised, so `pthread_mutex_init` sees
311            // the address the mutex keeps for life — see the type's doc.
312            let mutex: Box<UnsafeCell<libc::pthread_mutex_t>> =
313                Box::new(UnsafeCell::new(unsafe { std::mem::zeroed() }));
314            unsafe {
315                let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
316                let r = libc::pthread_mutexattr_init(&mut attr);
317                assert_eq!(r, 0, "pthread_mutexattr_init failed");
318                // On RTEMS `protocol()` is the probed value, so this call and
319                // the `pthread_mutex_init` below are being made with exactly
320                // the arguments the probe already proved acceptable. What
321                // remains assertable here is the ENOMEM class, which is C's
322                // `cantProceed` path (`osdMutex.c:98`), not the
323                // PI-unavailable path C degrades on.
324                let protocol = protocol();
325                let r = pthread_mutexattr_setprotocol(&mut attr, protocol);
326                assert_eq!(r, 0, "pthread_mutexattr_setprotocol({protocol}) failed");
327                let r = libc::pthread_mutex_init(mutex.get(), &attr);
328                assert_eq!(r, 0, "pthread_mutex_init failed");
329                libc::pthread_mutexattr_destroy(&mut attr);
330            }
331            Self {
332                inner: mutex,
333                data: UnsafeCell::new(value),
334            }
335        }
336
337        /// The address `pthread_mutex_init` was called with — the one RTEMS
338        /// validates every later operation against, and the one
339        /// `epicsMutexOsdShow` prints as `uaddr`.
340        pub fn raw_addr(&self) -> usize {
341            self.inner.get() as usize
342        }
343
344        /// `pthread_mutex_trylock`, the call C's `onlyLocked` filter makes on
345        /// every list entry (`epicsMutex.cpp:137-142`).
346        pub fn try_lock(&self) -> Option<PiMutexGuard<'_, T>> {
347            // SAFETY: `trylock` never blocks, and the guard returned here
348            // unlocks from this same thread, so POSIX's unlock-by-owner rule
349            // holds.
350            if unsafe { libc::pthread_mutex_trylock(self.inner.get()) } != 0 {
351                return None;
352            }
353            Some(PiMutexGuard {
354                mutex: self,
355                _not_send: std::marker::PhantomData,
356            })
357        }
358
359        pub fn lock(&self) -> PiMutexGuard<'_, T> {
360            unsafe {
361                let r = libc::pthread_mutex_lock(self.inner.get());
362                assert_eq!(r, 0, "pthread_mutex_lock failed");
363            }
364            PiMutexGuard {
365                mutex: self,
366                _not_send: std::marker::PhantomData,
367            }
368        }
369    }
370
371    impl<T> Drop for PiMutex<T> {
372        fn drop(&mut self) {
373            unsafe {
374                libc::pthread_mutex_destroy(self.inner.get());
375            }
376        }
377    }
378
379    /// The `parking_lot::Mutex` arm has `Debug`, so this arm must too.
380    ///
381    /// [`PriorityInheritanceMutex`](super::PriorityInheritanceMutex) is one
382    /// type alias with three `cfg` arms, and any trait the fallback arm has
383    /// that a PI arm lacks is a build break that only ever appears on the
384    /// target: a `#[derive(Debug)]` on a struct holding one compiles on a
385    /// developer's box and fails for `armv7-rtems-eabihf`. That is how this
386    /// impl was found — `qsrv::group_config::GroupPvDef` (`#[derive(Debug,
387    /// Clone)]`, holding the L33 atomic-write lock) is the first such struct,
388    /// and it broke nothing until the crate entered the RTEMS gate. The
389    /// guard's `!Send` note above already states the rule for auto traits;
390    /// this is the same rule for the named ones.
391    ///
392    /// `try_lock` rather than `lock`, matching `parking_lot`: a `Debug` impl
393    /// that blocks deadlocks the moment anything formats a structure while the
394    /// lock is held — including a panic message on the locking thread.
395    impl<T: std::fmt::Debug> std::fmt::Debug for PiMutex<T> {
396        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397            // SAFETY: `trylock` never blocks. On success this thread owns the
398            // mutex for the read below and unlocks before returning, so the
399            // POSIX "unlock by the locking thread" rule holds.
400            let acquired = unsafe { libc::pthread_mutex_trylock(self.inner.get()) } == 0;
401            if !acquired {
402                return f
403                    .debug_struct("PiMutex")
404                    .field("data", &"<locked>")
405                    .finish();
406            }
407            let out = f
408                .debug_struct("PiMutex")
409                .field("data", unsafe { &*self.data.get() })
410                .finish();
411            unsafe {
412                libc::pthread_mutex_unlock(self.inner.get());
413            }
414            out
415        }
416    }
417
418    pub struct PiMutexGuard<'a, T> {
419        mutex: &'a PiMutex<T>,
420        /// Makes the guard `!Send`, which `&PiMutex<T>` alone does not.
421        ///
422        /// Not a lint: POSIX requires `pthread_mutex_unlock` to be called by
423        /// the thread that locked the mutex (and an RTEMS PI mutex enforces
424        /// ownership), so a guard that could be moved to another thread and
425        /// dropped there would unlock from a non-owner. `parking_lot`'s
426        /// guard — the arm the host build compiles — is `!Send` for the same
427        /// reason, so this also keeps the two arms' auto traits identical
428        /// and the "no `.await` under a lock" build error target-independent.
429        _not_send: std::marker::PhantomData<*const ()>,
430    }
431
432    impl<T> Deref for PiMutexGuard<'_, T> {
433        type Target = T;
434        fn deref(&self) -> &T {
435            unsafe { &*self.mutex.data.get() }
436        }
437    }
438
439    impl<T> DerefMut for PiMutexGuard<'_, T> {
440        fn deref_mut(&mut self) -> &mut T {
441            unsafe { &mut *self.mutex.data.get() }
442        }
443    }
444
445    impl<T> Drop for PiMutexGuard<'_, T> {
446        fn drop(&mut self) {
447            unsafe {
448                libc::pthread_mutex_unlock(self.mutex.inner.get());
449            }
450        }
451    }
452}
453
454/// C `epicsMutex.cpp`'s `mutexList` and the node on it.
455///
456/// The whole module exists so `epicsMutexShowAll` can answer the question it
457/// is run to answer — *which lock is held right now* — rather than only how
458/// many exist. That answer needs a try-lock through a stable address, which is
459/// what pins the `Box` and the `Drop`.
460mod epics_mutex {
461    use std::sync::Mutex;
462    use std::sync::atomic::{AtomicU64, Ordering};
463
464    use super::MutexBackend;
465
466    /// One entry of C's `mutexList` (`epicsMutex.cpp:39`).
467    ///
468    /// `probe` is the entry's own `try_lock`, monomorphised for the `T` that
469    /// registered it and then type-erased to a plain function pointer. C needs
470    /// no equivalent because its node's payload is opaque bytes; here the
471    /// backend is generic, and the list must be one list.
472    struct Entry {
473        id: u64,
474        file: &'static str,
475        line: u32,
476        addr: usize,
477        osd_addr: usize,
478        probe: unsafe fn(usize) -> bool,
479    }
480
481    /// Creation order, as C's `ellAdd` appends.
482    static MUTEXES: Mutex<Vec<Entry>> = Mutex::new(Vec::new());
483    static NEXT_ID: AtomicU64 = AtomicU64::new(1);
484
485    /// A poisoned list is still a readable list — the same reasoning as the
486    /// thread registry's: a panic while formatting one row must not make the
487    /// IOC's lock report permanently unavailable.
488    fn lock() -> std::sync::MutexGuard<'static, Vec<Entry>> {
489        MUTEXES.lock().unwrap_or_else(|e| e.into_inner())
490    }
491
492    /// C `epicsMutexId` (`epicsMutex.cpp:72-93`) — see
493    /// [`PriorityInheritanceMutex`](super::PriorityInheritanceMutex) for why
494    /// this is a newtype and not an alias.
495    pub struct EpicsMutex<T> {
496        /// Allocated before registration and freed after deregistration, so
497        /// the address in the list is valid for exactly as long as the list
498        /// holds it.
499        inner: Box<MutexBackend<T>>,
500        id: u64,
501    }
502
503    impl<T> EpicsMutex<T> {
504        /// C `epicsMutexCreate()` — `epicsMutexOsiCreate(__FILE__, __LINE__)`.
505        #[track_caller]
506        pub fn new(value: T) -> Self {
507            let inner = Box::new(MutexBackend::new(value));
508            let caller = std::panic::Location::caller();
509            let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
510            lock().push(Entry {
511                id,
512                file: caller.file(),
513                line: caller.line(),
514                addr: &*inner as *const MutexBackend<T> as usize,
515                osd_addr: osd_addr(&inner),
516                probe: probe_locked::<T>,
517            });
518            Self { inner, id }
519        }
520
521        pub fn lock(&self) -> super::PriorityInheritanceMutexGuard<'_, T> {
522            self.inner.lock()
523        }
524
525        /// C `epicsMutexTryLock`. Also what [`report`] calls through `probe`.
526        pub fn try_lock(&self) -> Option<super::PriorityInheritanceMutexGuard<'_, T>> {
527            self.inner.try_lock()
528        }
529
530        /// The address `pthread_mutex_init` was called with — the one RTEMS
531        /// validates every later operation against, and the one C's
532        /// `epicsMutexOsdShow` prints as `uaddr`.
533        #[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
534        pub fn raw_addr(&self) -> usize {
535            self.inner.raw_addr()
536        }
537    }
538
539    /// C `epicsMutexDestroy` (`epicsMutex.cpp:105-113`): off the list first,
540    /// under the list lock, and only then freed. A walk holding that lock can
541    /// therefore dereference every address it is looking at.
542    impl<T> Drop for EpicsMutex<T> {
543        fn drop(&mut self) {
544            let id = self.id;
545            lock().retain(|entry| entry.id != id);
546        }
547    }
548
549    /// `parking_lot::Mutex` has `Debug`, so this must too — a `#[derive(Debug)]`
550    /// on a struct holding one would otherwise break on whichever arm lacked it.
551    impl<T: std::fmt::Debug> std::fmt::Debug for EpicsMutex<T> {
552        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553            self.inner.fmt(f)
554        }
555    }
556
557    #[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
558    fn osd_addr<T>(inner: &MutexBackend<T>) -> usize {
559        inner.raw_addr()
560    }
561
562    /// The fallback backend has no OS object under it, so the address that
563    /// stands in for C's `uaddr` is the lock's own — see [`super::MUTEX_OSD_LABEL`],
564    /// which says which of the two this build printed.
565    #[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
566    fn osd_addr<T>(inner: &MutexBackend<T>) -> usize {
567        inner as *const MutexBackend<T> as usize
568    }
569
570    /// # Safety
571    ///
572    /// `addr` must be the address a live `Box<MutexBackend<T>>` was registered
573    /// with, for the same `T`. [`report`] calls this only while holding the
574    /// list lock, and [`EpicsMutex::drop`] removes the entry under that same
575    /// lock before the box is freed, so an address reachable here is live.
576    unsafe fn probe_locked<T>(addr: usize) -> bool {
577        let backend = unsafe { &*(addr as *const MutexBackend<T>) };
578        match backend.try_lock() {
579            Some(guard) => {
580                drop(guard);
581                false
582            }
583            None => true,
584        }
585    }
586
587    /// One row of C's `epicsMutexShow` (`epicsMutex.cpp:118-127`).
588    #[derive(Clone, Debug)]
589    pub struct MutexInfo {
590        addr: usize,
591        osd_addr: usize,
592        file: &'static str,
593        line: u32,
594    }
595
596    impl MutexInfo {
597        /// C prints the node address as the mutex's identity; this is that
598        /// address, and it is stable for the mutex's whole life.
599        pub fn addr(&self) -> usize {
600            self.addr
601        }
602
603        /// Where the mutex was created — C's `pFileName` and `lineno`.
604        pub fn file(&self) -> &'static str {
605            self.file
606        }
607
608        pub fn line(&self) -> u32 {
609            self.line
610        }
611
612        /// C `epicsMutexShow` plus, above `level` 0, `epicsMutexOsdShow`
613        /// (`os/posix/osdMutex.c:188-195`).
614        pub fn show_lines(&self, level: u32) -> Vec<String> {
615            let mut lines = vec![format!(
616                "epicsMutexId {:#x} source {} line {}",
617                self.addr, self.file, self.line
618            )];
619            if level > 0 {
620                lines.push(format!(
621                    "    {} uaddr={:#x}",
622                    super::MUTEX_OSD_LABEL,
623                    self.osd_addr
624                ));
625            }
626            lines
627        }
628    }
629
630    /// What C prints from one `epicsMutexShowAll` call: the whole list's
631    /// length, then the rows that passed `onlyLocked`.
632    ///
633    /// Both come out of one lock acquisition. C reads `ellCount` outside the
634    /// list lock and walks inside it (`epicsMutex.cpp:133-136`), so its count
635    /// and its rows can disagree by a mutex created in between; there is no
636    /// reason to reproduce that.
637    pub struct MutexReport {
638        pub total: usize,
639        pub shown: Vec<MutexInfo>,
640    }
641
642    /// C `epicsMutexShowAll`'s list walk (`epicsMutex.cpp:129-146`).
643    ///
644    /// `only_locked` is C's try-lock filter. C's mutexes are recursive, so its
645    /// filter reads "held by another thread"; this port's are not reentrant
646    /// (see `server::database::record_lock`), so the filter reads "cannot be
647    /// acquired right now". The two agree for every caller that matters — the
648    /// iocsh thread holds none of these locks while running the command — and
649    /// the second is the honest phrasing of a non-recursive lock's try-lock.
650    pub fn report(only_locked: bool) -> MutexReport {
651        let entries = lock();
652        let mut shown = Vec::new();
653        for entry in entries.iter() {
654            if only_locked {
655                // SAFETY: see `probe_locked`. The list lock is held here, and
656                // deregistration takes it before freeing.
657                if !unsafe { (entry.probe)(entry.addr) } {
658                    continue;
659                }
660            }
661            shown.push(MutexInfo {
662                addr: entry.addr,
663                osd_addr: entry.osd_addr,
664                file: entry.file,
665                line: entry.line,
666            });
667        }
668        MutexReport {
669            total: entries.len(),
670            shown,
671        }
672    }
673}
674
675pub use epics_mutex::{MutexInfo, MutexReport};
676
677/// What this build's mutex actually is, for the `uaddr` line C prints from
678/// `epicsMutexOsdShow`. Naming it `pthread_mutex_t*` on the fallback arm would
679/// claim an OS object that arm does not create.
680#[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
681pub const MUTEX_OSD_LABEL: &str = "pthread_mutex_t*";
682
683/// See [`MUTEX_OSD_LABEL`].
684#[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
685pub const MUTEX_OSD_LABEL: &str = "parking_lot::Mutex*";
686
687/// Every EPICS mutex in the process, filtered as C's `onlyLocked` filters —
688/// `epicsMutexShowAll`'s list walk.
689pub fn mutex_report(only_locked: bool) -> MutexReport {
690    epics_mutex::report(only_locked)
691}
692
693/// C `epicsMutexOsdShowAll` (`os/posix/osdMutex.c:197-210`), the line
694/// `epicsMutexShowAll` prints between the count and the rows.
695///
696/// C has a third answer, `PI not supported`, for a build where
697/// `_POSIX_THREAD_PRIO_INHERIT` is undefined. There is no such build here: the
698/// fallback arm is a deliberate choice not to use a pthread mutex at all, so
699/// the honest report is that PI is not enabled, which is what
700/// [`is_pi_mutex_active`] already answers.
701pub fn osd_show_all_line() -> &'static str {
702    if is_pi_mutex_active() {
703        "PI is enabled"
704    } else {
705        "PI is not enabled"
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    /// The creation site C records as `__FILE__`/`__LINE__`, and the two
714    /// moments the entry exists between: C `ellAdd` in `epicsMutexOsiCreate`
715    /// and `ellDelete` in `epicsMutexDestroy`.
716    #[test]
717    fn an_entry_lasts_exactly_as_long_as_its_mutex() {
718        let expected_line = line!() + 1;
719        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(5);
720        let addr = find_entry(&m).expect("registered at construction").addr();
721
722        let entry = find_entry(&m).unwrap();
723        assert_eq!(entry.file(), file!(), "C's `pFileName` is the caller's");
724        assert_eq!(entry.line(), expected_line, "C's `lineno` is the caller's");
725
726        drop(m);
727        assert!(
728            !mutex_report(false).shown.iter().any(|e| e.addr() == addr),
729            "the entry must come off the list before the mutex is freed"
730        );
731    }
732
733    /// Locate `m`'s own row, which is the only way to test a process-global
734    /// list that other code also registers into.
735    fn find_entry<T>(m: &PriorityInheritanceMutex<T>) -> Option<MutexInfo> {
736        let want = mutex_addr(m);
737        mutex_report(false)
738            .shown
739            .into_iter()
740            .find(|e| e.addr() == want)
741    }
742
743    /// The address the list holds, reached the same way `new` computed it.
744    fn mutex_addr<T>(m: &PriorityInheritanceMutex<T>) -> usize {
745        // One row per mutex, so the row that reports this file and this
746        // mutex's line is this mutex — except that two mutexes can share a
747        // line, which is why the tests that need identity capture the addr
748        // once and compare against it afterwards.
749        let guard = m.try_lock();
750        let held = guard.is_none();
751        drop(guard);
752        assert!(!held, "helper must not be called on a held mutex");
753        // The registered address is the boxed backend's, and `try_lock`
754        // proved this mutex is the free one; find it by elimination on the
755        // locked probe.
756        let before: Vec<usize> = mutex_report(true).shown.iter().map(|e| e.addr()).collect();
757        let _g = m.lock();
758        let after: Vec<usize> = mutex_report(true).shown.iter().map(|e| e.addr()).collect();
759        after.into_iter().find(|a| !before.contains(a)).unwrap()
760    }
761
762    /// C's `onlyLocked` boundary, both sides of it: the filter try-locks every
763    /// entry and keeps the ones it could not take.
764    #[test]
765    fn only_locked_keeps_exactly_the_held_mutexes() {
766        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(0);
767        let addr = mutex_addr(&m);
768        // A second, never-held mutex, so "the filter excludes something" is a
769        // property of this test and not of whatever else the process created.
770        let other: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(0);
771        let other_addr = mutex_addr(&other);
772
773        let free = mutex_report(true);
774        assert!(
775            !free.shown.iter().any(|e| e.addr() == addr),
776            "an unheld mutex must not be listed under onlyLocked"
777        );
778
779        let guard = m.lock();
780        let held = mutex_report(true);
781        assert!(
782            held.shown.iter().any(|e| e.addr() == addr),
783            "a held mutex must be listed under onlyLocked"
784        );
785        assert_eq!(
786            held.total, free.total,
787            "the count is the whole list, not the filtered rows — C prints \
788             `ellCount(&mutexList)` before it filters"
789        );
790        assert!(
791            !held.shown.iter().any(|e| e.addr() == other_addr),
792            "the filter must exclude the mutex nobody holds"
793        );
794        assert!(
795            held.shown.len() < held.total,
796            "{} of {}",
797            held.shown.len(),
798            held.total
799        );
800        drop(guard);
801
802        assert!(!mutex_report(true).shown.iter().any(|e| e.addr() == addr));
803    }
804
805    /// The address in the list is the boxed backend's, so it survives moving
806    /// the mutex — the property that makes the `onlyLocked` probe sound. A
807    /// registry of addresses of the values themselves would dangle here.
808    #[test]
809    fn the_registered_address_survives_moving_the_mutex() {
810        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(1);
811        let addr = mutex_addr(&m);
812        let moved = Box::new(m);
813        assert_eq!(mutex_addr(&moved), addr);
814        assert!(mutex_report(false).shown.iter().any(|e| e.addr() == addr));
815        drop(moved);
816        assert!(!mutex_report(false).shown.iter().any(|e| e.addr() == addr));
817    }
818
819    /// C `epicsMutexShow` prints one line; `epicsMutexOsdShow` adds the second
820    /// only above level 0.
821    #[test]
822    fn show_lines_adds_the_osd_line_only_above_level_zero() {
823        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(0);
824        let entry = find_entry(&m).unwrap();
825
826        let plain = entry.show_lines(0);
827        assert_eq!(plain.len(), 1);
828        assert_eq!(
829            plain[0],
830            format!(
831                "epicsMutexId {:#x} source {} line {}",
832                entry.addr(),
833                entry.file(),
834                entry.line()
835            )
836        );
837
838        let detailed = entry.show_lines(1);
839        assert_eq!(detailed.len(), 2);
840        assert_eq!(detailed[0], plain[0]);
841        assert!(
842            detailed[1].starts_with(&format!("    {MUTEX_OSD_LABEL} uaddr=0x")),
843            "{}",
844            detailed[1]
845        );
846    }
847
848    /// The PI line and the PI fact are one fact, as C's are: both come from
849    /// what the process actually obtained.
850    #[test]
851    fn the_osd_show_all_line_is_the_pi_report() {
852        assert_eq!(
853            osd_show_all_line(),
854            if is_pi_mutex_active() {
855                "PI is enabled"
856            } else {
857                "PI is not enabled"
858            }
859        );
860    }
861
862    /// PI mutex API surface — works on both feature gates. Build path:
863    /// confirms the type is constructible and the lock guard derefs.
864    #[test]
865    fn pi_mutex_lock_unlock() {
866        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(42);
867        {
868            let g = m.lock();
869            assert_eq!(*g, 42);
870        }
871        // re-lock after drop
872        let g = m.lock();
873        assert_eq!(*g, 42);
874    }
875
876    /// The report must follow the arm that was actually compiled, per target.
877    /// Host CI only ever executes the last branch, which is the point: the
878    /// default build must not claim PI it does not have.
879    #[test]
880    fn is_pi_mutex_active_matches_the_cfg_arm() {
881        #[cfg(all(target_os = "linux", feature = "linux-rt"))]
882        assert!(
883            is_pi_mutex_active(),
884            "linux-rt selects the pthread PI arm unconditionally"
885        );
886
887        // On RTEMS the answer is the probe's, not the cfg's — C degrades to
888        // PTHREAD_PRIO_NONE when the target refuses PI
889        // (`posix/osdMutex.c:77-85`) and so do we. What this pins is that the
890        // report and the protocol actually obtained are one fact.
891        #[cfg(target_os = "rtems")]
892        assert_eq!(
893            is_pi_mutex_active(),
894            pi_mutex::protocol() == rtems_pi::PTHREAD_PRIO_INHERIT,
895            "the RTEMS report must be the probe result, not the cfg"
896        );
897
898        #[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
899        assert!(
900            !is_pi_mutex_active(),
901            "the parking_lot fallback arm has no priority inheritance"
902        );
903    }
904
905    /// The `pthread_mutex_t` must not travel with the `PiMutex` that owns it.
906    ///
907    /// RTEMS binds a POSIX mutex to its own address and returns `EINVAL` from
908    /// every operation on a relocated one (`PiMutex`'s doc). Measured on
909    /// target: with the mutex stored inline the IOC panicked on its first
910    /// `lock()` at boot. No host arm can reproduce *that* — glibc's mutex
911    /// survives the move and the default host build is `parking_lot` — so
912    /// what this pins is the property that makes it impossible: the address
913    /// handed to `pthread_mutex_init` is stable across a move of the owner.
914    /// Storing the mutex inline again fails here rather than on the next boot.
915    #[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
916    #[test]
917    fn the_pthread_object_does_not_move_with_the_mutex() {
918        let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(7);
919        let before = m.raw_addr();
920        let moved = Box::new(m);
921        assert_eq!(
922            moved.raw_addr(),
923            before,
924            "the pthread_mutex_t moved with its owner; RTEMS answers EINVAL to \
925             every lock on a relocated mutex"
926        );
927        assert_eq!(*moved.lock(), 7);
928    }
929
930    /// Whichever arm is compiled must be a mutex. On host CI this is the
931    /// `parking_lot` fallback — the arm the other two tests can only assert
932    /// *about* — so this is where the fallback's exclusion is actually
933    /// exercised.
934    #[test]
935    fn pi_mutex_serialises_concurrent_writers() {
936        const THREADS: u64 = 8;
937        const PER_THREAD: u64 = 10_000;
938
939        let m: Arc<PriorityInheritanceMutex<u64>> = Arc::new(PriorityInheritanceMutex::new(0));
940        let workers: Vec<_> = (0..THREADS)
941            .map(|_| {
942                let m = Arc::clone(&m);
943                std::thread::spawn(move || {
944                    for _ in 0..PER_THREAD {
945                        *m.lock() += 1;
946                    }
947                })
948            })
949            .collect();
950        for w in workers {
951            w.join().expect("worker panicked");
952        }
953        assert_eq!(*m.lock(), THREADS * PER_THREAD);
954    }
955}