Another lifetime problem

I would like to create a structure that contains a string cache (a Vec<String>). Also, in this structure, I would like to store a queue (can be implemented using Sender/Receiver), and I would like to send &str's that reference Strings inside my cache. Like this:

#![allow(unused)]
use std::{sync::mpsc::{channel, Receiver}};

#[derive(Debug)]
enum Event<'a> {
    Ref(&'a str),
}

struct Inner<'a> {
    cache: Vec<String>,
    queue: Receiver<Event<'a>>,
}

struct Context<'a> {
    inner: Inner<'a>,
}

fn main() {
    let (send, recv) = channel();
    let mut c = Context {
        inner: Inner {
            cache: Vec::new(),
            queue: recv,
        }
    };

    c.inner.cache.push("testing".to_string());
    c.inner.cache.push("abc".to_string());
    let rf = c.inner.cache[0].as_str();
    println!("rf={}", rf);
    let ev = Event::Ref(rf);
    send.send(ev);

    let received_rf = c.inner.queue.recv().unwrap();
    println!("received_rf={:?}", received_rf);
}

How can I jump over the fact that Rust thinks that my cache can expire before receiving the message? Can it even happen in this scenario?

1 Like

Rust's temporary scope-limited loans (&) are not the right tool for this. To get sendable strings "by reference" the correct reference type is Arc<str>.

Temporary loans will be limited to the scope they're borrowed in, so you will not have freedom to use them outside of the initial scope. Typical channel implementations aren't limited to a single function's scope, so they're incompatible. You could use temporary references with scoped threads through.

Another problem is that temporary loans mark the whole object as immutable, so you won't be able to add things to the cache while there are any outstanding temporary loans to it. You can fix that by using arenas/pools and interior mutability.

But the easiest is to use a shared reference that is not temporary, and is not anchored to a single function's scope: Arc.

1 Like

This doesn't change the answer to the original question, but think it's an important correction:

Most popular channel implementations are fine with carrying non-'static data, because they are simply generic over all types. (See std, crossbeam, flume, tokio — all have simply <T>.) Restrictions arise from how the two ends of the channel are used, not how they are implemented — the problem isn't putting 'a in the channel but 'a itself being still alive at both usage sites.

1 Like

Are they really "by reference" though? I did some ugly testing:

    let s1 = "testing".to_string();
    let s2 = "abc".to_string();

    c.inner.cache.push(s1);
    c.inner.cache.push(s2);

    {
        let rf = c.inner.cache[0].as_str();
        let arc: Arc<str> = Arc::from(rf);
        let ev = Event::Ref(arc);
        send.send(ev);
    }

    {
        let mut rf = c.inner.cache[0].as_mut_str();
        unsafe { rf.as_bytes_mut()[0] = 0x41; }
    }

    let received_rf = c.inner.queue.recv().unwrap();

    if let Event::Ref(s) = received_rf {
        println!("received {:?}", s);
        println!("first item is '{}'", c.inner.cache[0]);
    }

and it prints:

received "testing"
first item is 'Aesting'

(second item is mutated after sending the message with send(), and before reading the message through recv())

so it seems that Arc<str> copies the string and sends the copy? I would like to send only references (or pointers) to the original elements hosted inside the cache, without doing any copies.

You have to store Arc<str> in the cache, not String. When you crate Arc<str> you copy the string (which is why it's best to do it early, straight from &str), but when you clone Arc<str>, you only get another reference to the same string.

Also check out string interning libraries. Some of them can give you 32-bit numbers for your strings, which are cheap and easy to send around.

In general this is called a self-referential struct, and cannot be created in safe Rust (at least not without using libraries that wrap the necessary unsafe for you, but even then there are open questions on the soundness of them). And since you want a cache, you'll likely also want to mutate the Vec while there are outstanding references, which is an even bigger problem since there's basically no way to know statically whether you still have references to the elements you're mutating/removing.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.