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?
14
  • 2
    Maybe you want your cache to be HashMap<String, Box<dyn Any>>. That way the hashmap will own the values inserted into it. Also, you'll probably need to use something like lazy_static for the static hashmap, otherwise the compiler will complain that cache doesn't live long enough. Commented Jun 8, 2021 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? Commented Jun 8, 2021 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? Commented Jun 8, 2021 at 17:28
  • 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? Commented Jun 8, 2021 at 18:07
  • 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 =) Commented Jun 8, 2021 at 22:28

1 Answer 1

0

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 }
}
Sign up to request clarification or add additional context in comments.

7 Comments

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.
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. play.rust-lang.org/…
@Eren, Check out this playground example. I implemented an example of how to cast dyn Any objects to concrete types in the function, read_cache_as<T>(). 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 (?).
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.
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?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.