Unsafe Rust
これまでに説明してきたコードはすべて、Rust のメモリ安全性保証がコンパイル時に強制されるものでした。しかし、Rust には、こうしたメモリ安全性保証を強制しない、内部に隠れたもうひとつの言語があります。それが unsafe Rust で、通常の Rust と同じように動作しますが、追加のスーパーパワーを与えてくれます。
unsafe Rust が存在するのは、静的解析が本質的に保守的だからです。コンパイラがコードが保証を満たしているかどうかを判断しようとするとき、無効なプログラムをいくつか受け入れてしまうよりも、有効なプログラムをいくつか拒否してしまうほうが望ましいのです。コードは 問題ないかもしれません が、Rust コンパイラが確信を持つための十分な情報を持っていない場合、そのコードは拒否されます。このような場合には、unsafe コードを使ってコンパイラに「信じてください、自分が何をしているかは分かっています」と伝えられます。ただし、unsafe Rust の使用は自己責任であることに注意してください。unsafe コードを誤って使うと、null ポインタの逆参照のような、メモリ安全性の欠如による問題が発生する可能性があります。
Rust に unsafe というもうひとつの顔があるもうひとつの理由は、その土台となるコンピュータハードウェア自体が本質的に unsafe だからです。Rust が unsafe な操作を許可しなければ、特定の作業を行えません。Rust では、オペレーティングシステムと直接やり取りしたり、独自のオペレーティングシステムを書いたりするといった、低レベルのシステムプログラミングを可能にする必要があります。低レベルのシステムプログラミングを扱えることは、この言語の目標の 1 つです。unsafe Rust で何ができるのか、そしてそれをどう行うのかを見ていきましょう。
unsafeなスーパーパワーを使う
unsafe Rust に切り替えるには、unsafe キーワードを使い、その後に unsafe コードを含む新しいブロックを始めます。unsafe Rust では、safe Rust ではできない 5 つの操作が可能で、これらを unsafeなスーパーパワー と呼びます。これらのスーパーパワーには、次のことを行う能力が含まれます。
- 生ポインタを逆参照する。
- unsafe 関数またはメソッドを呼び出す。
- 可変な静的変数にアクセスまたは変更する。
- unsafe トレイトを実装する。
unionのフィールドにアクセスする。
理解しておくべき重要な点は、unsafe は借用チェッカを無効にしたり、Rust のほかの安全性チェックを無効にしたりするものではないということです。unsafe コードの中で参照を使えば、それは依然として検査されます。unsafe キーワードが与えるのは、コンパイラがメモリ安全性について検査しない、これら 5 つの機能へのアクセスだけです。unsafe ブロックの内部でも、ある程度の安全性は引き続き得られます。
さらに、unsafe は、そのブロック内のコードが必ずしも危険であるとか、確実にメモリ安全性の問題を抱えるという意味でもありません。意図としては、プログラマであるあなたが、unsafe ブロック内のコードが有効な方法でメモリにアクセスすることを保証する、ということです。
人は誤りを犯すものであり、ミスは起こります。しかし、これら 5 つの unsafe な操作を unsafe と注釈されたブロック内に置くことを要求することで、メモリ安全性に関するエラーは unsafe ブロック内にあるはずだと分かります。unsafe ブロックは小さく保ちましょう。後でメモリバグを調査するときに、そのありがたみが分かります。
unsafe コードをできるだけ隔離するためには、そのようなコードを安全な抽象化の中に閉じ込め、安全な API を提供するのが最善です。これについては、この章の後半で unsafe 関数とメソッドを調べるときに説明します。標準ライブラリの一部は、監査済みの unsafe コードの上に構築された安全な抽象化として実装されています。unsafe コードを安全な抽象化で包むと、あなたやその利用者が unsafe コードで実装された機能を使いたいあらゆる場所へ unsafe の使用が漏れ出すのを防げます。なぜなら、安全な抽象化を使うこと自体は安全だからです。
5 つの unsafe なスーパーパワーを順に見ていきましょう。また、unsafe コードに安全なインターフェースを提供するいくつかの抽象化も見ていきます。
生ポインタを逆参照する
第 4 章の [「ダングリング参照」][dangling-references] 節で、参照は常に有効であることをコンパイラが保証すると述べました。unsafe Rust には、参照に似た 生ポインタ と呼ばれる 2 つの新しい型があります。参照と同様に、生ポインタは不変にも可変にもでき、それぞれ *const T と *mut T と表記します。アスタリスクは逆参照演算子ではなく、型名の一部です。生ポインタの文脈では、不変 は、ポインタを逆参照してもその参照先に直接代入できないことを意味します。
参照やスマートポインタとは異なり、生ポインタは次の性質を持ちます。
- 借用規則を無視できるため、同じ場所に対して不変ポインタと 可変ポインタの両方、または複数の可変ポインタを持てる
- 有効なメモリを指していることが保証されない
- null であってもよい
- 自動的なクリーンアップを一切実装しない
Rust にこれらの保証を強制させないことを選ぶことで、保証された安全性を手放す代わりに、より高いパフォーマンスや、Rust の保証が適用されない他言語やハードウェアとのインターフェース能力を得られます。
リスト 20-1 は、不変の生ポインタと可変の生ポインタを作成する方法を示しています。
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
このコードには unsafe キーワードが含まれていないことに注目してください。生ポインタは safe コード内でも作成できます。ただし、少し後で見るように、unsafe ブロックの外で生ポインタを逆参照することはできません。
生借用演算子を使って生ポインタを作成しました。&raw const num
は *const i32 の不変な生ポインタを作成し、&raw mut num は *mut i32 の可変な生ポインタを作成します。これらはローカル
変数から直接作成したので、この特定の生ポインタが有効であることは分かります。しかし、任意の生ポインタについてそのようには仮定できません。
これを示すために次は、raw borrow 演算子を使う代わりに、キーワード as を使って値をキャストすることで、有効性をそれほど確信できない生ポインタを作成します。リスト 20-2 は、メモリ上の任意の位置への生ポインタを作成する方法を示しています。任意のメモリを使おうとすることは未定義動作です。そのアドレスにデータがあるかもしれませんし、ないかもしれません。コンパイラがコードを最適化して、メモリアクセスがまったく行われないこともありますし、プログラムがセグメンテーションフォールトで終了することもあります。通常、このようなコードを書く正当な理由はありません。特に、代わりに生借用演算子を使える場合はなおさらです。しかし、そうすること自体は可能です。
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
生ポインタは safe コード内で作成できますが、生ポインタを逆参照して、その指し示すデータを読み取ることはできないことを思い出してください。リスト 20-3 では、生ポインタに対して逆参照演算子 * を使用しており、そのために unsafe ブロックが必要になります。
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
ポインタを作成すること自体に害はありません。問題になり得るのは、そのポインタが指している値にアクセスしようとしたときであり、その際に無効な値を扱うことになる可能性があります。
また、リスト20-1および20-3では、どちらも num が格納されている同じメモリ位置を指す *const i32 と *mut i32 の生ポインタを作成したことにも注目してください。もし代わりに、num への不変参照と可変参照を作成しようとしていたら、そのコードはコンパイルされなかったでしょう。なぜなら、Rust の所有権ルールでは、不変参照が存在しているのと同時に可変参照を許可していないからです。生ポインタであれば、同じ位置への可変ポインタと不変ポインタを作成し、可変ポインタを通じてデータを変更できます。その結果、データ競合が発生する可能性があります。注意してください!
このような危険があるにもかかわらず、なぜ生ポインタを使うのでしょうか。大きなユースケースの 1 つは、次の節で見るように C コードとやり取りするときです。もう 1 つのケースは、借用チェッカーでは理解できない安全な抽象化を構築するときです。これから unsafe 関数を導入し、その後で unsafe コードを使う安全な抽象化の例を見ていきます。
unsafe 関数またはメソッドを呼び出す
unsafe ブロック内で実行できる 2 つ目の種類の操作は、unsafe 関数を呼び出すことです。unsafe 関数やメソッドは、通常の関数やメソッドとまったく同じ見た目ですが、定義の先頭に追加で unsafe が付きます。この文脈における unsafe キーワードは、その関数を呼び出す際に私たちが守らなければならない要件があることを示しています。Rust は、それらの要件が満たされていることを保証できないからです。unsafe 関数を unsafe ブロックの中で呼び出すことで、私たちはその関数のドキュメントを読み、その関数の契約を守る責任を引き受けることを表明しています。
以下は、本体では何もしない dangerous という名前の unsafe 関数です。
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
dangerous 関数は、別個の unsafe ブロックの中で呼び出さなければなりません。unsafe ブロックなしで dangerous を呼び出そうとすると、エラーになります。
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
unsafe ブロックを使うことで、私たちは Rust に対して、その関数のドキュメントを読み、適切な使い方を理解し、その関数の契約を満たしていることを確認したと表明しています。
unsafe 関数の本体の中で unsafe な操作を実行するには、通常の関数内と同様に、やはり unsafe ブロックを使う必要があります。忘れるとコンパイラが警告します。これにより、unsafe な操作が関数本体全体にわたって必要とは限らないため、unsafe ブロックをできるだけ小さく保つことができます。
unsafe コードに対する安全な抽象化を作る
関数に unsafe コードが含まれているからといって、その関数全体を unsafe としてマークする必要があるわけではありません。実際、unsafe コードを安全な関数で包むことは一般的な抽象化です。例として、標準ライブラリの split_at_mut 関数を見てみましょう。この関数には一部 unsafe コードが必要です。これをどのように実装できるかを探っていきます。この安全なメソッドは可変スライスに対して定義されています。1 つのスライスを受け取り、引数として与えられたインデックスでそのスライスを分割して 2 つにします。リスト20-4は split_at_mut の使い方を示しています。
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
この関数は、安全な Rust だけでは実装できません。試みると、リスト20-5のようなものになるでしょうが、これはコンパイルされません。簡単のため、ここでは split_at_mut をメソッドではなく関数として実装し、またジェネリック型 T ではなく i32 型のスライスに対してのみ実装します。
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
この関数はまずスライスの全長を取得します。次に、パラメータとして与えられたインデックスがスライス内にあることを、長さ以下かどうかを確認することでアサートします。このアサーションは、スライスを分割するインデックスとして長さより大きい値を渡した場合、そのインデックスを使おうとする前に関数が panic することを意味します。
次に、タプルの中に 2 つの可変スライスを返します。1 つは元のスライスの先頭から mid インデックスまで、もう 1 つは mid からスライスの末尾までです。
リスト20-5のコードをコンパイルしようとすると、次のようなエラーになります。
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Rust の借用チェッカーは、私たちがスライスの異なる部分を借用していることを理解できません。借用チェッカーが知っているのは、同じスライスから 2 回借用しているということだけです。スライスの異なる部分を借用すること自体は、本質的には問題ありません。2 つのスライスは重なっていないからです。しかし、Rust はそれを理解できるほど賢くありません。コードが正しいと私たちには分かっているのに、Rust には分からない場合、それは unsafe コードの出番です。
リスト20-6は、unsafe ブロック、生ポインタ、そしていくつかの unsafe 関数の呼び出しを使って、split_at_mut の実装を機能させる方法を示しています。
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
第4章の [「スライス型」][the-slice-type] 節で説明したように、スライスは何らかのデータへのポインタと、そのスライスの長さから成ります。len メソッドを使ってスライスの長さを取得し、as_mut_ptr
メソッドを使ってスライスの生ポインタにアクセスします。この場合、i32 値への可変スライスを持っているので、as_mut_ptr は型 *mut i32 の生ポインタを返し、これを変数 ptr に格納しています。
mid インデックスがスライス内にあることを確認するアサーションはそのまま維持します。次に unsafe コードに入ります。slice::from_raw_parts_mut 関数は、生ポインタと長さを受け取り、スライスを作成します。この関数を使って、ptr から始まり長さが mid 要素のスライスを作成します。次に、ptr に対して mid を引数として add メソッドを呼び出し、mid 位置から始まる生ポインタを取得し、そのポインタと mid 以降に残っている要素数を長さとして使ってスライスを作成します。
slice::from_raw_parts_mut 関数が unsafe なのは、生ポインタを受け取り、そのポインタが有効であることを信頼しなければならないからです。生ポインタに対する add メソッドも、オフセット先の位置がやはり有効なポインタであることを信頼しなければならないため unsafe です。そのため、これらを呼び出せるように、slice::from_raw_parts_mut と add の呼び出しを unsafe ブロックで囲む必要がありました。コードを見て、さらに mid が len 以下でなければならないというアサーションを追加することで、unsafe ブロック内で使われるすべての生ポインタが、スライス内のデータを指す有効なポインタであると判断できます。これは unsafe の許容できる適切な使い方です。
結果として得られる split_at_mut 関数を unsafe としてマークする必要は
なく、この関数は安全な Rust から呼び出せます。私たちは、この関数が使える
データから有効なポインタだけを生成するような、安全な方法で unsafe
コードを用いて関数を実装することで、unsafe なコードに対する安全な
抽象化を作り出しています。
対照的に、リスト 20-7 にある slice::from_raw_parts_mut の使用は、
そのスライスが使われたときにおそらくクラッシュするでしょう。このコードは
任意のメモリ位置を受け取り、長さ 10,000 のスライスを作成します。
fn main() {
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
私たちはこの任意の位置にあるメモリを所有しておらず、このコードが作成
するスライスに有効な i32 値が含まれている保証もありません。values
を有効なスライスであるかのように使おうとすると、未定義動作になります。
extern 関数を使って外部コードを呼び出す
Rust のコードが、別の言語で書かれたコードとやり取りする必要が生じることも
あります。このために、Rust には extern キーワードがあり、これにより
外部関数インターフェイス (FFI) を作成して利用しやすくなっています。FFI
は、あるプログラミング言語が関数を定義し、別の(外部の)
プログラミング言語がそれらの関数を呼び出せるようにする仕組みです。
リスト 20-8 では、C 標準ライブラリの abs 関数との統合をセットアップする
方法を示します。extern ブロック内で宣言された関数は、一般に Rust
コードから呼び出すには安全ではないため、extern ブロックにも unsafe
を付けなければなりません。その理由は、他の言語は Rust の規則や保証を
強制せず、Rust もそれらを検査できないので、安全性を確保する責任が
プログラマに委ねられるからです。
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
unsafe extern "C" ブロックの中では、呼び出したい別言語の外部関数の名前
とシグネチャを列挙します。"C" の部分は、その外部関数がどの
アプリケーションバイナリインターフェイス (ABI) を使うかを定義します:
ABI は、アセンブリレベルで関数をどのように呼び出すかを定義します。"C"
ABI はもっとも一般的で、C プログラミング言語の ABI に従います。Rust が
サポートするすべての ABI に関する情報は [Rust Reference][ABI] にあります。
unsafe extern ブロック内で宣言された各項目は、暗黙的に unsafe です。
しかし、FFI 関数の中には、呼び出しても 安全な ものもあります。たとえば、
C の標準ライブラリにある abs 関数にはメモリ安全性に関する考慮事項がなく、
任意の i32 で呼び出せることがわかっています。このような場合には、
unsafe extern ブロック内にあるとしても、この特定の関数は安全に呼び出せる
と示すために safe キーワードを使えます。その変更を行えば、リスト 20-9
に示すように、それを呼び出すのに unsafe ブロックはもう不要になります。
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
関数を safe としてマークしても、それだけで本質的に安全になるわけでは
ありません! むしろ、それは「これが安全である」と Rust に約束している
ようなものです。その約束が守られるようにする責任は、依然としてあなたにあります!
他の言語から Rust 関数を呼び出す
extern を使って、他の言語から Rust 関数を呼び出せるインターフェイスを
作成することもできます。extern ブロック全体を作成する代わりに、対象の
関数に対する fn キーワードの直前に extern キーワードを追加し、
使用する ABI を指定します。また、この関数名を Rust コンパイラが
マングルしないように伝えるため、#[unsafe(no_mangle)] アノテーションも
追加する必要があります。マングリング とは、コンパイラが関数に与えた
名前を、コンパイル過程の他の部分が利用できるより多くの情報を含む一方で、
人間には読みにくい別の名前に変更することです。各プログラミング言語の
コンパイラは名前を少しずつ異なる形でマングルするため、Rust の関数を他の
言語から名前で参照できるようにするには、Rust コンパイラの名前の
マングリングを無効にしなければなりません。これは、組み込みのマングリングが
ないとライブラリ間で名前衝突が起こる可能性があるため unsafe であり、
そのため、選んだ名前をマングリングなしで安全に公開できるようにする責任は私たちにあります。
次の例では、call_from_c 関数を共有ライブラリにコンパイルして C から
リンクしたあと、C コードからアクセスできるようにします:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("C から Rust 関数が呼び出されました!");
}
この extern の使い方では、unsafe が必要なのは属性だけであり、
extern ブロックには不要です。
可変な静的変数へのアクセスまたは変更
この本ではまだグローバル変数について触れていませんが、Rust はこれを サポートしている一方で、Rust の所有権規則と組み合わせると問題になることが あります。2 つのスレッドが同じ可変なグローバル変数にアクセスすると、 データ競合を引き起こす可能性があります。
Rust では、グローバル変数は 静的 変数と呼ばれます。リスト 20-10 には、 文字列スライスを値として持つ静的変数の宣言と使用の 例が示されています。
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
静的変数は、定数に似ています。定数については、第 3 章の
[「定数の宣言」][constants] 節で説明しました。静的変数の
名前は、慣例として SCREAMING_SNAKE_CASE です。静的変数には
'static ライフタイムを持つ参照しか保存できません。つまり、Rust
コンパイラがそのライフタイムを判断できるので、私たちはそれを明示的に
注釈する必要がありません。不変な静的変数へのアクセスは安全です。
定数と不変な静的変数の微妙な違いの 1 つは、静的変数内の値はメモリ上の
固定アドレスを持つことです。その値を使うと、常に同じデータにアクセスする
ことになります。一方、定数は使われるたびにそのデータを複製してよいことに
なっています。もう 1 つの違いは、静的変数は可変にもできることです。
可変な静的変数へのアクセスと変更は unsafe です。リスト 20-11 では、
COUNTER という名前の可変な静的変数を宣言し、アクセスし、変更する
方法を示します。
static mut COUNTER: u32 = 0;
/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
通常の変数と同様に、可変性は mut キーワードを使って指定します。COUNTER から読み取ったり COUNTER に書き込んだりするコードは、すべて unsafe ブロック内になければなりません。リスト 20-11 のコードはコンパイルされ、予想どおり COUNTER: 3 を出力します。これはシングルスレッドだからです。複数のスレッドから COUNTER にアクセスすると、おそらくデータ競合が発生するため、未定義動作になります。したがって、関数全体を unsafe としてマークし、安全性に関する制約を文書化しておく必要があります。そうすることで、この関数を呼び出す人は、何を安全に行えて、何を安全には行えないのかを把握できます。
unsafe 関数を書くときはいつでも、SAFETY で始まるコメントを書き、その関数を安全に呼び出すために呼び出し側が何をする必要があるのかを説明するのが慣例です。同様に、unsafe な操作を行うときもいつでも、SAFETY で始まるコメントを書き、安全性のルールがどのように守られているかを説明するのが慣例です。
さらに、コンパイラはコンパイラ lint によって、可変な静的変数への参照を作成しようとする試みをデフォルトで拒否します。その lint の保護を明示的に無効化するために #[allow(static_mut_refs)] アノテーションを追加するか、raw borrow 演算子のいずれかで作成した生ポインタを介して可変な静的変数にアクセスしなければなりません。これには、このコードリストの println! で使われているときのように、参照が見えない形で作成される場合も含まれます。静的な可変変数への参照を生ポインタ経由で作成するよう要求することで、それらを使う際の安全性要件がより明確になります。
グローバルにアクセス可能な可変データでは、データ競合がないことを保証するのが難しいため、Rust は可変な静的変数を unsafe だと見なします。可能であれば、異なるスレッドからのデータアクセスが安全に行われていることをコンパイラが検査できるように、第 16 章で説明した並行性のテクニックやスレッドセーフなスマートポインタを使うほうが望ましいです。
Unsafe トレイトを実装する
unsafe は unsafe トレイトの実装にも使えます。トレイトは、そのメソッドの少なくとも 1 つに、コンパイラが検証できない何らかの不変条件がある場合に unsafe になります。トレイトが unsafe であることは、trait の前に unsafe キーワードを追加して宣言し、リスト 20-12 に示すように、そのトレイトの実装も unsafe としてマークします。
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
unsafe impl を使うことで、私たちはコンパイラが検証できない不変条件を自分たちが守ると約束していることになります。
例として、第 16 章の [「Send と Sync による拡張可能な並行性」][send-and-sync] 節で説明した Send と Sync のマーカートレイトを思い出してください。私たちの型が Send と Sync を実装したほかの型だけで構成されている場合、コンパイラはこれらのトレイトを自動的に実装します。生ポインタのように Send や Sync を実装していない型を含む型を実装し、その型を Send や Sync としてマークしたい場合は、unsafe を使わなければなりません。私たちの型が、スレッド間で安全に送信されたり、複数のスレッドからアクセスされたりできるという保証を満たしているかどうかを Rust は検証できません。そのため、それらの検査を手動で行い、そのことを unsafe で示す必要があります。
Union のフィールドにアクセスする
unsafe でしか行えない最後の操作は、union のフィールドにアクセスすることです。ユニオン は struct に似ていますが、特定のインスタンスでは、ある時点で使われるのは宣言されたフィールドのうち 1 つだけです。ユニオンは主に、C コード内の union とやり取りするために使われます。union のフィールドへのアクセスが unsafe なのは、union インスタンスに現在格納されているデータの型を Rust が保証できないためです。union については [Rust Reference][unions] でさらに学べます。
Miri を使って Unsafe コードを検査する
unsafe コードを書くときには、自分が書いたものが本当に安全で正しいかどうかを確認したいと思うかもしれません。そのための最良の方法の 1 つが、未定義動作を検出するための公式 Rust ツールである Miri を使うことです。borrow checker がコンパイル時に動作する 静的 ツールであるのに対し、Miri は実行時に動作する 動的 ツールです。Miri は、あなたのプログラムやそのテストスイートを実行し、Rust がどのように動作すべきかについて Miri が理解しているルールに違反したときにそれを検出することで、コードを検査します。
Miri を使うには Rust の nightly ビルドが必要です(これについては [付録 G: Rust の作られ方と「Nightly Rust」][nightly] でさらに説明します)。Rust の nightly 版と Miri ツールの両方は、rustup +nightly component add miri と入力することでインストールできます。これは、プロジェクトが使用する Rust のバージョンを変更するものではありません。必要なときに使えるように、単にそのツールをシステムに追加するだけです。プロジェクトに対して Miri を実行するには、cargo +nightly miri run または cargo +nightly miri test と入力します。
これがどれほど役立つかの例として、リスト 20-7 に対して実行したときに何が起こるかを見てみましょう。
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
5 | let r = address as *mut i32;
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
= note: BACKTRACE:
= note: inside `main` at src/main.rs:5:13: 5:32
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
--> src/main.rs:7:35
|
7 | let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:7:35: 7:70
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri は、整数をポインタにキャストしており、それが問題かもしれないことを正しく警告してくれますが、そのポインタがどのように生成されたかを知らないため、実際に問題があるかどうかまでは判断できません。続いて、私たちがダングリングポインタを持っているため、リスト 20-7 に未定義動作がある箇所で Miri はエラーを返します。Miri のおかげで、今では未定義動作のリスクがあることが分かり、コードをどうすれば安全にできるかを考えられます。場合によっては、Miri はエラーの修正方法について提案までしてくれます。
Miri は、unsafe コードを書くときに起こしうるあらゆる誤りを捕まえるわけではありません。Miri は動的解析ツールなので、実際に実行されたコードに関する問題しか検出しません。つまり、書いた unsafe コードに対する信頼を高めるには、Miri を適切なテスト手法と併用する必要があります。また、Miri はコードが不健全になりうるあらゆる可能な形を網羅しているわけでもありません。
別の言い方をすると、Miri が問題を 検出した なら、そこにバグがあることは分かりますが、Miri がバグを 検出しなかった からといって、問題がないとは限りません。それでも、Miri は非常に多くのことを検出できます。この章にあるほかの unsafe コードの例でも試してみて、何と言うかを見てみてください。
Miri については、[GitHub リポジトリ][miri] でさらに学べます。
Unsafe コードを正しく使う
先ほど説明した 5 つの超能力のいずれかを使うために unsafe を使うこと自体は、間違いでも忌避されることでもありません。しかし、コンパイラがメモリ安全性の維持を助けてくれないため、unsafe コードを正しく書くのはより難しくなります。unsafe コードを使う理由があるなら、そうしてかまいませんし、unsafe という明示的な注釈があることで、問題が発生したときにその原因を追跡しやすくなります。unsafe コードを書くときはいつでも、Miri を使うことで、自分が書いたコードが Rust のルールを守っているという確信をより高められます。
unsafe Rust を効果的に扱う方法をさらに深く掘り下げたいなら、Rust の unsafe に関する公式ガイドである [The Rustonomicon][nomicon] を読んでください。
[dangling-references]: ch04-02-references-and-borrowing.html#dangling-references
[ABI]: ../reference/items/external-blocks.html#abi
[constants]: ch03-01-variables-and-mutability.html#declaring-constants
[send-and-sync]: ch16-04-extensible-concurrency-sync-and-send.html
[the-slice-type]: ch04-03-slices.html#the-slice-type
[unions]: ../reference/items/unions.html
[miri]: https://github.com/rust-lang/miri
[editions]: appendix-05-editions.html
[nightly]: appendix-07-nightly-rust.html
[nomicon]: https://doc.rust-lang.org/nomicon/