I'm looking into possible strategies to fix some of the soundness holes in macroquad
, which come from its use of static mut
variables. In order to minimize the amount of code reorganization, I came up with this primitive to reflect the kind patterns that macroquad
uses.
It's similar in idea to scoped-tls
, but is designed for &mut
references. Aliasing is prevented by removing the pointer from the static Cell
while a borrow is active and returning it afterwards. If the wrong value is present when replacing a borrow or ending a loan, the thread will panic.
Is this implementation sound (or can it be made so), or is there some fundamental problem that I'm missing?
(cc @parasyte, who filed the macroquad issue and @alexcrichton, who wrote scoped-tls
)
Edit: I'm also not happy with the name, and would be open to suggestions
use std::cell::Cell;
use std::thread::LocalKey;
use std::ptr::{NonNull, null_mut};
use std::ops::{Deref,DerefMut};
/// A thread-local static location which provieds &mut references to deep
/// stack frames.
pub struct StackTunnel<T:'static>(&'static LocalKey<Cell<*mut T>>);
// Template for definition macro (w/ payload Vec<u32>) -------------------
pub static VEC_TUNNEL:StackTunnel<Vec<u32>> = unsafe {
thread_local! {
static SLOT: Cell<*mut Vec<u32>> = Cell::new(null_mut());
}
crate::StackTunnel::new(&SLOT)
};
// End template ----------------------------------------------
impl<T:'static> StackTunnel<T> {
/// Safety: All access to `slot` must go through this instance, and it must be initialized to null
pub const unsafe fn new(slot: &'static LocalKey<Cell<*mut T>>)->Self {
StackTunnel(slot)
}
/// Leak val and make it available until the end of the program.
/// Will panic if there is an outstanding loan, but not necessarily immediately.
pub fn give_leak(&self, val: Box<T>) {
self.0.with(|cell| {
assert!(cell.get().is_null());
cell.set(Box::leak(val));
});
}
/// Provide a mutable reference to T to functions down the stack somewhere
pub fn loan<R>(&'static self, val: &mut T, f: impl FnOnce()->R)->R {
let next = val as *mut T;
let prev = self.0.with(|cell| cell.replace(next));
struct PanicGuard<T:'static> { slot: &'static StackTunnel<T>, next: *mut T, prev: *mut T }
impl<T:'static> Drop for PanicGuard<T> {
fn drop(&mut self) {
// Replace previous value, and panic if we saw something unexpected
assert_eq!(self.next, self.slot.0.with(|cell| cell.replace(self.prev)));
}
}
let _guard = PanicGuard { slot: self, next, prev };
f()
}
/// Borrow the mutable reference that was loaned in a higher stack frame.
/// Will panic if no such reference is available.
pub fn borrow_mut(&'static self)->StackTunnelGuard<T> {
let value = self.0.with(|cell| cell.replace(null_mut()));
StackTunnelGuard {
slot: self,
value: NonNull::new(value).expect("No reference available")
}
}
}
/// Temporary access to &mut T; will return the borrow on drop.
pub struct StackTunnelGuard<T:'static> {
slot: &'static StackTunnel<T>,
value: NonNull<T>,
}
impl<T:'static> Deref for StackTunnelGuard<T> {
type Target = T;
fn deref(&self)->&T {
unsafe { &*self.value.as_ptr() }
}
}
impl<T:'static> DerefMut for StackTunnelGuard<T> {
fn deref_mut(&mut self)->&mut T {
unsafe { &mut *self.value.as_ptr() }
}
}
impl<T:'static> Drop for StackTunnelGuard<T> {
fn drop(&mut self) {
let p = self.slot.0.with(|cell| cell.replace(self.value.as_ptr()));
assert!(p.is_null());
}
}