Rust storage patterns

Hey there, I was wondering if someone that has experience with rust canisters could spare a couple of minutes to explain the pros and cons of some basic usage patterns, and why certain choices were made. I’m mainly talking about two patterns, one found in the tutorials and the other found in the internet_identity canister.

So far I’ve been using this basic pattern, and everything works as expected:

struct GameCollection {
    pub total_games: u32,
    pub games: BTreeMap<u32, Game>,
    pub global_max_score: u32,
    pub global_max_score_id: u32,
}

fn list_games() -> Vec<Game> {
    let game_store = storage::get::<GameCollection>();

    let output = game_store.list_games();
    output
}

Similarly I’d use storage::get_mut::(); for updates.

In the internet_identity canister the following pattern is used:

thread_local! {
    static STATE: State = State::default();
    static ASSETS: RefCell<HashMap<&'static str, (Vec<HeaderField>, &'static [u8])>> = RefCell::new(HashMap::default());
}

STATE.with(|state| {
        let entries = state
            .storage
            .borrow()
            .read(user_number)
            .unwrap_or_else(|err| {
                trap(&format!(
                    "failed to read device data of user {}: {}",
                    user_number, err
                ))
            });

[...]

Excepting the memory-fu that the canister does to persistently write the entries into stable memory, and assuming a normal pattern of pre_upgrade and post_upgrade, what would be the differences / advantages / tradeoffs of both approaches? Any reason why the tutorials point towards the more straightforward storage::get and the identity canister uses a different pattern? Any gotchas or cautions on choosing one or the other?

Also, while we’re here, sync or async?

Thanks!

1 Like

https://mmapped.blog/posts/01-effective-rust-canisters.html

2 Likes

Wow, that’s exactly what I needed! Thanks a lot!