Last active
October 19, 2023 16:42
-
-
Save zicklag/82d2dd349fd1492ef4fde3f43b432d64 to your computer and use it in GitHub Desktop.
**Probalby Not Sound:** An attempt at simplifying https://gist.github.com/kyren/9b461b880ce37a36320e77e02cdf4134
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use paste::paste; | |
use std::{cell::RefCell, marker::PhantomData, rc::Rc}; | |
/// Trait that allows you to determine what the type of something would be if you changed its | |
/// lifetime. | |
/// | |
/// It is not recommended to implement this trait yourself, if not necessary. | |
/// | |
/// You may use the [`impl_WithLifetime`] macro to safely implement this for types with | |
/// a single lifetime parameter, and it is automatically implemented for [`&T`] and [`&mut T`]. | |
/// | |
/// # Safety | |
/// The `Lt` type must be a type with the exact same representation as `Self`, except | |
/// with the lifetime set to the `'a` lifetime. | |
/// | |
/// In other words, it should always be sound to [`transmute`][std::mem::transmute] for | |
/// any lifetimes `'a` and `'b` from [`WithLifetime::Lt<'a>`] to [`WithLifetime::Lt<'b>`]. | |
/// | |
/// Note that this doesn't mean that it has to be sound to actually _use_ the transmuted | |
/// value with the new lifetime `'b`. Despite unsafely re-labeling the lifetime, it must | |
/// still only be used within it's original lifetime `'a`. | |
pub unsafe trait WithLifetime { | |
type Lt<'a>: 'a; | |
} | |
unsafe impl<'a, T: 'static> WithLifetime for &'a T { | |
type Lt<'lt> = &'lt T; | |
} | |
unsafe impl<'a, T: 'static> WithLifetime for &'a mut T { | |
type Lt<'lt> = &'lt mut T; | |
} | |
/// A scope that can be used to create wrappers for reference types that are `'static` regardless | |
/// of the underlying lifetime. This uses runtime borrow checking to ensure soundness and that | |
/// the extended reference is never used outside of it's original lifetime. | |
pub struct Freeze<T>(PhantomData<T>); | |
impl<T: Freezable> Freeze<T> { | |
/// Create a new scope that freezes the provided arguments. | |
/// # Example | |
/// ```ignore | |
/// Freeze::<(&DataA, &DataA, DataB)>::scope((data1, data2, data3), |(data1, data2, data3)| { | |
/// assert_eq!(data1.borrow().0, 1); | |
/// assert_eq!(data2.borrow().0, 2); | |
/// assert_eq!(*data3.borrow().0, 3); | |
/// }); | |
/// ``` | |
pub fn scope<R, F>(t: <T::Unfrozen as WithLifetime>::Lt<'_>, f: F) | |
where | |
T: Freezable, | |
F: FnOnce(T::Frozen) -> R, | |
{ | |
let frozen = unsafe { T::freeze(t) }; | |
f(frozen.clone()); | |
T::thaw(frozen); | |
} | |
} | |
/// A frozen value. | |
/// | |
/// This is a `'static` container that references a `non-static` value. You may attempt to borrow | |
/// the inner value, but it will panic if the non-static value has fallen out of scope. | |
pub struct Frozen<T: 'static>(Rc<RefCell<Option<T>>>); | |
impl<T> Clone for Frozen<T> { | |
fn clone(&self) -> Self { | |
Self(self.0.clone()) | |
} | |
} | |
impl<T> Frozen<T> { | |
/// Execute a closure with access to the frozen value. | |
/// # Panics | |
/// Panics if the frozen value is no longer within scope. | |
pub fn with<R, F>(&self, f: F) | |
where | |
F: FnOnce(&mut T) -> R, | |
{ | |
let mut guard = RefCell::borrow_mut(&self.0); | |
let borrow = guard.as_mut().unwrap(); | |
f(borrow); | |
} | |
} | |
/// Trait implemented for things that may be passed to [`Freeze::scope`]. | |
/// | |
/// It is usually not necessary to implement this yourself. It is already implemented for | |
/// all types that implement [`WithLifetime`] and for tuples of types that implement | |
/// [`WithLifetime`]. | |
pub trait Freezable { | |
type Unfrozen: WithLifetime; | |
type Frozen: Clone; | |
/// # Safety | |
/// You **must** call thaw on the frozen value before the lifetime expires. | |
unsafe fn freeze(t: <Self::Unfrozen as WithLifetime>::Lt<'_>) -> Self::Frozen; | |
fn thaw(frozen: Self::Frozen); | |
} | |
impl<T: WithLifetime> Freezable for T { | |
type Unfrozen = T; | |
type Frozen = Frozen<<T as WithLifetime>::Lt<'static>>; | |
unsafe fn freeze<'a>(t: <T as WithLifetime>::Lt<'a>) -> Self::Frozen { | |
let extended = | |
std::mem::transmute::<<T as WithLifetime>::Lt<'a>, <T as WithLifetime>::Lt<'static>>(t); | |
Frozen(Rc::new(RefCell::new(Some(extended)))) | |
} | |
fn thaw(frozen: Self::Frozen) { | |
match RefCell::try_borrow_mut(&frozen.0) { | |
Ok(mut borrow) => { | |
borrow.take(); | |
} | |
Err(_) => std::process::abort(), | |
} | |
} | |
} | |
#[doc(hidden)] | |
#[repr(transparent)] | |
pub struct FreezeSet<T>(pub T); | |
macro_rules! impl_with_lifetime_for_tuple { | |
() => (); | |
($($id:ident),*) => { | |
unsafe impl<$($id: WithLifetime),*> WithLifetime for FreezeSet<($($id),*,)> { | |
type Lt<'a> = ( | |
$( <$id as WithLifetime>::Lt<'a>, )* | |
); | |
} | |
}; | |
} | |
macro_rules! impl_with_lifetime_for_all_tuples { | |
() => {}; | |
($first:ident) => { | |
impl_with_lifetime_for_tuple!($first); | |
}; | |
($first:ident, $($id:ident),*) => { | |
impl_with_lifetime_for_tuple!($first, $($id),*); | |
impl_with_lifetime_for_all_tuples!($($id),*); | |
}; | |
} | |
impl_with_lifetime_for_all_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); | |
macro_rules! impl_freezable_for_tuple { | |
() => {}; | |
($($id:ident),*) => { | |
impl<$($id: WithLifetime),*> Freezable for ($( $id ),*,) { | |
type Unfrozen = FreezeSet<($($id),*,)>; | |
type Frozen = ( | |
$(<$id as Freezable>::Frozen),*, | |
); | |
unsafe fn freeze(unfrozen: <Self::Unfrozen as WithLifetime>::Lt<'_>) -> Self::Frozen { | |
paste! { | |
#[allow(non_snake_case)] | |
let ( | |
$( [<unfrozen $id>] ),*, | |
) = unfrozen; | |
($( | |
<$id as Freezable>::freeze([<unfrozen $id>]) | |
),*,) | |
} | |
} | |
fn thaw(frozen: Self::Frozen) { | |
paste! { | |
#[allow(non_snake_case)] | |
let ( | |
$( [<frozen $id>] ),*, | |
) = frozen; | |
$( | |
<$id as Freezable>::thaw([<frozen $id>]); | |
)* | |
} | |
} | |
} | |
}; | |
} | |
macro_rules! impl_freezable_for_all_tuples { | |
() => {}; | |
($first:ident) => { | |
impl_freezable_for_tuple!($first); | |
}; | |
($first:ident, $($id:ident),*) => { | |
impl_freezable_for_tuple!($first, $($id),*); | |
impl_freezable_for_all_tuples!($($id),*); | |
}; | |
} | |
impl_freezable_for_all_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q); | |
/// Safely implement [`WithLifetime`] for a type with a single lifetime parameter. | |
#[macro_export] | |
macro_rules! impl_WithLifetime { | |
($struct:ident) => { | |
unsafe impl<'a> WithLifetime for $struct<'a> { | |
type Lt<'any> = $struct<'any>; | |
} | |
}; | |
} | |
/// Helper macro to more easily freeze multiple arguments. This is merely syntax sugar, | |
/// for the [`Freeze::scope`] function. | |
/// | |
/// # Example | |
/// ```ignore | |
/// fn multi_freeze(data1: &DataA, data2: &DataA, data3: DataB) { | |
/// freeze!( | |
/// |data1: Frozen<&DataA>, data2: Frozen<&DataA>, data3: Frozen<DataB>| { | |
/// assert_eq!(data1.borrow().0, 1); | |
/// assert_eq!(data2.borrow().0, 2); | |
/// assert_eq!(*data3.borrow().0, 3); | |
/// } | |
/// ); | |
/// } | |
/// ``` | |
#[macro_export] | |
macro_rules! freeze { | |
( | |
|$($arg:ident : Frozen<$typ:ty>),* $(,)?| { | |
$($body:tt)* | |
} | |
) => { | |
Freeze::<($($typ),*)>::scope(($($arg),*), |($($arg),*)| { | |
$($body)* | |
}) | |
}; | |
} | |
#[cfg(test)] | |
mod test { | |
use std::sync::mpsc::channel; | |
use super::*; | |
struct DataA(i32); | |
struct DataB<'a>(&'a i32); | |
impl_WithLifetime!(DataB); | |
fn wants_static_data<T: 'static>(_t: T) {} | |
#[test] | |
#[should_panic] | |
fn bad_borrow_panics() { | |
let data = DataA(32); | |
borrows_some_data_bad(&data); | |
} | |
fn borrows_some_data_bad(data: &DataA) { | |
// Create a channel that will be used to leak our borrow | |
let (send, recv) = channel(); | |
// Create a frozen scope for our data reference. | |
Freeze::<&DataA>::scope(data, |frozen| { | |
// We can send our frozen handle anywhere that expects a `'static` lifetime. | |
wants_static_data(frozen.clone()); | |
// Including out of the scope! Bad sender! | |
send.send(frozen).ok(); | |
}); | |
// We can receive our frozen value | |
let b = recv.recv().unwrap(); | |
// But trying to access it will panic. | |
b.with(|data| { | |
dbg!(data.0); | |
}); | |
} | |
#[test] | |
fn good_borrow_works() { | |
let data = DataA(32); | |
borrows_some_data_good(&data); | |
let data2 = DataB(&32); | |
good_borrow_2(data2); | |
} | |
fn borrows_some_data_good(data: &DataA) { | |
Freeze::<&DataA>::scope(data, |frozen| { | |
// We can send our frozen handle anywhere that expects a `'static` lifetime. | |
wants_static_data(frozen.clone()); | |
// And we can access it | |
frozen.with(|data| { | |
assert_eq!(data.0, 32); | |
}) | |
}); | |
} | |
fn good_borrow_2(data: DataB) { | |
Freeze::<DataB>::scope(data, |frozen| { | |
wants_static_data(frozen.clone()); | |
frozen.with(|data| { | |
assert_eq!(*data.0, 32); | |
}) | |
}); | |
} | |
#[test] | |
fn multi_freeze() { | |
let data1 = DataA(1); | |
let data2 = DataA(2); | |
let data3 = DataB(&3); | |
multi_freeze_borrow(&data1, &data2, data3); | |
} | |
fn multi_freeze_borrow(data1: &DataA, data2: &DataA, data3: DataB) { | |
Freeze::<(&DataA, &DataA, DataB)>::scope((data1, data2, data3), |(data1, data2, data3)| { | |
data1.with(|data| assert_eq!(data.0, 1)); | |
data2.with(|data| assert_eq!(data.0, 2)); | |
data3.with(|data| assert_eq!(*data.0, 3)); | |
}); | |
} | |
#[test] | |
fn freeze_macro() { | |
let data1 = DataA(1); | |
let data2 = DataA(2); | |
let data3 = DataB(&3); | |
multi_freeze_borrow_macro(&data1, &data2, data3); | |
} | |
fn multi_freeze_borrow_macro(data1: &DataA, data2: &DataA, data3: DataB) { | |
freeze!( | |
|data1: Frozen<&DataA>, data2: Frozen<&DataA>, data3: Frozen<DataB>| { | |
data1.with(|data| assert_eq!(data.0, 1)); | |
data2.with(|data| assert_eq!(data.0, 2)); | |
data3.with(|data| assert_eq!(*data.0, 3)); | |
} | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment