Thread local storage can only be accessed while the thread is still alive, whereas the associated LocalKey can have a 'static lifetime. At least that was given to me as the reason why the API only supports LocalKey::with and LocalKey::try_with.
But can we not tie the guarantee that "the thread is still alive" to another struct that we force to be not-Send and not-Sync (so that you can't store it in a static or send it away)? Behold, then,
/// Small helper to only change lifetimes. Caller must hold up invariants for the cast
unsafe fn promote_lifetime<'a, 'b, T: ?Sized>(r: &'a T) -> &'b T {
unsafe { &*(r as *const T) }
}
struct NaughtyLocal<'key /* always 'static atm, but allow coercions */, T: ?Sized> {
_not_send_sync: PhantomData<*mut u8>,
// SAFETY: only use this as &'a T when there is some &'a Self around
the_t: &'key T,
}
impl<T: 'static> NaughtyLocal<'static, T> {
pub fn new(key: &'static LocalKey<T>) -> Self
{
key.with(|value| NaughtyLocal {
_not_send_sync: PhantomData,
// SAFETY: The data is alive for the lifetime of the surrounding thread.
// But, since this datatype is not send nor sync, it itself proves
// that the thread is still alive!
the_t: unsafe { promote_lifetime(value) },
})
}
}
impl<'key, T: ?Sized> Deref for NaughtyLocal<'key, T> {
type Target = T;
fn deref(&self) -> &T {
self.the_t
}
}
The reason to have this is two-fold: I always disliked how LocalKey::with forced you to nest a code-block. Secondly, that API doesn't play well with async code. Let me show:
thread_local! {
static FRANKENSTEIN: String = String::from("It lives!");
}
fn foo() {
FRANKENSTEIN.with(|monster| {
println!("{monster}");
});
// vs (no closure, no extra scope)
let monster = NaughtyLocal::new(&FRANKENSTEIN);
print!("{}", *monster);
}
and, when using it with async
type SomeRefFuture = /* as an example, see async_once_cell::Lazy */;
impl<'a> IntoFuture for Pin<&'a SomeRefFuture> {
type Output = &'a i32;
// ...
}
thread_local! {
static FUT: SomeRefFuture = SomeRefFuture::new();
}
async fn bar() -> i32 {
let future = NaughtyLocal::new(&FUT);
// SAFETY: we promise not to move the future
let future = unsafe { Pin::new_unchecked(future) };
future.as_ref().await
// This future is not Send nor Sync
}
I'm not even sure how I would write async fn bar without the abstraction (a manual poll_fn perhaps? Still, I suppose one should make sure that the future can't move to other threads).
All of this however, is on a shaky footing and depends on the safety of NaughtyLocal. Pointing out flaws, criticism of the idea and are welcome ![]()