[SOLVED] What pattern would you suggest for caching since there's no concept of global heap variables in rust?

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)

8 Likes

Thank you for your answer. It clarified a lot of things. I have first decided to add another grpc service for this but then i noticed i will have the same problem even with a new service since theres no way to pass my local state variables into grpc impls so i will write this bit in golang and connect it to rust using grpc. Hopefully in the future rust will allow having const maps and mutexes so i can port this back to rust

Eureka!!! :smiley: @ExpHP From the reddit thread someone suggested instead of having a global state i can actually bind the state to the base struct used with the grpc

1 Like

BTW, it's normal in Rust to use crates for everything. Cargo makes it painless, so the language and the standard library intentionally doesn't provide even some basic things.

2 Likes

I know but in any language i prefer to understand the thing and then use a library for it so in the future i am able to identify a weird bug. If i use magic everywhere then i cant even guess the reason of the thing. Im not agains using crates im agains using crates that i dont understand how they work and @ExpHP made how lazy_static works clear tonight :slight_smile:

1 Like

once_cell is a macro-less alternative to lazy_static. It's probably easier to understand.

5 Likes

Awesome.

1 Like

I wonder if you do grpc without using a crate (assuming grpc is as simple as lazy_static). Do you write them from scratch in Rust? How's your experience?

Hi. I am using grpc-rust crate. Instead of using a global state attaching things to the base struct solved the issue. i was just thinking about it all wrong