maitake_sync/blocking/
default_mutex.rs

1//! Default "chef's choice" [`ScopedRawMutex`] implementation.
2//!
3//! This type is what users will get when they don't override the `Lock` type
4//! parameter for `maitake-sync`'s synchronziation primitives.
5//!
6//! # Notes
7//!
8//! - The `DefaultMutex` cannot ever implement `RawMutex`, only
9//!   `ScopedRawMutex`. This is because it's impossible for a
10//!   `critical-section`-based implementation to implement `RawMutex`, due to
11//!   [`critical-section`'s safety requirements][cs-reqs], which we can't uphold
12//!   in a RAII situation with multiple locks. If we implemented `RawMutex` for
13//!   the non-`critical-section` implementations, then the `critical-section`
14//!   feature flag would *take away* methods that would otherwise be available,
15//!   making it non-additive, which is a BIG NO-NO for feature flags.
16//!
17//! - On the other hand, it *is* okay to have `cfg(loom)` not implement
18//!   `ConstInit` where every other implementation does. This is because `cfg(loom)`
19//!   is a `RUSTFLAGS` cfg rather than a feature flag, and therefore can only be
20//!   enabled by the top-level build. It can't be enabled by a dependency and
21//!   suddenly make your code not compile. Loom users are already used to stuff
22//!   being const-initializable in real life, but not in loom tests, so this is
23//!   more okay.
24//!
25//! [cs-reqs]:
26//!     https://docs.rs/critical-section/latest/critical_section/fn.acquire.html#safety
27#[cfg(not(loom))]
28use super::ConstInit;
29use super::ScopedRawMutex;
30
31/// Default, best-effort [`ScopedRawMutex`] implementation.
32///
33/// This is the default `Lock` type parameter for the [`Mutex`](crate::Mutex)
34/// type, and for the async synchronization primitives that use the blocking
35/// `Mutex`. This type makes a best-effort attempt to Do The Right Thing based
36/// on the currently enabled [feature flags]. In particular, here's what we
37/// currently give you:
38///
39/// - **If `cfg(loom)` is enabled, then the `DefaultMutex` is a [`loom` mutex]**
40///   so that `maitake-sync` primitives work nicely in `loom` tests
41///
42/// - **If the `std` feature is enabled, then the `DefaultMutex` is a
43///   [`std::sync::Mutex`]**, so that `std` users get an OS mutex rather than a
44///   spinlock.
45///
46/// - **If the `critical-section` feature is enabled, then the `DefaultMutex` is
47///   a spinlock that [acquires a critical section][cs] once locked**. This
48///   ensures that bare-metal users who have enabled `critical-section` get a
49///   mutex that disables IRQs when locked.
50///
51/// - **Otherwise, the `DefaultMutex` is a [`Spinlock`]**. This is the default
52///   behavior and will at least work on all platforms, but may not be the most
53///   efficient, and may not be IRQ-safe.
54///
55/// # Notes
56///
57/// - Regardless of feature flags, this type implements the [`ScopedRawMutex`]
58///   trait, *not* the [`RawMutex`] trait. In order to use methods or types that
59///   require a [`RawMutex`], you must [provide your own `RawMutex`
60///   type][overriding].
61/// - :warning: If the `critical-section` feature is enabled, you **MUST**
62///   provide a `critical-section` implementation.  See the [`critical-section`
63///   documentation][cs-providing] for details on how to select an
64///   implementation. If you don't provide an implementation, you'll get a
65///   [linker error][cs-link-err] when compiling your code.
66/// - This type has a `const fn new()` constructor and implements the
67///   [`ConstInit` trait](super::ConstInit) *except* when `cfg(loom)` is
68///   enabled.
69///
70///   Loom users are probably already aware that `loom`'s simulated
71///   types cannot be const initialized, as they must bind to the *current* test
72///   iteration when constructed. This is not a non-additive feature flag,
73///   because `loom` support can only be enabled by a `RUSTFLAGS` cfg set by the
74///   top-level build, and not by a dependency.`s`
75///
76/// [feature flags]: crate#features
77/// [`loom` mutex]: https://docs.rs/loom/latest/loom/sync/struct.Mutex.html
78/// [cs]: https://docs.rs/critical-section/latest/critical_section/fn.with.html
79/// [`Spinlock`]: crate::spin::Spinlock
80/// [overriding]: crate::blocking#overriding-mutex-implementations
81/// [`RawMutex`]: mutex_traits::RawMutex
82/// [cs-providing]:
83///     https://docs.rs/critical-section/latest/critical_section/index.html#usage-in-no-std-binaries
84/// [cs-link-err]:
85///     https://docs.rs/critical-section/latest/critical_section/index.html#undefined-reference-errors
86///
87// N.B. that this is a wrapper type around the various impls, rather than just a
88// re-export, because I didn't want to duplicate the docs for all the impls...
89#[must_use = "why create a `DefaultMutex` if you're not going to lock it?"]
90pub struct DefaultMutex(Inner);
91
92impl DefaultMutex {
93    loom_const_fn! {
94        /// Returns a new `DefaultMutex`.
95        ///
96        /// See the [type-level documentation](Self) for details on how to use a `DefaultMutex`.
97        // loom::sync::Mutex`'s constructor captures the location it's
98        // constructed, so that we can track where it came from in test output.
99        // That's nice, let's not break it!
100        #[track_caller]
101        #[inline]
102        pub fn new() -> Self {
103            Self(Inner::new())
104        }
105    }
106}
107
108impl Default for DefaultMutex {
109    #[track_caller] // again, for Loom Reasons
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl core::fmt::Debug for DefaultMutex {
116    #[inline]
117    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118        self.0.fmt(f)
119    }
120}
121
122#[cfg(not(loom))]
123impl ConstInit for DefaultMutex {
124    // As is traditional, clippy is wrong about this.
125    #[allow(clippy::declare_interior_mutable_const)]
126    const INIT: Self = Self::new();
127}
128
129unsafe impl ScopedRawMutex for DefaultMutex {
130    #[track_caller]
131    fn with_lock<R>(&self, f: impl FnOnce() -> R) -> R {
132        self.0.with_lock(f)
133    }
134
135    #[track_caller]
136    fn try_with_lock<R>(&self, f: impl FnOnce() -> R) -> Option<R> {
137        self.0.try_with_lock(f)
138    }
139
140    #[inline]
141    fn is_locked(&self) -> bool {
142        self.0.is_locked()
143    }
144}
145
146#[cfg(loom)]
147use loom_impl::LoomDefaultMutex as Inner;
148
149#[cfg(all(not(loom), feature = "std"))]
150use std_impl::StdDefaultMutex as Inner;
151
152#[cfg(all(not(loom), not(feature = "std"), feature = "critical-section"))]
153use cs_impl::CriticalSectionDefaultMutex as Inner;
154
155#[cfg(all(not(loom), not(feature = "std"), not(feature = "critical-section")))]
156use spin_impl::SpinDefaultMutex as Inner;
157
158#[cfg(loom)]
159mod loom_impl {
160    use super::ScopedRawMutex;
161    use core::panic::Location;
162    use tracing::{debug, debug_span};
163
164    #[derive(Debug)]
165    pub(super) struct LoomDefaultMutex(loom::sync::Mutex<()>);
166
167    impl LoomDefaultMutex {
168        // loom::sync::Mutex`'s constructor captures the location it's
169        // constructed, so that we can track where it came from in test output.
170        // That's nice, let's not break it!
171        #[track_caller]
172        pub(super) fn new() -> Self {
173            Self(loom::sync::Mutex::new(()))
174        }
175    }
176
177    unsafe impl ScopedRawMutex for LoomDefaultMutex {
178        #[track_caller]
179        #[inline]
180        fn with_lock<R>(&self, f: impl FnOnce() -> R) -> R {
181            let location = Location::caller();
182            trace!(
183                target: "maitake_sync::blocking",
184                %location,
185                "DefaultMutex::with_lock()",
186            );
187
188            let guard = self.0.lock();
189            let _span = debug_span!(
190                target: "maitake_sync::blocking",
191                "locked",
192                %location,
193            )
194            .entered();
195            debug!(
196                target: "maitake_sync::blocking",
197                "DefaultMutex::with_lock() -> locked",
198            );
199
200            let result = f();
201            drop(guard);
202
203            debug!(
204                target: "maitake_sync::blocking",
205                "DefaultMutex::with_lock() -> unlocked",
206            );
207
208            result
209        }
210
211        #[track_caller]
212        #[inline]
213        fn try_with_lock<R>(&self, f: impl FnOnce() -> R) -> Option<R> {
214            let location = Location::caller();
215            trace!(
216                target: "maitake_sync::blocking",
217                %location,
218                "DefaultMutex::try_with_lock()",
219            );
220
221            match self.0.try_lock() {
222                Ok(guard) => {
223                    let _span = debug_span!(target: "maitake_sync::blocking", "locked", %location)
224                        .entered();
225                    debug!(
226                        target: "maitake_sync::blocking",
227                        "DefaultMutex::try_with_lock() -> locked",
228                    );
229
230                    let result = f();
231                    drop(guard);
232
233                    debug!(
234                        target: "maitake_sync::blocking",
235                        "DefaultMutex::try_with_lock() -> unlocked",
236                    );
237
238                    Some(result)
239                }
240                Err(_) => {
241                    debug!(
242                        target: "maitake_sync::blocking",
243                        %location,
244                        "DefaultMutex::try_with_lock() -> already locked",
245                    );
246
247                    None
248                }
249            }
250        }
251
252        fn is_locked(&self) -> bool {
253            self.0.try_lock().is_err()
254        }
255    }
256}
257
258#[cfg(all(not(loom), feature = "std"))]
259mod std_impl {
260    use super::ScopedRawMutex;
261    #[derive(Debug)]
262    #[must_use]
263    pub(super) struct StdDefaultMutex(std::sync::Mutex<()>);
264
265    impl StdDefaultMutex {
266        #[inline]
267        pub(super) const fn new() -> Self {
268            Self(std::sync::Mutex::new(()))
269        }
270    }
271
272    unsafe impl ScopedRawMutex for StdDefaultMutex {
273        #[track_caller]
274        #[inline(always)]
275        fn with_lock<R>(&self, f: impl FnOnce() -> R) -> R {
276            let _guard = self.0.lock().unwrap();
277            f()
278        }
279
280        #[track_caller]
281        #[inline(always)]
282        fn try_with_lock<R>(&self, f: impl FnOnce() -> R) -> Option<R> {
283            let _guard = self.0.try_lock().ok()?;
284            Some(f())
285        }
286
287        fn is_locked(&self) -> bool {
288            self.0.try_lock().is_ok()
289        }
290    }
291}
292
293#[cfg(all(not(loom), not(feature = "std"), feature = "critical-section"))]
294mod cs_impl {
295    use super::ScopedRawMutex;
296    use crate::spin::Spinlock;
297
298    #[derive(Debug)]
299    pub(super) struct CriticalSectionDefaultMutex(Spinlock);
300
301    impl CriticalSectionDefaultMutex {
302        #[inline]
303        pub(super) const fn new() -> Self {
304            Self(Spinlock::new())
305        }
306    }
307
308    unsafe impl ScopedRawMutex for CriticalSectionDefaultMutex {
309        #[track_caller]
310        #[inline(always)]
311        fn with_lock<R>(&self, f: impl FnOnce() -> R) -> R {
312            critical_section::with(|_cs| self.0.with_lock(f))
313        }
314
315        #[track_caller]
316        #[inline(always)]
317        fn try_with_lock<R>(&self, f: impl FnOnce() -> R) -> Option<R> {
318            critical_section::with(|_cs| self.0.try_with_lock(f))
319        }
320
321        #[inline]
322        fn is_locked(&self) -> bool {
323            self.0.is_locked()
324        }
325    }
326}
327
328#[cfg(all(not(loom), not(feature = "std"), not(feature = "critical-section")))]
329mod spin_impl {
330    use super::ScopedRawMutex;
331    use crate::spin::Spinlock;
332
333    #[derive(Debug)]
334    pub(super) struct SpinDefaultMutex(Spinlock);
335
336    impl SpinDefaultMutex {
337        #[inline]
338        pub(super) const fn new() -> Self {
339            Self(Spinlock::new())
340        }
341    }
342
343    unsafe impl ScopedRawMutex for SpinDefaultMutex {
344        #[track_caller]
345        #[inline(always)]
346        fn with_lock<R>(&self, f: impl FnOnce() -> R) -> R {
347            self.0.with_lock(f)
348        }
349
350        #[track_caller]
351        #[inline(always)]
352        fn try_with_lock<R>(&self, f: impl FnOnce() -> R) -> Option<R> {
353            self.0.try_with_lock(f)
354        }
355
356        #[inline(always)]
357        fn is_locked(&self) -> bool {
358            self.0.is_locked()
359        }
360    }
361}
362
363#[cfg(test)]
364mod test {
365    use super::DefaultMutex;
366
367    // Check that a `DefaultMutex` will always implement the traits we expect it
368    // to.
369    #[test]
370    fn default_mutex_trait_impls() {
371        fn assert_scoped_raw_mutex<T: mutex_traits::ScopedRawMutex>() {}
372        fn assert_send_and_sync<T: Send + Sync>() {}
373        fn assert_default<T: Default>() {}
374        fn assert_debug<T: core::fmt::Debug>() {}
375
376        assert_scoped_raw_mutex::<DefaultMutex>();
377        assert_send_and_sync::<DefaultMutex>();
378        assert_default::<DefaultMutex>();
379        assert_debug::<DefaultMutex>();
380    }
381
382    // Check that a non-`loom` `DefaultMutex` has a const-fn constructor, and
383    // implements `ConstInit`.
384    #[cfg(not(loom))]
385    #[test]
386    fn const_constructor() {
387        fn assert_const_init<T: mutex_traits::ConstInit>() {}
388
389        assert_const_init::<DefaultMutex>();
390
391        static _MY_COOL_MUTEX: DefaultMutex = DefaultMutex::new();
392    }
393}