譯者注:如果你一時(shí)半會沒啃動Pinning遭商,也別心急姨丈,試試閱讀這篇《Rust的Pin與Unpin - Folyd》巍扛,理解起來會容易不少。
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是對a的引用贡茅,因此我們將其存儲為指針秘蛇,因?yàn)镽ust的借用規(guī)則不允許我們定義這種生命周期。現(xiàn)在顶考,我們有了所謂的自引用結(jié)構(gòu)赁还。
如果我們不移動任何數(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ù)移動會發(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è)指向不同對象字段的指針。這意味著我們不能再依賴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
類型封裝了指針類型青扔,它保證不會移動指針后面的值。例如翩伪,Pin<&mut T>
微猖,Pin<&T>
,Pin<Box<T>>
都保證T
不被移動缘屹,當(dāng)且僅當(dāng)T:!Unpin
凛剥。
大多數(shù)類型在移動時(shí)都沒有問題。這些類型實(shí)現(xiàn)了Unpin
特型轻姿〉被冢可以將Unpin
類型的指針自由的放置到Pin
中或從中取出。例如踢代,u8
是Unpin
盲憎,因此Pin<&mut u8>
的行為就像普通的&mut u8
。
但是胳挎,固定后無法移動的類型具有一個(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
,則將對象固定到棧始終是不安全的磅甩。您可以使用諸如[pin_utils](https://docs.rs/pin-utils/0.1.0/pin_utils/)
之類的板條箱來避免在固定到棧時(shí)編寫我們自己的不安全代碼炊林。 下面,我們將對象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)在嘗試移動數(shù)據(jù)卷要,則會出現(xiàn)編譯錯誤:
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)阻止我們移動數(shù)據(jù)渣聚。
需要注意,棧固定將始終依賴于您在編寫unsafe
時(shí)提供的保證僧叉。雖然我們知道&'a mut T
所指的對象在生命周期'a
中固定奕枝,但我們不知道'a
結(jié)束后數(shù)據(jù)&'a mut T
指向的數(shù)據(jù)是不是沒有移動。如果移動了瓶堕,就違反了Pin約束隘道。
容易犯的一個(gè)錯誤就是忘記隱藏原始變量,因?yàn)槟梢詃ropPin
并移動&'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ù)在固定后將無法移動。與棧固定相反宛蚓,我們知道數(shù)據(jù)將在對象的生命周期內(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
。對于沒有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
表示即使固定了此類型也可以移動盖喷,因此Pin
將對這種類型沒有影響爆办。 - 如果是
T:!Unpin
,獲得已固定T的&mut T
需要unsafe课梳。 - 大多數(shù)標(biāo)準(zhǔn)庫類型都實(shí)現(xiàn)了
Unpin
距辆。對于您在Rust中遇到的大多數(shù)“常規(guī)”類型也是如此。由async / await生成的Future
是此規(guī)則的例外暮刃。 - 您可以在nightly使用功能標(biāo)記添加
!Unpin
綁定到一個(gè)類型上跨算,或者通過在stable將std::marker::PhantomPinned
添加到您的類型上。 - 您可以將數(shù)據(jù)固定到椡职茫或堆上诸蚕。
- 將
!Unpin
對象固定到棧上需要unsafe
。 - 將
!Unpin
對象固定到堆并不需要unsafe
。使用Box::pin
可以執(zhí)行此操作背犯。 - 對于
T:!Unpin
的固定數(shù)據(jù)坏瘩,您必須保持其不可變,即從固定到調(diào)用drop為止漠魏,其內(nèi)存都不會失效或重新利用倔矾。這是pin約束的重要組成部分。