Rust-Pin

162 阅读5分钟

Pin的出现主要是为了解决自引用问题

自引用问题

Test 类型提供了方法,来获取字段 a 或 b 的引用。因为 b 是指向 a 的引用,但由于 Rust 的借用规则,我们不能定义它的生命周期(lifetime),所以我们把它存成指针。这就是一个自引用结构体


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
}

impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
        }
    }

    fn init(&mut self) {
        let self_ref: *const String = &self.a;
        self.b = self_ref;
    }

    fn a(&self) -> &str {
        &self.a
    }

    fn b(&self) -> &String {
        assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
        unsafe { &*(self.b) }
    }
}
}

如果不把我们的数据四处转移,我们的例子可以运行得很好:

fn main() {
    let mut test1 = Test::new("test1");
    test1.init();
    let mut test2 = Test::new("test2");
    test2.init();

    println!("a: {}, b: {}", test1.a(), test1.b());
    println!("a: {}, b: {}", test2.a(), test2.b());

}

我们可以得到预期结果:

a: test1, b: test1
a: test2, b: test2

来看看如果我们把 test1 和 test2 交换了,会发生什么:

fn main() {
    let mut test1 = Test::new("test1");
    test1.init();
    let mut test2 = Test::new("test2");
    test2.init();

    println!("a: {}, b: {}", test1.a(), test1.b());
    std::mem::swap(&mut test1, &mut test2);
    println!("a: {}, b: {}", test2.a(), test2.b());

}

我们得到得结果是:

a: test1, b: test1
a: test1, b: test2

其根本原因是test1和test2在内存中的位置变了 image.png

Pin的使用

  1. 如果 T: Unpin(默认会实现),那么 Pin<'a, T> 完全等价于 &'a mut T。换言之: Unpin 意味着这个类型被移走也没关系,就算已经被固定了,所以 Pin 对这样的类型毫无影响。
  2. 如果 T: !Unpin, 获取已经被固定的 T 类型示例的 &mut T需要 unsafe。
  3. 标准库中的大部分类型实现 Unpin,在 Rust 中遇到的多数“平常”的类型也是一样。但是, async/await 生成的 Future 是个例外。
  4. 你可以在 nightly 通过特性标记来给类型添加 !Unpin 约束,或者在 stable 给你的类型加 std::marker::PhatomPinned 字段。
  5. 你可以将数据固定到栈上或堆上
  6. 固定 !Unpin 对象到栈上需要 unsafe
  7. 固定 !Unpin 对象到堆上不需要 unsafeBox::pin可以快速完成这种固定

固定到栈上

回到我们的例子。我们能用 Pin 来解决我们的问题。我们来看看,如果我们需要用一个固定的指针,我们的例子会编程什么样:


use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}


impl Test {
    fn new(txt: &str) -> Self {
        Test {
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned, // This makes our type `!Unpin`
        }
    }

    fn init(self: Pin<&mut Self>) {
        let self_ptr: *const String = &self.a;
        let this = unsafe { self.get_unchecked_mut() };
        this.b = self_ptr;
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        assert!(!self.b.is_null(), "Test::b called without Test::init being called first");
        unsafe { &*(self.b) }
    }
}

如果我们的类型实现了 !Unpin,那么固定这个类型的对象到栈上总是 unsafe 的行为。你可以用像是 pin_utils 的库来在将数据固定到栈上的时候避免写 unsafe

下面,我们将对象 test1 和 test2 固定到栈上:

pub fn main() {
    // test1 is safe to move before we initialize it
    let mut test1 = Test::new("test1");
    // Notice how we shadow `test1` to prevent it from being accessed again
    let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
    Test::init(test1.as_mut());

    let mut test2 = Test::new("test2");
    let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
    Test::init(test2.as_mut());

    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}

现在,如果我们尝试将我们的数据移走,我们会遇到编译错误:

pub fn main() {
    let mut test1 = Test::new("test1");
    let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };
    Test::init(test1.as_mut());

    let mut test2 = Test::new("test2");
    let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };
    Test::init(test2.as_mut());

    println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));
    std::mem::swap(test1.get_mut(), test2.get_mut());
    println!("a: {}, b: {}", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}

类型系统会阻止我们移动这些数据。

重点记住,固定到栈总是依赖你在写 unsafe 代码时提供的保证。例如,我们知道了 &'a mut T 的 被指向对象(pointee)  在生命周期 'a 期间固定,我们不知道被 &'a mut T 指向数据是否在 'a 结束后仍然不被移动。如果移动了,将会违反固定的协约。

另外一个常见错误是忘记遮蔽(shadow)原本的变量,因为你可以释放 Pin 然后移动数据到 &'a mut T,像下面这样(这违反了固定的协约):

fn main() {
   let mut test1 = Test::new("test1");
   let mut test1_pin = unsafe { Pin::new_unchecked(&mut test1) };
   Test::init(test1_pin.as_mut());

   drop(test1_pin);
   println!(r#"test1.b points to "test1": {:?}..."#, test1.b);

   let mut test2 = Test::new("test2");
   mem::swap(&mut test1, &mut test2);
   println!("... and now it points nowhere: {:?}", test1.b);
}

固定到堆上

固定 !Unpin 类型到堆上,能给我们的数据一个稳定的地址,所以我们知道我们指向的数据不会在被固定之后被移动走。和在栈上固定相反,我们知道整个对象的生命周期期间数据都会被固定在一处。

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
    a: String,
    b: *const String,
    _marker: PhantomPinned,
}

impl Test {
    fn new(txt: &str) -> Pin<Box<Self>> {
        let t = Test {
            a: String::from(txt),
            b: std::ptr::null(),
            _marker: PhantomPinned,
        };
        let mut boxed = Box::pin(t);
        let self_ptr: *const String = &boxed.a;
        unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };

        boxed
    }

    fn a(self: Pin<&Self>) -> &str {
        &self.get_ref().a
    }

    fn b(self: Pin<&Self>) -> &String {
        unsafe { &*(self.b) }
    }
}

pub fn main() {
    let test1 = Test::new("test1");
    let test2 = Test::new("test2");

    println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());
    println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}

一些函数需要他们协作的 future 是 Unpin 的。为了让这些函数使用不是 Unpin 的 Future 或 Stream,你首先需要这个值固定,要么用 Box::pin(创建 Pin<Box<T>>)要么使用 pin_utils::pin_mut!(创建 Pin<&mut T>)。Pin<Box<Fut>> 和 Pin<&mut Fut> 都能用作 future,并且都实现了 Unpin

例如:

use pin_utils::pin_mut; // `pin_utils` is a handy crate available on crates.io

// A function which takes a `Future` that implements `Unpin`.
fn execute_unpin_future(x: impl Future<Output = ()> + Unpin) { /* ... */ }

let fut = async { /* ... */ };
execute_unpin_future(fut); // Error: `fut` does not implement `Unpin` trait

// Pinning with `Box`:
let fut = async { /* ... */ };
let fut = Box::pin(fut);
execute_unpin_future(fut); // OK

// Pinning with `pin_mut!`:
let fut = async { /* ... */ };
pin_mut!(fut);
execute_unpin_future(fut); // OK