Skip to content

Instantly share code, notes, and snippets.

@zicklag
Last active October 19, 2023 16:42
Show Gist options
  • Save zicklag/82d2dd349fd1492ef4fde3f43b432d64 to your computer and use it in GitHub Desktop.
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
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