So, the way to tackle this, is that the peripheral is basically the same as a static mut
, of sorts:
pub
struct RawPeripheral(());
pub
static mut RAW_PERIPHERAL: RawPeripheral = RawPeripheral(());
impl RawPeripheral {
pub
fn clear (self: &'_ mut Self) { ... }
}
Now, since access to the RAW_PERIPHERAL
is very error-prone (unguarded mutable global state), the most typical pattern here is indeed that of using some kind of "singleton", although that term is quite overloaded and may refer to multiple distinct implementations.
The way I'd do it would be the following.
First of all, the key idea:
pub
- static mut RAW_PERIPHERAL: RawPeripheral = RawPeripheral(());
+ static MUTEX_PERIPHERAL: Mutex<RawPeripheral> = const_mutex(RawPeripheral());
That way you do have a locking mechanism that guards the &mut
accesses to the RawPeripheral
needed to manipulate the API, and all is good.
Except that having to .lock()
such a thing is a bit cumbersome:
-
people may try to lock the stuff in a finer-grained fashion, which is not optimal, performance-wise.
-
the types involved (MutexGuard<'static, RawPeripheral>
) are a bit unwieldy;
-
we are carrying indirection (the address of the static
) around, at runtime, when the address of a static
is "known at compile-time" (more on that below).
The correct pattern would be for users to, very early in the process, get hold of a MutexGuard<'static, RawPeripheral>
, and keep that one around / keep passing it around. The DerefMut
of such a guard is "zero-cost", since it knows it has exclusive access to the static
MUTEX_PERIPHERAL
, token which symbolically represents the contents of the peripheral.
So, to try and make people fall into the "pit of success" of correctly using our API, we make MUTEX_PERIPHERAL
private, and expose a a newtype wrapper around that MutexGuard
(by construction, it's guaranteed that there will be at most one instance alive at a time):
/* private */ pub(self)
static MUTEX_PERIPHERAL: Mutex<RawPeripheral> = const_mutex(RawPeripheral());
pub
struct Peripheral(MutexGuard<'static, RawPeripheral>);
impl Peripheral {
pub
fn try_new ()
-> Option<Peripheral>
{
MUTEX_PERIPHERAL.try_lock().ok().map(Self)
}
pub
fn clear (self: &'_ mut Self) { ... }
}
And at this point you basically have already a singleton pattern, except for the const_mutex
requiring a parking_lot
impl, and the Peripheral
token holding a runtime pointer / indirection inside it, when we know all such instance(s) will always be pointing to that static.
So we can optimize it a bit:
The singleton pattern (full snippet)
Assuming a raw API such as:
mod raw {
pub
unsafe
fn clear ()
{
/* ... */
}
}
we'd do:
use ::std::sync::atomic::{self, AtomicBool};
static SINGLETON_EXISTS: AtomicBool = AtomicBool::new(false);
pub
struct Singleton(());
impl Singleton {
pub
fn try_new ()
-> Option<Singleton>
{
if SINGLETON_EXISTS.swap(true, atomic::Ordering::Acquire) {
None
} else {
impl Drop for Singleton {
fn drop (self: &'_ mut Singleton)
{
SINGLETON_EXISTS.store(false, atomic::Ordering::Release);
}
}
Some(Self(()))
}
}
pub
fn clear (self: &'_ mut Singleton)
{
unsafe {
raw::clear();
}
}
}
So, I don't know if you implemented the singleton like I've showcased: I'd be interested in hearing more about the disadvantages.
There is an extension of my pattern to allow for a zero-cost unchecked API whereby you could offer an unsafe fn
constructor of a Singleton
that does not read the SINGLETON_EXISTS
atomic. But since such singleton is still kind of expected to be instanced once per execution of the program, it does look like a terribly premature and overkill optimization, I'd say: they'd get UB rather than panics on misuse, just to skip a wait-free boolean check.
If you really want to, there is a way to kind of further nudge users into the "pit of success", whereby reducing the chances of a panic!
: to offer a constructor that can only be called in one location of the code, by virtue of a macro that would reserve some linker symbol, causing a linker failure if the macro were to be called in two places.
Combined with an invocation inside an fn main()
which is not recursively called, and you have a non-unsafe
non-panicking API (assuming Singleton::try_new()
is not exposed (e.g., #[doc(hidden)]
))
#[macro_export]
macro_rules! new_singleton {() => ({
#[export_name = "\n \
Error, called `new_singleton!` in multiple places of the code!\n\
"]
pub static __: () = ();
$crate::lib::Singleton::try_new()
.expect("Reached the `new_singleton!` code path multiple times: re-entrant `main`?")
})}
Here is an example of calling lib::new_singleton!()
in two lines of code:
error: symbol `
Error, called `new_singleton!` in multiple places of the code!
` is already defined
--> src/main.rs:50:9
|
50 | pub static __: () = ();
| ^^^^^^^^^^^^^^^^^^^^^^^
...
59 | let _ = lib::new_singleton!();
| --------------------- in this macro invocation
|
= note: this error originates in the macro `lib::new_singleton`