TL;DR
- unsafe コードは極力避ける。使ってよいのは、FFI や低レイヤの実装、必要な機能が標準ライブラリや crates.io にない場合に限られる。
- 必要な unsafe コードは、ほとんどの場合すでに誰かが書いてカプセル化している。
内容
Rust のメモリ安全性とそのコスト
Rust のメモリ安全性は、実行時のオーバーヘッドなしで保証される。その代わり、Rust を書くときは借用チェッカが通るコードを書き、使う参照型を正確に指定する必要がある。
Unsafe Rust とは
Unsafe Rust は Rust のスーパーセットで、いくつかの制約とそれによる安全の保証を緩める。ブロックの前に unsafe と書くと、そのブロックの中では通常の Rust で許されない操作ができる。たとえば、C のポインタのように振る舞う生ポインタを使える。生ポインタは借用ルールの対象外なので、参照外しの時点で有効なメモリを指していることを、プログラマが保証しなければならない。
なぜ避けるべきか
Rust に移行しても、Rust の中で C のようなコードを書くなら意味がない。ただし、低レベルなライブラリを書く場合や、他の言語のコードと連携する場合など、unsafe コードが必要になる場面もある(項目34)。
この項目のタイトルで大事なのは「書く」という部分である。必要な unsafe コードはほとんどの場合すでに誰かが書いているので、上のような場面を除けば、自分で書く必要はほぼない。
標準ライブラリとクレートの unsafe コード
標準ライブラリには多くの unsafe コードが含まれている。
Rc、RefCell、Arcなどのスマートポインタは、内部で生ポインタを使って、それぞれ固有の動作を提供するMutex、RwLockなどの同期プリミティブは、内部で OS 固有の unsafe なコードを使う- std::pin::Pin は、値がメモリ上で移動しないことを保証する。これにより、自分自身のアドレスを参照する自己参照データ構造を安全に扱える
std::sync::atomicは、プリミティブ型のアトミック版を提供する。複数のスレッドが同じ変数を操作してもデータ競合が起きない- std::mem の
take、swap、replaceなどは、借用チェッカの制約を避けながらメモリ上の値を操作する
これらを正しく使うにも注意は要るが、unsafe コードはカプセル化されて隠されているので、多くの問題は起きないようになっている。crates.io にも、once_cell(一度だけ初期化する値)、rand(乱数生成)、byteorder(バイト列と数値の変換)、cxx(C++ との相互運用)のように、unsafe コードをカプセル化したクレートがある。
unsafe が必要になったら
FFI で他の言語のコードと連携する場合など、unsafe を使わざるを得ないこともある。その場合でも、unsafe コードをラップする層を作って、unsafe な操作をその中に閉じ込めるのがよい。問題の起きる場所を局所化できる。
unsafe コードを書くときは、次の点に気を付ける。
- 安全性に関するコメントを書き、unsafe コードが依存する前提条件と不変条件を記す。Clippy はこれに関する警告を出せる(項目29)
- unsafe ブロックを最小にして、失敗したときの影響範囲を限る。lint の unsafe_op_in_unsafe_fn を有効にすると、unsafe 関数の中でも unsafe な操作には明示的な unsafe ブロックが必要になる
- 普段以上にテストを書く(項目30)
- Miri で診断する。Miri はコンパイラが出力する中間表現を解釈するので、コンパイラが検出できないさまざまなエラーを見つけられる
- 共有状態があるときは特に、マルチスレッドでの使い方を慎重に考える(項目17)