Drop Checkerの規則をちゃんと理解する
概要: Drop CheckerはRustのボローチェッカに付属する追加の検査器で、コンパイラが自動で挿入するdrop処理の安全性を保証するためのものである。これの詳細な規則とその正当性を理解したかったので自分なりに整理した。
dropckの概要についてはRustonomiconの説明とか以前の記事とかを参照。
注意: 本記事の内容はコンパイラの実装との対応をとっていないので実際の実装とは異なる可能性がある。
規格
用語
mem::drop_in_place()
の実装は型ごとにコンパイラが生成するが、大雑把に言うと以下のようになる:
struct Foo { bar: Bar, baz: Baz, } fn drop_in_place<T>(foo: &mut T) where T == Foo { // Drop::drop() を呼ぶ (存在すれば) <Foo as Drop>::drop(foo); // 各メンバについて再帰的に呼ぶ drop_in_place(&mut foo.bar); drop_in_place(&mut foo.baz); }
問題
mem::drop_in_place()
の呼び出しを挿入するとライフタイム条件が壊れる
{ +------ let x = ..; | +---- : | | 'in : | +---- : | 'out << mem::drop_in_place(&mut x); >> // コンパイラが挿入するdrop +------ }
このとき、
- xの型自体に
'in
が全く使えないのは困る。これは相互参照するデータを作るために必要。 - しかし、
drop_in_place
の途中で'in
は使えなくなるから、drop_in_place
が'in
に関してフルアクセスできる保証はない
解決方法
mem::drop_in_place
の中では、 x
に関係するライフタイム全てが有効であるとは限らないことにする。例えば、 x: Foo<'a>
を使う関数は、通常 'a
がまだ有効であることを仮定してよいが、 mem::drop_in_place
に関してはこれを仮定できない場合がある。そのかわり、以下のルールに従って、一部のライフタイムに関してだけは、その有効性を仮定してよい。
コンパイラは、それらのライフタイムが上記の 'out
まで生存すること (つまり 'in
よりも真に長く生存すること) をチェックする。
drop_lifetimes
収集の流れ
mem::drop_in_place<T>
で有効性を仮定してよいライフタイムの一覧を drop_lifetimes(T)
と呼ぶことにする。 mem::drop_in_place<T>
は以下の手順で定義される。
Drop::drop()
が触れうるライフタイムの列挙
例として、 Foo<'a, 'b, T, U>
という型を考える。この Drop::drop()
が触れうるライフタイムの一覧を、以下のように保守的に列挙する。
Foo
が Drop
を実装していない場合
Drop
で何もしないことがはっきりわかるので、どのライフタイムも必要ではない。
user_drop_lifetimes(Foo<'a, 'b, T, U>) = {}
Foo
が Drop
を実装している場合
保守的に、全てのパラメーターのライフタイムを仮定する。
user_drop_lifetimes(Foo<'a, 'b, T, U>) = {'a, 'b} ∪ {Tの全てのライフタイム} ∪ {Uの全てのライフタイム}
Foo
の Drop
実装が #[may_dangle]
を持っている場合
unsafe
と #[may_dangle]
を組み合わせることで、ユーザーの責任で「このライフタイムしか使わない」と宣言することができる。例えば、
unsafe impl<'a, #[may_dangle] 'b, T, #[may_dangle] U> Drop for Foo<'a, 'b, T, U> { .. }
の場合、
user_drop_lifetimes(Foo<'a, 'b, T, U>) = {'a} ∪ {Tの全てのライフタイム}
となる。
mem::drop_in_place()
の再帰的呼び出し構造
この再帰呼び出し構造のことを、単に「型T
が型U
を所有する」ともいう。これは、「mem::drop_in_place<T>()
の中で mem::drop_in_place<U>()
が呼び出されうる」という関係を近似したものである。例えば、 struct
や enum
の drop_in_place
内では、全てのメンバの drop_in_place
が呼び出されうる。
実際にコンパイラで検査をするさいは、循環的な所有関係があることに注意する必要がある。
この規則はほぼ上の説明通りだが、以下の2点に注意が必要である。
PhantomData<T>
はT
を所有する。[T; 0]
はT
を所有する。
user_drop_lifetimes
を再帰的に収集する
結局、 drop_lifetimes
は以下のように定義される。
drop_lifetimes(T) = { 'a | T は U を所有し、 'a ∈ user_drop_lifetimes(U) である }
最終的な規約
<T as Drop>::drop()
内では、drop_lifetimes(T)
(user_drop_lifetimes(T)
ではないことに注意) の有効性のみを仮定できる。#[may_dangle]
を使わない限りは、自動的に保証される。
- 上記の規約により、
mem::drop_in_place<T>()
も、drop_lifetimes(T)
の有効性のみを仮定すれば安全であることが保証される。 - 逆に、利用する側では、以下の条件がチェックされる: 型
T
の値がdropされるとき、drop_lifetimes(T)
のライフタイムはその値よりも真に長く生存する必要がある。
最後に Rc
で復習
Rc
は以下のように定義されている。
struct Rc<T: ?Sized> { ポインタ, _marker: PhantomData<T>, } unsafe impl<#[may_dangle] T: ?Sized> Drop for Rc<T> { fn drop(&mut self) { ... mem::drop_in_place::<T>(ポインタ); ... } }
ポイントとして、 Rc
の Drop
実装は T
に #[may_dangle]
がついている。これは mem::drop_in_place::<T>
を呼んでいるから、一見おかしいように見える。しかし実は問題ない。確かに、 #[may_dangle]
の効果により
user_drop_lifetimes(Rc<T>) = {}
となるが、 PhantomData<T>
があるので
drop_lifetimes(Rc<T>) = drop_lifetimes(T)
となる。
上の規約で
<T as Drop>::drop()
内では、drop_lifetimes(T)
の有効性のみを仮定できる。
を
<T as Drop>::drop()
内では、user_drop_lifetimes(T)
の有効性のみを仮定できる。
と書かなかったのはそのためである。
なお、 Rc
で PhantomData<T>
と #[may_dangle]
をやめて
struct Rc<T: ?Sized> { ポインタ, } impl<T: ?Sized> Drop for Rc<T> { fn drop(&mut self) { ... mem::drop_in_place::<T>(ポインタ); ... } }
としても安全だと思われる。しかし、この場合は
drop_lifetimes(Rc<T>) = drop_lifetimes(T)
ではなく
drop_lifetimes(Rc<T>) = lifetimes(T)
(T
の全てのライフタイム)
となってしまう。つまり、この状態だとdropckはより厳しい挙動をしてしまう。つまり、この2つの違いは、
T
のdrop_in_place
を呼ぶだけなら、PhantomData<T>
を使って#[may_dangle] T
とすることで、drop_lifetimes(T)
の保証のみを得ることができる。T
に対してより一般的な処理をするなら、何もしないことでlifetimes(T)
の保証を得ることができる。 (そのぶんdropckが厳しくなる)
といえる。
RFC1238時代はどう違ったか
RFC1238とRFC1327の違いは、単に #[may_dangle]
の粒度の違いだけである。
例えば、RFC1238では
impl<'a, 'b, T, U> Drop for Foo<'a, 'b, T, U> { #[unsafe_destructor_blind_to_params] fn drop(&mut self) { .. } }
と書くと、今でいう
unsafe impl<#[may_dangle] 'a, #[may_dangle] 'b, #[may_dangle] T, #[may_dangle] U> Drop for Foo<'a, 'b, T, U> { .. }
と同じ意味になる。このように #[may_dangle]
を一括で適用するしかないので、
unsafe impl<'a, #[may_dangle] 'b, T, #[may_dangle] U> Drop for Foo<'a, 'b, T, U> { .. }
と同等の記述はできない。
RFC0769時代はどう違ったか
RFC0769の時点ではパラメトリシティを用いて、より積極的に安全性を推論していた。パラメトリシティとは、例えば
fn foo<T: Sync>(x: T) { .. }
のような関数があったときに、 foo
が x
に対してできることは限られている (具体的には drop
するか forget
するくらいしかない) という仮定である。
この仮定が成立する理由は、 foo
が x
に関して、 Sized + Sync
であるという限定的な情報しか持っていないからである。 x: T: Sync
だとわかっても、 Sync
は何もメソッドを持たないので、特にできることがないということになる。
しかし、現在はこのパラメトリシティという仮定は崩れている。特殊化を用いると、
fn foo<T: Sync>(x: T) { // 何もしない } fn foo<T: Sync + Foo>(x: T) { // TがFooを実装していたときは、何かをする x.foo(); }
のように、 T
があるトレイトを実装しているときだけ実装を切り替えるということができる。(実際にはトレイト実装か固有実装を経由しないと特殊化できないので、もう少しコードを書く必要がある)
RFC0769時点では、このパラメトリシティを用いて「マーカートレイトのみを持つ型パラメーター」には自動的に #[may_dangle]
が仮定される仕様だった。
RFC0769より前はどうだったか
RFC0769より前は、drop checkerはなかった。つまり現在でいうところの user_drop_lifetimes(T) = {}
が仮定されていた。
これを保証するため、ジェネリクスパラメーターを持つ Drop
は全てunsafeで、 #[unsafe_destructor]
が必要だった。これがRFC0769の "Sound Generic Drop" という名称の由来である。
まとめ
Drop Checkerの規則について、 #[may_dangle]
の意味を含めてまとめてみた。Drop
で変な処理をするunsafeなコードを書くときはこの点を理解しておくとunsoundnessを踏み抜きにくくなると思われる。