Hello,
I'm relatively new to Rust, so please bear with me if this thread is too basic. Consider a trait like this:
trait Mapper
{
fn map<'a, 'b>(&'a self, key: &'b str) -> &'a str;
}
I won't bother you with the details of my concrete use case, but the basic idea is that this returns the address of a given service, identified by key
in this example. Our first implementation just stored a HashMap
prefilled with the address of all existing services, but that quickly became a burden to maintain. This implementation made it easy, since we can return a borrow for the address and rest assured that it will survive for as long as the Mapper
itself.
The thing is that the the address of every service can be systematically built (either reading an environment variable, or constructed from the service name using some convention). So instead of pre-fill a HashMap
, we want to switch to an "on-demand"/memoized version. We'd like to keep the signature for compatibility. The projects makes heavy use of async code, so we need some sort of synchronization mechanism (we choose RwLock
for concurrent reads).
In this context, a naive implementation fails since you borrow the data from a lock object which gets dropped, so you get the "cannot return value referencing local variable" error.
I found two possible solutions:
(1) leak the address to make it 'static
. The leak is mostly not a problem since the object instance usually lives for the whole program execution, but might become a problem if somebody creates short live Mapper
objects.
struct MemoizedMapperLeak
{
list: Arc<RwLock<HashMap<String, &'static str>>>,
}
impl Mapper for MemoizedMapperLeak
{
fn map<'a, 'b>(&'a self, key: &'b str) -> &'a str {
// Check if exists
{
let read = self.list.read().unwrap();
if let Some(value) = read.get(key)
{
return value;
}
// Drop read lock
}
// Insert it
{
let value_owned = format!("computed-for: {}", key);
let value_static = Box::leak( value_owned.into_boxed_str() );
let mut write = self.list.write().unwrap();
write.insert(key.to_string(), value_static);
value_static
}
}
}
And (2) use unsafe transmutation to trick the borrow checker. This should not cause problems because the already inserted addresses are never changed, so borrows should be good for the entire lifetime of the object ('a
). However, I do not know if this could backfire me in some way.
struct MemoizedMapper
{
list: Arc<RwLock<HashMap<String, String>>>,
}
impl Mapper for MemoizedMapper
{
fn map<'a, 'b>(&'a self, key: &'b str) -> &'a str {
// Check if exists
{
let read = self.list.read().unwrap();
if let Some(value) = read.get(key)
{
// This is safe because values are never mutated once inserted,
// so they are guaranteed to exists for the whole 'a lifetime
return unsafe {
std::mem::transmute::<_, &'a str>(value.as_str())
};
}
// Drop read lock
}
// It didn't exist, so insert it
{
let value = format!("computed-for: {}", key); // Just placeholder for the real implementation
// This is safe because values are afterwards moved to the list,
// where they are never mutated nor dropped in the self ('a) lifetime
let output = unsafe { std::mem::transmute::<_, &'a str>(value.as_str()) };
let mut write = self.list.write().unwrap();
write.insert(key.to_string(), value);
output
}
}
}
Both leaks and unsafe are kinda spooky, so I wonder if there's some other more idiomatic approach.
Of course, a pragmatic solution could be change the signature and return an owned (cloned) string; and let the compiler tell us all the compatibility problems. There would be only a tiny performance loss due to the clone. But more important than that, as a learning opportunity I would like to know whether there is something fundamentally wrong with my candidate approaches (leak and unsafe transmute), and whether there is a less safe and clone-free option.