Borrowing a variable in a thread from an endless loop inside main thread


#1

I posted the same question in users forum, however, I could not find an acceptable answer (other than a workaround) for the question.

Please consider the following code:

use std::thread;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;

fn handle(_rx:Receiver<(i32, &str)>) {
    // ...
}

fn main() {
    let s = String::from("Some text");
    
    let (tx, rx) = mpsc::channel();
    thread::spawn(move|| handle(rx));

    loop {
        // Endless loop
        tx.send((5, s.as_str())).unwrap();
    }
}

the code is not compiled with the following error:

error[E0597]: `s` does not live long enough
  --> a.rs:17:21
   |
17 |         tx.send((5, s.as_str())).unwrap();
   |                     ^ does not live long enough
18 |     }
19 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...

However, since s has been defined in the main function, it would live for the entire life of program (i.e. static lifetime).

rustc 1.23.0 (766bd11c8 2018-01-01)


#2

Rust doesn’t have any special concept of lifetimes for main(), so from perspective of the borrow checker it is a short-lived value. Rust doesn’t look at what your program actually does. It only enforces a few general rules.

To make the borrow checker happy:

  • clone and send owned values
  • use string literal
  • wrap it in Arc
  • look for string interning libraries

#3

I recommend just using Box::leak to get a &'static str.


#4

Technically, you could even call main() again from some other context.


#5

I used lazy_static to initiate a static reference and sent it via channel sender.

@cuviper as you mentioned, calling main function from some other context would cause lifetime issues, however, I think the recalling of main function should be considered as an error in this case. But, in my example where no recalling of main function has occurred, the compilation error is not true.


#6

I don’t think the issue here is about recursive calls to main. Rather, I think the issue is that the spawned thread can continue to exist after main has returned. In this specific case, main never returns, but I guess the borrow-checker doesn’t know that, and I suspect teaching it enough about the CFG to know that would open its own can of worms.)

The existing thread library doesn’t model thread lifetimes explicitly, even when they aren’t detached; but you could imagine an improved one that does, such that tx.send could safely and soundly be applied to any object whose lifetime was provably shorter than the lifetime of tx itself. Unfortunately I am not clever enough with lifetime annotations to know what that would look like.

(I think recursive calls to main should be allowed, for the record. Rust’s zero-argument main makes it less useful than it might be in other languages, but if you can think of something to do with it, who am i to say no?)


#7

Yes. Even with the code given, main can panic (if handle closes the receiver, or even just if there’s a bug in this part of the standard library). This panic will unwind out of main, dropping the string, and the thread can continue to run afterwards (concurrently to, or preempting, the little bit of work that remains to be done on the main thread after the string has been dropped), accessing deallocated memory. So the code above is, in fact, potentially memory unsafe!

Similar reasoning will apply to virtually every non-trivial piece of code. Even if it has been painstakingly ensured that it cannot actually panic (something that I believe to be completely impossible in practice for any piece of code larger than a few simple lines), the compiler cannot and should not have to prove absence of panics.