0

I am trying to create a static HashMap that I will read from or write to throughout the program.

Here's my attempt (playground):

extern crate lazy_static; // 1.4.0

use std::{any::Any, collections::HashMap};

use chrono::{Local, NaiveDate}; // 0.4.19

fn test() -> () {
    let cache: &'static mut HashMap<String, HashMap<String, &'static dyn Any>> =
        &mut HashMap::new();

    let function_name = "Zaxd".to_string();
    let params = vec!["zuha".to_string(), "haha".to_string(), "hahaha".to_string()];
    let date = Local::today().naive_local();

    write(function_name, params, &date, &18, cache);
}

fn write(
    function_name: String,
    params: Vec<String>,
    valid_to: &'static (dyn Any + 'static),
    return_value: &'static (dyn Any + 'static),
    cache: &'static mut HashMap<String, HashMap<String, &'static dyn Any>>,
) -> () {
    let mut key = function_name;

    key.push_str("-");
    key.push_str(&params.join("-"));

    let mut value: HashMap<String, &dyn Any> = HashMap::new();

    value.insert("value".to_string(), return_value);
    value.insert("valid_to".to_string(), valid_to);

    cache.insert(key.clone(), value);
}

fn main() {
    test();
}

I am getting the following errors:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:9:14
   |
8  |     let cache: &'static mut HashMap<String, HashMap<String, &'static dyn Any>> =
   |                --------------------------------------------------------------- type annotation requires that borrow lasts for `'static`
9  |         &mut HashMap::new();
   |              ^^^^^^^^^^^^^^ creates a temporary which is freed while still in use
...
16 | }
   | - temporary value is freed at the end of this statement

error[E0597]: `date` does not live long enough
  --> src/main.rs:15:34
   |
15 |     write(function_name, params, &date, &18, cache);
   |                                  ^^^^^
   |                                  |
   |                                  borrowed value does not live long enough
   |                                  cast requires that `date` is borrowed for `'static`
16 | }
   | - `date` dropped here while still borrowed

I have two questions:

  1. How do I solve this error?
  2. I want the hashmap to be static but not the values I insert into it (i.e. I want the values to be in the hashmap but that variable should be deleted). But with this implementation, it looks like they will in the heap as long as the program runs which is unacceptable for my purposes. However, I cannot define them to live less than static because then the compiler complains about how they need to live at least as long as static. What is the correct implementation for a static mutable hashmap?
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 2
    Maybe you want your cache to be `HashMap>`. That way the hashmap will own the values inserted into it. Also, you'll probably need to use something like [`lazy_static`](https://docs.rs/lazy_static/1.4.0/lazy_static/#example) for the static hashmap, otherwise the compiler will complain that `cache` doesn't live long enough. – user4815162342 Jun 08 '21 at 17:21
  • @user4815162342 Box seems like it solves the second problem. I tried lazy_static but it did not work with dynamic types. Does it work with a box? – astackoverflowuser Jun 08 '21 at 17:26
  • lazy_static is not limited in what types it supports, so I'm not sure what you mean by it not working for dynamic types. Can you edit the question to show an example of what you tried and how it failed to work? – user4815162342 Jun 08 '21 at 17:28
  • @user4815162342 I completely deleted that part, I will try to implement this and if it doesn't work, I'll update the question – astackoverflowuser Jun 08 '21 at 17:34
  • 1
    Also, using `Any` is likely an antipattern, at least for beginners in Rust. (It's ok to use it for very specialized use cases.) Are you sure you can't create a meaningful trait that encapsulates the things you want to _do_ with the values in the hash table, and put boxed trait objects of that trait into the hash map? – user4815162342 Jun 08 '21 at 18:07
  • It's hard to answer your question because it doesn't include a [MRE]. We can't tell what crates (and their versions), types, traits, fields, etc. are present in the code. It would make it easier for us to help you if you try to reproduce your error on the [Rust Playground](https://play.rust-lang.org) if possible, otherwise in a brand new Cargo project, then [edit] your question to include the additional info. There are [Rust-specific MRE tips](//stackoverflow.com/tags/rust/info) you can use to reduce your original code for posting here. Thanks! – Shepmaster Jun 08 '21 at 18:12
  • [The code you have provided does not produce the error you are asking about](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=de0cfc0524f984fb70ac2ff565ffc2d0) – Shepmaster Jun 08 '21 at 18:12
  • It looks like your question might be answered by the answers of [How do I create a global, mutable singleton?](https://stackoverflow.com/q/27791532/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Jun 08 '21 at 18:13
  • @Shepmaster while my question is not answered there like user4815162342 pointed out what I was trying to do is an anti-pattern and should be done in the "rust way". I changed the code quite a bit to achieve my goal. It would not be entirely inaccurate to mark this already answered since that question may lead people to the correct path. – astackoverflowuser Jun 08 '21 at 19:40
  • I don't want to close the question *just* to close it. If you fully edit your question (and title!) to have a [MRE] and the correct error to demonstrate how it's *not* a duplicate, we can either answer it or find a better duplicate. – Shepmaster Jun 08 '21 at 19:52
  • @Shepmaster I've added the link. – astackoverflowuser Jun 08 '21 at 20:03
  • Why did you chose `&static dyn Any`? Why not [`Box`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d05870723705b968ac7637fb9432c035)? – Shepmaster Jun 08 '21 at 20:13
  • Lack of knowledge I'd say. I am new to rust. I switched to box now. However, Any was still problematic in most places so I removed it completely and stuck with the types. – astackoverflowuser Jun 08 '21 at 20:22
  • 1
    Alright, we could get lost in details just going back and forth on all the concepts and details I'd like to share with you, so I just threw together an alternative implementation that you can look at and contrast with the decisions you'd prefer to make. Please feel free to ask me questions, or ask for me to fill in more detail to help you understand what I did and how it contrasts with your implementation =) – Todd Jun 08 '21 at 22:28

1 Answers1

1

First let's address why you'd want to not use "static" references as values in a static HashMap. If you're going to pass bare references to a statically declared HashMap, the refs are going to have to have a 'static lifetime. There's no way around that. However, you can also give concrete objects to the HashMap as values which it then owns, and these objects can be smart pointers that encapsulate shared objects.

To Share objects you intend to mutate after sharing with a HashMap and other parts of an application, it's a good idea to wrap them in the smart pointer types like Rc and RefCell for single-threaded applications, or Arc and RwLock (or Mutex) for multi-threaded.

I took your code and modified it while trying to stick as close to your implementation as I could. I assumed you'd want thread-safety built in, but if that's not the case, the synchronized types can be replaced with Rc<RefCell<...>> - or maybe just Box<...>. But I believe lazy_static requires thread-safe content.

With the implementation below, the shared objects are not static, and if you remove the smart pointers sharing any particular object from the HashMap, and all clones of the smart pointers pointing to it go out of scope, the shared object will be automatically garbage collected (ref counting model).

SharedAny below is a type that can hold any arbitrary object. wrap_shared() consumes its parameter and wraps it in a SharedAny instance. I've implemented the static HashMap using lazy_static as it's usually used. I assume you wanted that since your code includes it.

use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;

use chrono::{Local, NaiveDate};
use lazy_static::lazy_static;

type SharedAny = Arc<RwLock<dyn Any + Sync + Send>>;

lazy_static! {
    static ref CACHE: RwLock<HashMap<String, HashMap<String, SharedAny>>> = {
        RwLock::new(HashMap::new())
    };
}
// See note below about 'static.
fn wrap_shared<T: 'static + Sync + Send>(any: T) -> SharedAny
{
    Arc::new(RwLock::new(any))
}

'static has different meanings when used as a lifetime (&'static T) vs. use as a bound (T: 'static) as it is in the declaration of wrap_shared() above. When used as a bound, T can be a dynamically created object with an indefinite lifetime. It's not the same thing as a static variable. For a good discussion of this and other lifetime misconceptions, here's a good article.

The Sync and Send bounds in the code above simply tell the compiler that the parameters thus marked should be safe to pass among threads.

write() below consumes its parameters, which are expected to be pre-wrapped. Alternatively, you could pass in references and clone and wrap them inside the function body.

fn write_cache(function_name  : &String,
               params         : &Vec<String>,
               valid_to       : SharedAny,
               return_value   : SharedAny)
{
    let mut key = function_name.clone();
    key.push('-');
    key.push_str(&params.join("-"));
    
    let mut value: HashMap<String, SharedAny> = HashMap::new();
    
    value.insert("value".to_string(), return_value);
    value.insert("valid_to".to_string(), valid_to);
    
    CACHE.write().unwrap().insert(key, value);
}

test() function demonstrating how data could be added to the cache, read from the cache, applied to a callback.

pub fn test() 
{
    let function_name = "Zaxd".to_string();
    let date          = Local::today().naive_local();
    let params        = vec!["zuha".to_string(), 
                             "haha".to_string(), 
                             "hahaha".to_string()];
    
    write_cache(&function_name, 
                &params, 
                wrap_shared(date), 
                wrap_shared(18_i32));
    
    println!("{:#?}", CACHE.read().unwrap());

    let val = read_cache_as::<i32>(&function_name, 
                                   &params
                                  ).unwrap();

    println!("val = {}", val);

    let res = apply_cache_data(&function_name, 
                               &params, 
                               |d| d + 3
                              ).unwrap();

    println!("val + 3 = {}", res);
}

If the arbitrary values added to the static cache can just be copied or cloned, and shared access isn't a requirement, then we can dispense with the SharedAny type and just give wrapper-less values (cloned, copied, or consumed) to the cache. I think they'd still need to be Box'd though since their size isn't known at compile time.

Some examples on how to access the cache data and one to cast dyn Any to concrete types. You may not want to use read_cache_as<T>(), but you can draw from it the way dyn Any is cast to a concrete type.

pub fn read_cache(function_name : &String, 
                  params        : &Vec<String>
                 ) -> Option<SharedAny> 
{
    let mut key = function_name.clone();

    key.push_str("-");
    key.push_str(&params.join("-"));
    
    let cache = CACHE.read().unwrap();

    match cache.get(&key) {
        // Clones smart pointer - not data within.
        Some(val) => Some(val["value"].clone()),
        None => None,
    }
}

pub fn read_cache_as<T>(function_name : &String,
                        params        : &Vec<String>
                       ) -> Option<T>
where T: 'static + Clone,
{
    if let Some(shared_val) = read_cache(function_name, params) {
        let any_val = &*shared_val.read().unwrap();
        
        match any_val.downcast_ref::<T>() {
            // Clones data within smart pointer.
            Some(v) => Some(v.clone()),
            None => None,
        }
    } else { None }
}

A way to apply the cache's dyn Any values to a callback closure avoiding cloning the cached value.

pub fn apply_cache_data<T, F, R>(function_name : &String,
                                 params        : &Vec<String>,
                                 func          : F
                                ) -> Option<R>
where F: FnOnce(&T) -> R,
      T: 'static             
{
    if let Some(shared_val) = read_cache(function_name, params) {
        let any_val = &*shared_val.read().unwrap();
        
        match any_val.downcast_ref::<T>() {
            // No cloning involved. Casts value and passes it to callback.
            Some(v) => Some(func(v)),
            None => None,
        }
    } else { None }
}

Another version of the apply function to demonstrate casting a dyn Any to a mutable reference.

pub fn apply_cache_data_mut<T, F, R>(function_name : &String,
                                     params        : &Vec<String>,
                                     func          : F
                                    ) -> Option<R>
where F: FnOnce(&mut T) -> R,
      T: 'static             
{
    if let Some(shared_val) = read_cache(function_name, params) {
        // .write() instead of .read()
        let any_val = &mut *shared_val.write().unwrap(); 

        // .downcast_mut() instead of .downcast_ref()
        match any_val.downcast_mut::<T>() { 
            Some(v) => Some(func(v)),
            None => None,
        }
    } else { None }
}
Todd
  • 4,669
  • 1
  • 22
  • 30
  • Thanks a lot! This really helped me wrap my head around why the code is there and how it works rather than just throwing it together and edit it until it works as I did. It is also better than what I ended up with. – astackoverflowuser Jun 09 '21 at 08:14
  • One problem I am having is to read the cache in the required format. How can I convert it back to its original format? Here's my attempt but I am getting a size not known error. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9fa64fe8fa90163dfbadb7e677ff52df – astackoverflowuser Jun 09 '21 at 09:23
  • @Eren, [Check out this playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c7ccbb0f19af3dba0e279d849ecfe379). I implemented an example of how to cast `dyn Any` objects to concrete types in the function, `read_cache_as()`. BTW, probably don't want to declare `CACHE` as `pub` - better to only permit access to it through the functions defined in the same file. Looks like you're trying to implement a memoization feature like Python's `configtools.lru_cache` or some other similar library (?). – Todd Jun 09 '21 at 21:59
  • Yes, that's what I'm trying to do. It made me realize how little I understand about rust :). I will check this code out, thank you. If I can't get this cast right, there's going to be a lot of boilerplate code. – astackoverflowuser Jun 10 '21 at 02:43
  • Well, I just tried it out with a custom struct and it works. Can you explain the difference between read_cache_as and apply_cache_data both in implementation and performance? – astackoverflowuser Jun 10 '21 at 02:56
  • Yeah. `read_cache_as()` returns a clone of the actual data that was added to the cache at the given key. I expect it to perform about as well as the other function assuming the cloning operation isn't slow for some reason. `apply_cache_data()` lets you work with the original object that was inserted at the given key, while avoiding the extra code it takes to access and cast it after acquiring its smart pointer using `read_cache()`. @Eren – Todd Jun 10 '21 at 03:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233588/discussion-between-todd-and-eren). – Todd Jun 10 '21 at 03:41