Search code examples
pointersrustlifetimeunsafe

Is it safe to use a raw pointer to access the &T of a RefCell<HashMap<T>>?


I have a cache-like structure which internally uses a HashMap:

impl Cache {
    fn insert(&mut self, k: u32, v: String) {
        self.map.insert(k, v);
    }

    fn borrow(&self, k: u32) -> Option<&String> {
        self.map.get(&k)
    }
}

Playground with external mutability

Now I need internal mutability. Since HashMap does not implement Copy, my guess is that RefCell is the path to follow. Writing the insert method is straight forward but I encountered problems with the borrow-function. I could return a Ref<String>, but since I'd like to cache the result, I wrote a small Ref-wrapper:

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  &'a String,
}

This won't work since value references borrow, so the struct can't be constructed. I know that the reference is always valid: The map can't be mutated, because Ref locks the map. Is it safe to use a raw pointer instead of a reference?

struct CacheRef<'a> {
    borrow: Ref<'a, HashMap<u32, String>>,
    value:  *const String,
}

Am I overlooking something here? Are there better (or faster) options? I'm trying to avoid RefCell due to the runtime overhead.

Playground with internal mutability


Solution

  • I'll complement @Shepmaster's safe but not quite as efficient answer with the unsafe version. For this, we'll pack some unsafe code in a utility function.

    fn map_option<'a, T, F, U>(r: Ref<'a, T>, f: F) -> Option<Ref<'a, U>>
    where
        F: FnOnce(&'a T) -> Option<&'a U>
    {
        let stolen = r.deref() as *const T;
        let ur = f(unsafe { &*stolen }).map(|sr| sr as *const U);
        match ur {
            Some(u) => Some(Ref::map(r, |_| unsafe { &*u })),
            None => None
        }
    }
    

    I'm pretty sure this code is correct. Although the compiler is rather unhappy with the lifetimes, they work out. We just have to inject some raw pointers to make the compiler shut up.

    With this, the implementation of borrow becomes trivial:

    fn borrow<'a>(&'a self, k: u32) -> Option<Ref<'a, String>> {
        map_option(self.map.borrow(), |m| m.get(&k))
    }
    

    Updated playground link

    The utility function only works for Option<&T>. Other containers (such as Result) would require their own modified copy, or else GATs or HKTs to implement generically.