安全と危険の相互作用

安全な Rust と危険な Rust とはどう関係しているのでしょうか? どのように影響し合うのでしょうか?

unsafe キーワードがインターフェースとなり、安全な Rust と危険な Rust とを分離します。 このため、安全な Rust は安全な言語で、危険な部分は完全に境界外に管理されている、と言うことができるのです。

unsafe は 2 つの目的に使われます。コンパイラがチェックできない契約が存在する事を宣言することと、 コードが契約に準拠していることがプログラマによってチェックされた事を宣言する事です。

関数trait の宣言 に未チェックな契約が存在する事を、unsafe を使って示すことができます。 関数に unsafe を使うと、ドキュメントを読んで、 要求された契約を守るように関数を使うことを、その関数のユーザーに要請することになります。 trait の宣言に unsafe を使うと、その trait を実装するユーザーに対し、ドキュメントをチェックして契約を守るよう要請します。

コードブロックに使われた unsafe は、そのブロックで呼ばれている危険な関数が要求する契約は守られていて、コードが信頼出来る事を意味します。unsafe を trait の実装に使うと、その実装が trait のドキュメントに書かれている契約に準拠している事を示します。

標準ライブラリにはいくつもの危険な関数があります。例えば、

  • slice::get_unchecked は未チェックのインデックス参照を実行します。自由自在にメモリ安全性に違反できます。
  • mem::transmute は、型安全の仕組みを好きなようにすり抜けて、ある値が特定の型であると再解釈します(詳細は 変換 をみてください)。
  • サイズが確定している型の生のポインタには、固有の offset メソッドがあります。渡されたオフセットが LLVM が定める "境界内" になければ、未定義の挙動を引き起こします。
  • すべての FFI 関数は unsafe です。なぜなら Rust コンパイラは、他の言語が実行するどんな操作もチェックできないからです。

Rust 1.0 現在、危険な traits は 2 つしかありません。

  • Send は API を持たないマーカー trait で、実装された型が他のスレッドに安全に送れる(move できる)ことを約束します。
  • Sync もマーカー trait で、この trait を実装した型は、共有リファレンスを使って安全に複数のスレッドで共有できる事を約束します。

また、多くの Rust 標準ライブラリは内部で危険な Rust を使っています。ただ、標準ライブラリの 実装はプログラマが徹底的にチェックしているので、危険な Rust の上に実装された安全な Rust は安全であると仮定して良いでしょう。

このように安全と危険の分けると、安全な Rust は、自分が利用する危険な Rust が正しく書かれている事、 つまり危険な Rust がそれが守るべき契約を実際に守っている事、を本質的に信頼しなくてはいけません。 逆に、危険な Rust は安全な Rust を注意して信頼しなくてはいけません。

例えば、Rust には PartialOrd trait と Ord trait があり、単に比較可能な型と全順序が 定義されている型(任意の値が同じ型の他の値と比べて等しいか、大きいか、小さい)とを区別します。 順序つきマップの BTreeMap は半順序の型には使えないので、キーとして使われる型が Ord trait を 実装している事を要求します。 しかし BTreeMap の実装は危険な Rust が使っていて、危険な Rust は渡された Ord の実装が 適切であるとは仮定できません。 BTreeMap 内部の危険な部分は、キー型の Ord の実装が全順序ではない場合でも、必要な契約が すべて守られるよう注意深く書かれなくてはいけません。

危険な Rust は安全な Rust を無意識には信頼できません。危険な Rust コードを書くときには、 安全な Rust の特定のコードのみに依存する必要があり、 安全な Rust が将来にわたって同様の安全性を提供すると仮定してはいけません。

この問題を解決するために unsafe な trait が存在します。理論上は、BTreeMap 型は キーが Ord ではなく、新しい trait UnsafeOrd を実装する事を要求する事ができます。 このようなコードになるでしょう。

use std::cmp::Ordering;

unsafe trait UnsafeOrd {
    fn cmp(&self, other: &Self) -> Ordering;
}

この場合、UnsafeOrd を実装する型は、この trait が期待する契約に準拠している事を示すために unsafe キーワードを使うことになります。 この状況では、BTreeMap 内部の危険な Rust は、キー型が UnsafeOrd を正しく実装していると 信用する事ができます。もしそうで無ければ、それは trait の実装の問題であり、 これは Rust の安全性の保証と一致しています。

trait に unsafe をつけるかどうかは API デザインにおける選択です。 Rust では従来 unsafe な trait を避けてきました。そうしないと危険な Rust が 蔓延してしまい、好ましくないからです。 SendSyncunsafe となっているのは、スレッドの安全性が 基本的な性質 であり、 間違った Ord の実装に対して危険なコードが防衛できるのと同様の意味では防衛できないからです。 あなたが宣言した trait を unsafe とマークするかどうかも、同じようにじっくりと考えてください。 もし unsafe なコードがその trait の間違った実装から防御することが合理的に不可能であるなら、 その trait を unsafe とするのは合理的な選択です。

余談ですが、unsafe な trait である SendSync は、それらを実装する事が安全だと 実証可能な場合には自動的に実装されます。 Send は、Send を実装した型だけから構成される型に対して、自動的に実装されます。 Sync は、Sync を実装した型だけから構成される型に対して、自動的に実装されます。

これが安全な Rust と危険な Rust のダンスです。 これは、安全な Rust をできるだけ快適に使えるように、しかし危険な Rust を書くには それ以上の努力と注意深さが要求されるようなデザインになっています。 この本の残りでは、どういう点に注意しなくはいけないのか、 危険な Rust を維持するための契約とは何なのかを議論します。