June 18, 2020
English | 日本語
Rust Pin trait (日本語)¶
Rust の Pin trait はマニュアルが分かりにくいと思ったので少し書いてみる。
結論から言うと以下のようになる。
普通は object を move しても問題ないが、例外もある。例えば自己参照を持つ場合などだ。
Rust の型システムでは move を禁止することは出来ない。
Rust には move を防ぐためのパターンが 2 個ある。どちらのパターンでもポインターの様な変数 (参照など) を使う。
std::mem::swap() など一部の関数は文法としての move を使うことは無いが同じ効果を持つ。
ポインターの様な変数を Pin 型でラップすれば、文法としての move だけではなく上記のような関数に関するバグもコンパイラーで検知できるようになる。
自己参照型の例¶
最初になぜ自己参照型のオブジェクトを move してはいけないのか確認しておく。
struct SelfRef {
x: i32,
ptr_x: *const i32, // x を指すポインター
}
impl SelfRef {
/// コンストラクターだが、問題がある
pub fn new(x: i32) -> Self {
let mut this = Self {
x,
ptr_x: std::ptr::null(),
};
this.ptr_x = &this.x;
// このチェックは問題なく通る
this.check();
// 戻り値を返す時に move している
// ここで問題が発生
this
}
/// ptr_x が変数 x を指しているか確認する
pub fn check(&self) {
assert!(&self.x as *const i32 == self.ptr_x);
}
}
fn main() {
let obj = SelfRef::new(5);
// ここで assertion error が発生する。
// obj 自身は move されているが obj.ptr_x は
// move 前に obj.x があった場所を指し続けているため
obj.check();
}
SelfRef::ptr_x は自身の変数 x を指しているはず。つまり自己参照型だ。 ここが問題となる。
最後の obj.check() はエラーとなってしまう。 なぜなら obj は初期化後に move されているから。 当然 obj.x も move と同時に移動するが、obj.ptr_x は依然として obj.x が初期化された時にあった場所を指し続ける。
どうやって move を防ぐか?¶
SelfRef を move すると、どうやっても問題が発生する。
そのため「move はしない」という前提の元で「うっかり move してしまう事によるバグをどうやって回避するか?」にフォーカスする事になる。
これには以下の 2 種類の方法がある。
オブジェクトを heap に作ることで move を回避する。
オブジェクトを stack に作り、もし move したらコンパイラーがバグを見つけられるようにする。
オブジェクトを heap に作る¶
この方法はコンストラクターの戻り値を変えるだけでうまく行く
... これ以前は前例と同じなので省略
impl SelfRef {
/// コンストラクターだが、戻り値の型が
/// SelfRef ではなく Box<SelfRef> になっている
pub fn new(x: i32) -> Box<Self> {
let mut this = Box::new(SelfRef {
x,
ptr_x: std::ptr::null(),
});
this.ptr_x = &this.x;
this.check();
// 返すのは「オブジェクトへのポインター」であり
// オブジェクトの場所は変わらない (move されない)
this
}
... 以降は前例と同じなので省略
コンストラクターは heap 上にメモリーを確保し、そこにオブジェクトを作る。 戻り値はオブジェクトへのポインターなので、オブジェクト自身の移動は発生しない。
この方法は上手く行くが、heap メモリーを確保するのでパフォーマンスはあまり良くない。
コンパイラーにバグを検知できるようにする¶
2 番目の方法はオブジェクトを stack 上に構築し、すぐにその変数を隠してしまうものだ。
変数が無いので move する事もできなくなる。
具体的には次のようにする。 (実際にはコンパイラーがバグを検知するので、コンパイルできない。)
... これ以前は前例と同じなので省略
impl SelfRef {
/// コンパイルエラーを起こす
pub fn new(x: i32) -> Self {
let mut this = Self {
x,
ptr_x: std::ptr::null(),
};
// 変数 this を上書きすることで、元の変数を隠す。
// しかし、オブジェクト自身は stack に残り続ける。
//
// 新しい "this" はオブジェクトへの参照なので
// オブジェクトへのアクセスは可能。
let this = &mut this;
this.ptr_x = &this.x;
this.check();
// 関数の戻り値は SelfRef だが
// "this" の型は &mut SelfRef である。
// そのため、コンパイルできない
this
}
... 以降は前例と同じなので省略
このコンストラクターの戻り値は SelfRef 型だが、そのような型の変数は無い。("this" の型は &mut SelfRef。)
もちろん、SelfRef に Copy trait を実装して最後に this ではなく *this を返せばコンパイルは通る。 しかし、一般的に move できない型は copy もするべきでは無い。 この方法はそれ以前の問題だ。
Pin trait¶
上で紹介した 2 種類の方法は move される事を防いでくれる。 しかし std::mem::swap() のような関数は move はしていないが実際には同じ結果をもたらす。 この場合、依然としてバグは発生してしまう。
下記の例は 2 番目 (コンストラクターが Box<SelfRef> を返す例) とほとんど同じだが、main() の中で std::mem::swap() を呼んでいる。
struct SelfRef {
x: i32,
ptr_x: *const i32, // x を指すポインター
}
impl SelfRef {
/// コンストラクター
pub fn new(x: i32) -> Box<Self> {
let mut this = Box::new(Self {
x,
ptr_x: std::ptr::null(),
});
this.ptr_x = &this.x;
this.check();
this
}
pub fn check(&self) {
assert!(&self.x as *const i32 == self.ptr_x);
}
}
fn main() {
// obj1 の構築。
let mut obj1 = SelfRef::new(1);
obj1.check();
// obj2 の構築。
let mut obj2 = SelfRef::new(2);
obj2.check();
// obj1 と obj2 をスワップする
// 文法上は move していないが、move と同じ効果を持つ
std::mem::swap(&mut *obj1, &mut *obj2);
// 実質的に move しているのでエラー
obj1.check();
}
最後の obj.check() は直前の std::mem::swap() が move と同じ働きをするのでエラーとなる。
こんな時は Pin trait の出番だ。
main() 関数を以下のように変更してみる
... これ以前は前例と同じなので省略
// Pin trait を使用
// (ただし、これだけでは効果が無い)
fn main() {
// std::pin::Pin::new_unchecked() が unsafe なので
// unsafe {} でくくっている。
unsafe {
// obj1 と obj2 の構築
let obj1 = SelfRef::new(1);
obj1.check();
let obj2 = SelfRef::new(2);
obj2.check();
// obj1 と obj2 を Pin でラップする
let mut obj1 = std::pin::Pin::new_unchecked(obj1);
let mut obj2 = std::pin::Pin::new_unchecked(obj2);
std::mem::swap(&mut *obj1, &mut *obj2);
// 相変わらずエラー発生
obj1.check();
}
}
最後の obj1.check() では相変わらずエラーとなる。
実は Pin trait は Unpin trait を実装している型のポインターをラップしても何もしないのだ。
SelfRef は Unpin を実装しているのだろうか?
実際には実装している。
Rust のコンパイラーは全てのプロパティーが Unpin である型に対して自動的に Unpin を実装する。 実際に move を禁止する型など少ないし、極めて自然だ。
そのため、今回は明示的に Unpin の実装を止める必要がある。
std::marker::PhantomPinned は Unpin を実装していない phantom data だ。 (Phantom data とは Rust ではサイズが 0 byte の型.)
これを SelfRef に加えてみる (main 関数には変更は無い)
struct SelfRef {
x: i32,
ptr_x: *const i32, // x を指すポインター
_phantom: std::marker::PhantomPinned,
}
impl SelfRef {
pub fn new(x: i32) -> Box<Self> {
let mut this = Box::new(Self {
x,
ptr_x: std::ptr::null(),
_phantom: std::marker::PhantomPinned,
});
this.ptr_x = &this.x;
this.check();
this
}
pub fn check(&self) {
assert!(&self.x as *const i32 == self.ptr_x);
}
}
fn main() {
unsafe {
// obj1 と obj2 の構築
let obj1 = SelfRef::new(1);
obj1.check();
let obj2 = SelfRef::new(2);
obj2.check();
// obj1 と obj2 を Pin でラップする
// これにより、obj1 や obj2 の可変なポインターを
// 取得できなくなる
let mut obj1 = std::pin::Pin::new_unchecked(obj1);
let mut obj2 = std::pin::Pin::new_unchecked(obj2);
// `&mut *obj1` の取得が出来ないので
// コンパイルエラー
std::mem::swap(&mut *obj1, &mut *obj2);
obj1.check();
}
}
これで問題が解決。 コンパイラーがバグを検知できるようになった。
結論¶
C++ 等と異なり Rust では move 不可能な型を定義できない。
代わりに Rust には 2 種類のビルドパターンが存在する。
heap 上にメモリを確保しオブジェクトをそこで構築する。このオブジェクトは move されなくなる。
stack 上にオブジェクトを構築し、その変数をオブジェクトへの参照で上書きする。オブジェクトを表す変数が無いので move しようとすると型エラーとなりコンパイラーがバグを検知できるようになる。
いずれの方法でもプログラマーはポインターのような変数を通じてオブジェクトへアクセスする。 このポインターのような変数を Pin でラップすると、std::mem::swap() のような move と同じ働きをする関数もコンパイルエラーで検知できるようになる。
ただ、この仕組みって本当に良いか? まあ色々と理由はあるんだろうけど、複雑すぎないだろうか? 例えば Unpin trait とか、本当に必要だったんだろうか?
何よりオブジェクトを構築時にこのパターンを使う事を忘れたら、結局バグを防ぐ事はできないじゃないか。 (マクロとかで対応できるかもしれないけど、もっと複雑になる。)
好きになれないなぁ。