0%

Self-Reference Type process between C++ and Rust

為了搞懂Rust Pin在做什麼,耗費了很多精力,還真是有夠難懂的

About Self-Reference Type

有個資料結構, 其中有個指標指向結構自己或是結構中的某個欄位
例如

1
2
3
4
5
6
7
8
9
struct Test {
protected:
std::string a_;
const std::string* b_;
public:
Test(std::string text) : a_(std::move(text)), b_(&a_) {}
const std::string& a() const { return a_; }
const std::string& b() const { return *b_; }
};

這裡的b_指向a_的地址, 同樣的事情在Rust寫成這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)}
}
}

不看Rust的safe機制造成的不同,原理是相同的
現在的問題是,假設物件被移動了,指向結構中某部分的指標該怎麼辦
例如

1
std::swap(test1, test2);

先從我比較熟悉的C++來說好了

Solution1: Keep invariant

雖然達成目標的方法有很多,不過原則都是一樣:維持不變量就好了

1
2
3
4
void swap(Test& lhs, Test& rhs) {
std::swap(lhs.a_, rhs.a_);
}
swap(test1, test2);

很顯然,這個方法不適用於Rust

Solution2: Don’t move the object

所謂的Pin也就是這麼一回事,當物件停留在記憶體的某個位置之後,就不會再移動了,所以Self-Reference Type的物件,在生命週期結束之前,所有的pointer和reference都會有效
在C++禁止的方法也不只一種,這是方法之一

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void swap(T&, T&) {}
struct Test {
protected:
std::string a_;
const std::string* b_;
public:
Test(std::string text) : a_(std::move(text)), b_(&a_) {}
friend void swap(Test&, Test&) = delete;
const std::string& a() const { return a_; }
const std::string& b() const { return *b_; }
};

不過由於Rust講究Safety,所以訂了一堆規則

About Pin in Rust

在Rust中對Self-Reference Type的處理,我們要禁止的只有這件事

1
2
3
4
5
6
7
pub fn swap<T>(x: &mut T, y: &mut T) {
// SAFETY: the raw pointers have been created from safe mutable references satisfying all the
// constraints on `ptr::swap_nonoverlapping_one`
unsafe {
ptr::swap_nonoverlapping_one(x, y);
}
}

禁止Rust拿到&mut T的Reference,&mut Tˊ自然是不行,Box<T>也做不到這件事,所以就是Pin<T>登場的時候

Rust的Type分成兩類:

  • Default Type:可以安全在Rust Move的類型
    • Default Type都實作了auto Unpin trait,也就是什麼都不用做
  • Self-Reference Type:也就是上面提到的部分
    • 必須實作!Unpin的部分
    • 使用PhantomPinned就可以了

以下是個範例程式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct Test {
_marker: PhantomPinned,
}

impl Test {
fn new() -> Self {
Test {
_marker: PhantomPinned
}
}
}
pub fn main() {
let mut test1 = Box::pin(Test::new());
let mut test2 = Box::pin(Test::new());
// compile failed
std::mem::swap(test1.as_mut().get_mut(), test2.as_mut().get_mut());
}

你把上面的PhantomPinned註解掉,程式就能正常運作了
Pin還有很多細節,等我真的變成全職Rust工程師在研究吧

Reference