安全と危険の相互作用

安全な Rust とアンセーフな Rust とはどう関係しているのでしょうか? どのように影響し合うのでしょうか?

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

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

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

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

標準ライブラリにはいくつものアンセーフな関数があります。例えば、

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

Rust 1.0 現在、アンセーフなトレイトは 2 つしかありません。

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

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

このように分離する目的は、結局のところ、安全な Rust のたった一つの基本的な性質にあります。

どうやっても、安全な Rust では未定義な挙動を起こせない。

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

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

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

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


#![allow(unused)]
fn main() {
use std::cmp::Ordering;

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

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

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

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

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

関連キーワード:  実装, セーフ, unsafe, 契約, チェック, コード, 関数, Ord, Send, 要求