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}