
譯者注:如果你一時(shí)半會(huì)沒啃動(dòng)Pinning,也別心急,試試閱讀這篇《Rust的Pin與Unpin - Folyd》,理解起來會(huì)容易不少。
Pinning詳解
讓我們嘗試使用一個(gè)比較簡單的示例來了解pinning。前面我們遇到的問題,最終可以歸結(jié)為如何在Rust中處理自引用類型的引用的問題。
現(xiàn)在,我們的示例如下所示:
use std::pin::Pin;
?
#[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 {
unsafe {&*(self.b)}
}
}
Test提供了獲取字段a和b值引用的方法。由于b是對(duì)a的引用,因此我們將其存儲(chǔ)為指針,因?yàn)镽ust的借用規(guī)則不允許我們定義這種生命周期。現(xiàn)在,我們有了所謂的自引用結(jié)構(gòu)。
如果我們不移動(dòng)任何數(shù)據(jù),則該示例運(yùn)行良好,可以通過運(yùn)行示例觀察:
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());
?
}
我們得到了我們期望的結(jié)果:
a: test1, b: test1
a: test2, b: test2
讓我們看看如果將test1與test2交換導(dǎo)致數(shù)據(jù)移動(dòng)會(huì)發(fā)生什么:
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());
?
}
我們天真的以為應(yīng)該兩次獲得test1的調(diào)試打印,如下所示:
a: test1, b: test1
a: test1, b: test1
但我們得到的是:
a: test1, b: test1
a: test1, b: test2
test2.b的指針仍然指向了原來的位置,也就是現(xiàn)在的test1的里面。該結(jié)構(gòu)不再是自引用的,它擁有一個(gè)指向不同對(duì)象字段的指針。這意味著我們不能再依賴test2.b的生命周期和test2的生命周期的綁定假設(shè)了。
如果您仍然不確定,那么下面可以讓您確定了吧:
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);
test1.a = "I've totally changed now!".to_string();
println!("a: {}, b: {}", test2.a(), test2.b());
?
}
下圖可以幫助您直觀地了解正在發(fā)生的事情:

這很容易使它展現(xiàn)出未定義的行為并“壯觀地”失敗。
Pinning實(shí)踐
讓我們看下Pinning和Pin類型如何幫助我們解決此問題。
Pin類型封裝了指針類型,它保證不會(huì)移動(dòng)指針后面的值。例如,Pin<&mut T>,Pin<&T>,Pin<Box<T>>都保證T不被移動(dòng),當(dāng)且僅當(dāng)T:!Unpin。
大多數(shù)類型在移動(dòng)時(shí)都沒有問題。這些類型實(shí)現(xiàn)了Unpin特型。可以將Unpin類型的指針自由的放置到Pin中或從中取出。例如,u8是Unpin,因此Pin<&mut u8>的行為就像普通的&mut u8。
但是,固定后無法移動(dòng)的類型具有一個(gè)標(biāo)記為!Unpin的標(biāo)記。由async / await創(chuàng)建的Futures就是一個(gè)例子。
棧上固定
回到我們的例子。我們可以使用Pin來解決我們的問題。讓我們看一下我們的示例的樣子,我們需要一個(gè)pinned的指針:
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<'a>(self: Pin<&'a mut Self>) {
let self_ptr: *const String = &self.a;
let this = unsafe { self.get_unchecked_mut() };
this.b = self_ptr;
}
?
fn a<'a>(self: Pin<&'a Self>) -> &'a str {
&self.get_ref().a
}
?
fn b<'a>(self: Pin<&'a Self>) -> &'a String {
unsafe { &*(self.b) }
}
}
如果我們的類型實(shí)現(xiàn)!Unpin,則將對(duì)象固定到棧始終是不安全的。您可以使用諸如[pin_utils](https://docs.rs/pin-utils/0.1.0/pin_utils/)之類的板條箱來避免在固定到棧時(shí)編寫我們自己的不安全代碼。 下面,我們將對(duì)象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()));
}
如果現(xiàn)在嘗試移動(dòng)數(shù)據(jù),則會(huì)出現(xiàn)編譯錯(cuò)誤:
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()));
}
類型系統(tǒng)阻止我們移動(dòng)數(shù)據(jù)。
需要注意,棧固定將始終依賴于您在編寫unsafe時(shí)提供的保證。雖然我們知道&'a mut T所指的對(duì)象在生命周期'a中固定,但我們不知道'a結(jié)束后數(shù)據(jù)&'a mut T指向的數(shù)據(jù)是不是沒有移動(dòng)。如果移動(dòng)了,就違反了Pin約束。
容易犯的一個(gè)錯(cuò)誤就是忘記隱藏原始變量,因?yàn)槟梢詃ropPin并移動(dòng)&'a mut T背后的數(shù)據(jù),如下所示(這違反了Pin約束):
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類型固定到堆將為我們的數(shù)據(jù)提供穩(wěn)定的地址,所以我們知道指向的數(shù)據(jù)在固定后將無法移動(dòng)。與棧固定相反,我們知道數(shù)據(jù)將在對(duì)象的生命周期內(nèi)固定。
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.as_ref().a;
unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };
?
boxed
}
?
fn a<'a>(self: Pin<&'a Self>) -> &'a str {
&self.get_ref().a
}
?
fn b<'a>(self: Pin<&'a Self>) -> &'a String {
unsafe { &*(self.b) }
}
}
?
pub fn main() {
let mut test1 = Test::new("test1");
let mut 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());
}
有的函數(shù)要求與之配合使用的futures是Unpin。對(duì)于沒有Unpin的Future或Stream,您首先必須使用Box::pin(用于創(chuàng)建Pin<Box<T>>)或pin_utils::pin_mut!宏(用于創(chuàng)建Pin<&mut T>)來固定該值。 Pin<Box<Fut>>和Pin<&mut Fut>都可以作為futures使用,并且都實(shí)現(xiàn)了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
總結(jié)
- 如果是
T:Unpin(這是默認(rèn)設(shè)置),則Pin <'a, T>完全等于&'a mut T。換句話說:Unpin表示即使固定了此類型也可以移動(dòng),因此Pin將對(duì)這種類型沒有影響。 - 如果是
T:!Unpin,獲得已固定T的&mut T需要unsafe。 - 大多數(shù)標(biāo)準(zhǔn)庫類型都實(shí)現(xiàn)了
Unpin。對(duì)于您在Rust中遇到的大多數(shù)“常規(guī)”類型也是如此。由async / await生成的Future是此規(guī)則的例外。 - 您可以在nightly使用功能標(biāo)記添加
!Unpin綁定到一個(gè)類型上,或者通過在stable將std::marker::PhantomPinned添加到您的類型上。 - 您可以將數(shù)據(jù)固定到?;蚨焉?。
- 將
!Unpin對(duì)象固定到棧上需要unsafe。 - 將
!Unpin對(duì)象固定到堆并不需要unsafe。使用Box::pin可以執(zhí)行此操作。 - 對(duì)于
T:!Unpin的固定數(shù)據(jù),您必須保持其不可變,即從固定到調(diào)用drop為止,其內(nèi)存都不會(huì)失效或重新利用。這是pin約束的重要組成部分。