Okay, so this is basically what lazy_static
expands into:
use std::cell::Cell;
use std::sync::{RwLock, Once, ONCE_INIT};
use std::collections::HashMap;
type Data = HashMap<String, String>;
static ONCE: Once = ONCE_INIT;
static GLOBAL: Cell<Option<RwLock<Data>>> = Cell::new(None);
// The non-const expression we want to use to initialize it
fn init_global() -> RwLock<Data> {
RwLock::new(HashMap::new())
}
pub fn get_global() -> &'static RwLock<Data> {
ONCE.call_once(|| {
GLOBAL.set(Some(init_global()));
});
// Unsafe creation of a &'static T from a *mut T. This is only valid because
// we know the cell will never be mutated again or deinitialized.
unsafe {
match *GLOBAL.as_ptr() {
Some(ref x) => x,
None => panic!("attempted to derefence an uninitialized lazy static. This is a bug"),
}
}
}
The value is stored inside of a Cell
, allowing the None
to be replaced with a Some
at runtime.
Normally, the inner contents of a Cell
cannot be borrowed. That's why some unsafe
must be used to take a &'static
borrow of the innards. In doing so, we're promising to the compiler that the cell will never be written to again, We know this is true because the only line of code that mutates it is behind a std::once::Once
.
But this code by itself doesn't work either!
Compiling playground v0.0.1 (/playground)
error[E0277]: `std::cell::Cell<std::option::Option<std::sync::RwLock<std::collections::HashMap<std::string::String, std::string::String>>>>` cannot be shared between threads safely
--> src/main.rs:9:1
|
9 | static GLOBAL: Cell<Option<RwLock<Data>>> = Cell::new(None);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::cell::Cell<std::option::Option<std::sync::RwLock<std::collections::HashMap<std::string::String, std::string::String>>>>` cannot be shared between threads safely
|
= help: the trait `std::marker::Sync` is not implemented for `std::cell::Cell<std::option::Option<std::sync::RwLock<std::collections::HashMap<std::string::String, std::string::String>>>>`
= note: shared static variables must have a type that implements `Sync`
You see, even though you can't borrow from a Cell
, Cell
is not thread safe, because writes to a Cell
are not guaranteed to be atomic. Therefore, we're forbidden from putting it in a static. This technicality doesn't matter to us, however, because... (actually, I'm not sure why not? Can somebody explain to me what guarantees no data races here? Presumably no memory accesses can be reordered around the Once
?).
To tell rust that this Cell is only used in a threadsafe manner, we have to wrap it in a type that implements Sync (this is why lazy_static
needs to use macros!). Again, this requires unsafe
because the burden of proof for safety lies on us.
static GLOBAL: Global = Global(Cell::new(None));
struct Global(Cell<Option<RwLock<Data>>>);
// This is safe as long as RwLock<Data> impls Sync
unsafe impl Sync for Global { }
(note: the actual implementation of lazy_static
is safer because it properly verifies that the inner type impls Sync
)
with that, the compiler now allows GLOBAL
to exist, and you have basically simulated all of lazy_static
.
There is one last bit lazy_static
does just to make things more ergonomic (this is optional!). It implements Deref
for the wrapper type to make it behave like the type inside the Option.
impl std::ops::Deref for Global {
type Target = RwLock<Data>;
fn deref(&self) -> &RwLock<Data> {
get_global()
}
}
Now the methods of RwLock
(or whatever type you put inside your global, if something else) can be called directly on GLOBAL
, like GLOBAL.read()
.
Here is a complete example on the playground (for everything but the optional Deref bit)