Return a reference to thread local?

I have a thread_local string and I want to return a reference to it. Looks like I just can't do this, because the compiler does not know that info is actually static.

    use std::{cell::{RefCell, Ref}, ops::Deref};

    thread_local!(static TEST_INFO: RefCell<Option<String>> = RefCell::new(None));

	pub fn get() -> impl Deref<Target = str> {
		TEST_INFO.with(|info| {
			Ref::map(info.borrow(), |info| info.as_ref().unwrap() as &str)
		})
	}

This does not compile with error:

  |
6 |         TEST_INFO.with(|info| {
  |                         ----- return type of closure is Ref<'2, str>
  |                         |
  |                         has type `&'1 RefCell<Option<String>>`
7 |             Ref::map(info.borrow(), |info| info.as_ref().unwrap() as &str)
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`

error: could not compile `playground` due to previous error

This is not allowed as there is no way to guarantee that the reference will not outlive the current thread otherwise. You will have to pass a closure to get which accepts as argument a reference to the string.

2 Likes

Ok, now I understand the reason.

Maybe I can mark the returned type so that it is not passable to different threads?

Even within the same thread you have a problem if you manage to sneak the reference into another thread local that gets destructured after the original thread local you took a reference from.

3 Likes

You can use Rc<str> (slightly thinner Rc<String>) if you don't need to mutate it, otherwise you'll need Rc<RefCell<…>> and return a refcounted clone of that.

1 Like

To help other readers avoid confusion trying to understand this explanation: I would use “dropped” instead of “destructured”, as “destructuring” is something different…

Thread locals (unlike global / ordinary statics) are only alive for the time the thread is running, and when the thread finishes will be dropped. So their destructors are run. (The order of those destructors seems to be not specified AFAICT.) Now, if one thread local’s value could contain a reference (or a way to obtain a reference infallibly) to another thread local, then – depending on their drop order – it could happen, than the destructor of the first thread local has, through that reference value, access to the memory of another thread local that has already been dropped.

Of course, there’s nothing preventing other thread locals to be accessed in a destructor, and in fact some possible negative effects are mentioned in the docs, though those effects are safe: e.g. the accessed thread-local could be re-initialized, or a panic could occur. But this is only about accessing the thread-local through its local key, any direct access to another thread-local without a step that might perform re-initialization or panicking if needed would be unsound.