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}