user14037074
user14037074

Reputation:

How to insert a non-static value to a static HashMap with &'static dyn Any values?

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?

Upvotes: 0

Views: 895

Answers (1)

Todd
Todd

Reputation: 5385

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 }
}

Upvotes: 0

Related Questions