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

lazy_static lets you initialize a static at runtime rather than compile time. That’s why it can call non-const functions.

2 Likes

that’s exactly what I want. To declare a type then initialize it myself. And that’s the very thing compiler refuses me to do. So I have to use unsafe for that as I understand from your previous comment, correct?
that’s what I actually asking for there’s no safe way of doing this.

lazy_static is a safe way, and common way, to do it.

I think i found the answer. It seems static mut is exactly what i have been looking for

This will require use of unsafe, so make sure you know what you’re doing so that you implement your cache correctly without data races. The primary bug you need to make sure you avoid is racey initialization of the global. lazy_static will let you do this without unsafe.

  • static mut won’t work either. You still can’t initialize it!
  • static mut is virtually impossible to use correctly (i.e. every program that tries to use it sensibly technically invokes UB) and there have been talks of removing it from the language

That’s why we want static with interior mutability instead.

3 Likes

@ExpHP yes static mut doesn’t work as well. this is the updated code and I cannot unwrap a static mut i don’t understand if I cannot unwrap it why it’s allowed in the first place. It exists but rust doesn’t let me see what’s inside of it.

So what you are saying is if I want to use rust and if I want to build a cache I have to rely on 3rd party crates because there’s no straight way to create anything global even using unsafe without going through unsafe pointers and memory locations.

So it seems theres no way for me to build a cache in rust. I dont want to use third party crates for something this simple and esseantial and i cannot pass a state variable down with the function call because the function call will be a grpc call so at that context i will never have a local variable for state. and i dont want to build an entire service for this.

every time i want to use rust for something in prod i hit a wall like this. Seriously this is an awful experience I have wasted literally 8+ hours for this today and there’s no way to do it in rust language in the end without magic

Dude. Seriously. Can you wait 10 minutes for me to finish typing my post? :slight_smile:

1 Like

oops sorry :slight_smile: I am so much frustrated i just cant help it :smiley:

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