#self-referential #data-structures #pinned #self-reference #aliasable #unboxed

无 std pinned-aliasable

用于自引用数据结构中未装箱可别名的基于 Pin 的临时解决方案

4 个版本

0.1.3 2022年5月11日
0.1.2 2022年3月26日
0.1.1 2021年7月20日
0.1.0 2021年6月27日

Rust 模式 中排名 #681

Download history 30/week @ 2024-03-15 27/week @ 2024-03-22 50/week @ 2024-03-29 27/week @ 2024-04-05 79/week @ 2024-04-12 63/week @ 2024-04-19 45/week @ 2024-04-26 29/week @ 2024-05-03 26/week @ 2024-05-10 30/week @ 2024-05-17 30/week @ 2024-05-24 37/week @ 2024-05-31 38/week @ 2024-06-07 62/week @ 2024-06-14 27/week @ 2024-06-21 10/week @ 2024-06-28

每月下载 142
8 包中使用(直接使用 4 个)

MIT 许可证

26KB
124

pinned-aliasable

Pin 基于的用于自引用数据结构中未装箱可别名的临时解决方案。

唯一性

为了优化,Rust 编译器喜欢假设所有可变引用(&mut)都是完全唯一的。这种唯一性给了它一些极其重要的保证,可以轻松利用来编写更快的代码,例如

  • 如果引用在之间没有写入,则从一个 &mut 位置进行的所有读取都保证是相同的。
  • 除非显式地用相同的可变引用覆盖,否则写入位置将保持不变。
  • 在存在期间,没有人能够看到不使用该可变引用而存储在可变引用后面的数据。

一个简单示例是此代码,其中 &mut 唯一性很有用

fn foo(x: &mut i32) -> i32 {
    *x = 400;
    do_some_other_stuff();
    *x
}

编译器会将此函数优化为始终返回常量 400,而不是每次都要实际加载 x 后面的值。它之所以能够做到这一点,仅仅是因为 x 是一个唯一指针;如果不是,那么 do_some_other_stuff 在之间可能对其进行修改,总是返回常量将导致意外行为。

自引用类型

然而,当使用自引用类型时,这种假设开始出现问题。如果 x 不是一个简单的整数,而是一个包含对其自身引用的类型,会发生什么?尽管这并不明显,但实际上唯一性保证已被违反:存储在 x 中的自引用与 &mutx 的别名,这意味着可变引用 不再是唯一的!这个问题不仅仅是理论上的,它还导致在野外出现编译错误。例如,这个基于 owning-ref 包中实际存在的一个类型安全的错误

use std::cell::Cell;

struct Helper {
    reference: &'static Cell<u8>,
    owner: Box<Cell<u8>>,
}
fn helper(x: Helper) -> u8 {
    x.owner.set(10);
    x.reference.set(20);
    x.owner.get()
}

let owner = Box::new(Cell::new(0));
let reference = unsafe { &*(&*owner as *const Cell<u8>) };
let x = Helper { reference, owner };
println!("{}", helper(x));

当以发布模式运行时,此程序输出的是10而不是预期的值20。这是因为在内置的helper函数中,优化器看到我们对Cell<u8>(如&mut这样的Box,被视为唯一指针)有唯一访问权,因此它假设对该位置的任何写入都不会被覆盖。但由于我们违反了优化器的预期,结果变得毫无意义。

那么解决方案是什么呢?目前来说,还没有一个既合理又不会牺牲性能的解决方案。可以使用与Box不同的智能指针,这种指针不允许编译器假设其指针是唯一的,并且对于上述情况几乎没有性能影响——但如果自引用的值一开始就不是被装箱的,那么这会是一个更加困难的选择。

很可能Rust最终会找到解决这个问题的方法,这是一个需要修复的已知bug。至于这个解决方案将是什么样子,它很可能是形如存在于libcore中的Aliasable<T>包装类型的形状,它保证了任何对值的&mut引用都不会被视为唯一的,这样就可以存在一个&mut Aliasable<T>和要么一个&mut T,要么任何数量的&T(但不能两个&mut T或两个&mut Aliasable<T>,常规的借用规则仍然适用)。不幸的是,这个类型今天还不存在,甚至还没有关于它的具体提案。那么我们在这段时间里能做些什么呢?

解决方案

虽然不能创建稳定的自引用类型,但事实上我们可以创建不稳定的自引用类型,而且我们知道它们不会导致编译错误。这是因为为了确保异步块(它们生成自引用类型)在今天的Rust中不会产生编译错误,我们暂时放宽了对&mut唯一性规则:它只适用于引用的类型没有实现Unpin的情况。因此,为了创建这些自引用类型,我们只需确保它们是!Unpin,一切就会像预期的那样工作。

然而,手动进行这项操作并维护所有随之而来的不变性是一件痛苦的事情,更不用说一旦Rust支持真正的自引用类型,未来所需的迁移工作量了。这就是这个crate的作用所在。它提供了一个类型 Aliasable<T>,它既抽象了将容器类型 !Unpin 的操作,又应与假设的libcore等效类型兼容。一旦 Aliasable<T> 被添加到语言本身,我就能基于它发布这个crate的新版本,并撤回所有之前的版本,那时它们将是不稳定和过时的。

这就是全部了!虽然这个crate很小,但它对于定义任何类型的自引用类型非常有用,因为你不再需要过多地担心是否会引发错误编译。

然而,有一个重要的细节需要注意。还记得我上面提到 Box 也被视为始终唯一的指针吗?这是真的,但不幸的是,它们没有像 &mut 那样的漏洞。这意味着你在处理boxed Aliasable<T> 时必须非常小心——确保任何接受它们为值的函数都委托给一个接受唯一或共享引用的第二个函数,这样Rust就不会假设你的指针是唯一的。

示例

一个存储自身子切片的boxed切片

use core::pin::Pin;
use core::ptr::NonNull;
use core::slice::SliceIndex;
use core::cell::UnsafeCell;

use pin_project::pin_project;
use pin_utils::pin_mut;
use pinned_aliasable::Aliasable;

#[pin_project]
pub struct OwningSlice<T: 'static> {
    // In a real implementation you would avoid the `T: 'static` bound by using some kind of
    // raw pointer here.
    slice: Option<&'static mut [T]>,
    #[pin]
    data: Aliasable<UnsafeCell<Box<[T]>>>,
}
impl<T: 'static> From<Box<[T]>> for OwningSlice<T> {
    fn from(data: Box<[T]>) -> Self {
        Self {
            slice: None,
            data: Aliasable::new(UnsafeCell::new(data)),
        }
    }
}
impl<T> OwningSlice<T> {
    pub fn slice(self: Pin<&mut Self>, range: impl SliceIndex<[T], Output = [T]>) {
        let mut this = self.project();
        let current_slice = this.slice.take().unwrap_or_else(|| {
            unsafe { &mut **this.data.as_ref().get_extended().get() }
        });
        *this.slice = Some(&mut current_slice[range]);
    }
    pub fn get(self: Pin<&Self>) -> &[T] {
        let this = self.project_ref();
        this.slice.as_deref().unwrap_or_else(|| unsafe { &**this.data.get().get() })
    }
    pub fn get_mut(self: Pin<&mut Self>) -> &mut [T] {
        let this = self.project();
        let data = this.data.as_ref();
        this.slice.as_deref_mut().unwrap_or_else(|| unsafe { &mut **data.get().get() })
    }
}

let slice = OwningSlice::from(vec![1, 2, 3, 4, 5].into_boxed_slice());
pin_mut!(slice);
assert_eq!(slice.as_ref().get(), &[1, 2, 3, 4, 5]);

slice.as_mut().slice(1..);
assert_eq!(slice.as_ref().get(), &[2, 3, 4, 5]);

slice.as_mut().slice(2..=3);
assert_eq!(slice.as_ref().get(), &[4, 5]);

slice.as_mut().slice(0..0);
assert_eq!(slice.as_ref().get(), &[]);

一个对类型

use core::pin::Pin;
use core::cell::Cell;

use pin_project::{pin_project, pinned_drop};
use pin_utils::pin_mut;
use pinned_aliasable::Aliasable;

#[pin_project(PinnedDrop)]
pub struct Pair(#[pin] Aliasable<PairInner>);

struct PairInner {
    value: u64,
    other: Cell<Option<&'static PairInner>>,
}

#[pinned_drop]
impl PinnedDrop for Pair {
    fn drop(self: Pin<&mut Self>) {
        if let Some(other) = self.project().0.as_ref().get().other.get() {
            other.other.set(None);
        }
    }
}

impl Pair {
    pub fn new(value: u64) -> Self {
        Self(Aliasable::new(PairInner {
            value,
            other: Cell::new(None),
        }))
    }
    pub fn get(self: Pin<&Self>) -> u64 {
        self.project_ref().0.get().other.get().unwrap().value
    }
}

pub fn link_up(left: Pin<&Pair>, right: Pin<&Pair>) {
    let left = unsafe { left.project_ref().0.get_extended() };
    let right = unsafe { right.project_ref().0.get_extended() };
    left.other.set(Some(right));
    right.other.set(Some(left));
}

fn main() {
    let pair_1 = Pair::new(10);
    let pair_2 = Pair::new(20);
    pin_mut!(pair_1);
    pin_mut!(pair_2);

    link_up(pair_1.as_ref(), pair_2.as_ref());

    assert_eq!(pair_1.as_ref().get(), 20);
    assert_eq!(pair_2.as_ref().get(), 10);
}

许可证:MIT

无运行时依赖