コンテンツにスキップ

Rustのunsafe

ダングリング参照がない、データ競合がない、範囲外アクセスがない。このコースで話してきた保証は、どれもコンパイラが強制しているものです。unsafe は、コンパイラに「あなたには安全だと確かめられないことをさせてほしい。その代わり、責任は自分が持つ」と伝える方法です。

unsafe でできること、できないこと

Section titled “unsafe でできること、できないこと”

よくある誤解は、unsafe を使うと Rust の安全性チェックがすべてオフになる、というものです。そうではありません。unsafe ブロックの中でできるようになるのは、ふだんは禁止されている、ちょうど5つの操作だけです。

  1. 生ポインタ(raw pointer)を参照外しする
  2. unsafe な関数を呼び出す
  3. 可変な static 変数にアクセスしたり、変更したりする
  4. unsafe なトレイトを実装する
  5. union のフィールドにアクセスする

それ以外のこと、つまり借用のルールやムーブの仕組み、型チェックなどは、unsafe なコードの中でもすべてそのまま適用されます。unsafe は、5つの決まった操作のためのメスであって、安全装置のオフスイッチではないのです。

Rust のふつうの参照(& と &mut)は、いつも有効で、正しくアラインメントされたデータを指しています。これはコンパイラが保証していることの一部です。生ポインタ(*const T や *mut T)にはこの保証がありません。だから、生ポインタから値を読むには unsafe が必要なのです。

fn main() {
let value = 5;
let pointer = &value as *const i32;
unsafe {
println!("{}", *pointer);
}
}

実行結果:

5
▶ 実際に試してみよう

生ポインタを作ることは、安全なコードの中でもできます。unsafe が必要になるのは、それを参照外しする(指している値を読む)ときだけです。コンパイラには、もうそのポインタが有効かどうかを確かめられないからです。

関数の中には、それ自体に unsafe が付いているものがあります。コンパイラではチェックできない前提条件があるからです。そういう関数を呼び出すことは、「前提条件は自分で確認しました」という約束になります。

unsafe fn dangerous() {
println!("この関数は、前提条件をあなたが確認済みだと信じています");
}
fn main() {
unsafe {
dangerous();
}
}

実行結果:

この関数は、前提条件をあなたが確認済みだと信じています
▶ 実際に試してみよう

標準ライブラリには、実際に呼び出せる unsafe な関数があります。そのひとつが、スライスの get_unchecked です。numbers[i] と同じように使えますが、範囲チェックを省略します。i が範囲外でもパニックしません。そこにたまたまあるメモリを読んでしまうのです。これを未定義動作(undefined behavior)と呼びます。プログラムがクラッシュするかもしれないし、でたらめな値を表示するかもしれないし、ある日突然おかしくなるまでは問題なく見えるかもしれません。

ですから、この関数を呼ぶときのルールは「有効だとわかっているインデックスでだけ呼ぶこと」です。よく使われるのは、ふつうの安全な関数の中で、そのルールを自分でチェックするというパターンです。

fn first_or_zero(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
return 0;
}
// SAFETY: スライスは空ではないので、インデックス 0 は範囲内にある。
unsafe { *numbers.get_unchecked(0) }
}
fn main() {
println!("{}", first_or_zero(&[7, 8, 9]));
println!("{}", first_or_zero(&[]));
}

実行結果:

7
0
▶ 実際に試してみよう

呼び出す側から見えるものに注目してください。fn first_or_zero(numbers: &[i32]) -> i32 です。シグネチャには unsafe がどこにもありませんし、間違った呼び出し方をする方法もありません。関数が unsafe ブロックにたどり着く前に、ルールをチェックしているからです。unsafe は小さく、内側に隠れたままです。

経験豊富な Rust 開発者は、次の2つの習慣を守っています。

  • すべての unsafe ブロックの上に // SAFETY: コメントを書く。ルールが守られている理由を書いておきます。次にコードを読む人のため、そして未来の自分のためです。
  • unsafe ブロックはできるだけ小さくする。そうすれば、念入りに確かめるべきコードが少なくて済みます。

正直に言うと、実際のコードでこの関数をこのとおりに書くことはありません。numbers.first() が同じことを安全にやってくれますし、コンパイラは不要だと証明できる範囲チェックをとても上手に取り除いてくれるからです。それでも、このパターン(ルールをチェックしてから、安全な関数の中で unsafe を呼ぶ)は、Vec や String などが作られている方法そのものです。FFIのページでも、同じ方法で C の関数を包んでいます。

そもそも unsafe があるのはなぜ?

Section titled “そもそも unsafe があるのはなぜ?”

Rust は安全性がすべてなのに、なぜこんなものを許しているのでしょうか? それは、実際には完全に安全なのに、コンパイル時に安全だと証明するのがどうしても不可能なことがあるからです。ハードウェアを直接操作する、C のライブラリを呼び出す(FFIを参照)、ある種の高性能なデータ構造を作る、といったことです。unsafe があるおかげで、標準ライブラリ(と、まれにあなた自身のコード)はこうした低レベルの部品を実装できます。そして、その上に作られるものはすべて、ふつうの、完全にチェックされる安全な Rust のままでいられるのです。

unsafe を書いたからといって、コードにバグがあるということにはなりません。そのブロックの中では、ある種の間違いをコンパイラが見つけてくれなくなるので、同じ保証を自分の手で守る責任があなたに移る、ということです。経験豊富な Rust 開発者の多くも、unsafe なコードを直接書くことはほとんどありません。unsafe はたいてい、ライブラリの奥にある、小さく慎重にレビューされた部分にだけ置かれ、ほかの人が使うための安全な API で包まれています。

1. 生ポインタを通して値を変える

Section titled “1. 生ポインタを通して値を変える”

let mut score = 10; を作り、それを指す *mut i32 の生ポインタを作って、そのポインタを使って 5 を足してください。そのあと score を表示します。ヒント:ポインタは &mut score as *mut i32 で作り、unsafe ブロックの中で *pointer += 5 と書きます。

解答を見る
fn main() {
let mut score = 10;
let pointer = &mut score as *mut i32; // 作るのは安全
unsafe {
*pointer += 5; // それを通して書き込むのは安全ではない
}
println!("スコア = {score}");
}

実行結果:

スコア = 15
▶ 実際に試してみよう

これが実際に安全なのは、score がまだ生きていて、書き込んでいるあいだにほかの誰もそれを使っていないからです。unsafe を使ったら、こうした約束を守るのはあなたの仕事になります。

fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 を書いてください。index の位置にある要素を返し、インデックスが範囲外なら default を返します。読み取りには get_unchecked を使い、ルールを破るような呼び出し方が絶対にできないようにしてください。ヒント:まず index < numbers.len() を比べて、それが true のときだけ get_unchecked を呼びます。

解答を見る
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 {
if index < numbers.len() {
// SAFETY: index が範囲内であることを、たった今チェックした。
unsafe { *numbers.get_unchecked(index) }
} else {
default
}
}
fn main() {
let numbers = [10, 20, 30];
println!("{}", get_or(&numbers, 1, -1));
println!("{}", get_or(&numbers, 5, -1));
}

実行結果:

20
-1
▶ 実際に試してみよう

インデックス 5 は unsafe ブロックまでたどり着かないので、安全に処理されます。比べてみると、完全に安全なバージョンは numbers.get(index).copied().unwrap_or(default) です。ふだん書くのはこちらでしょう。

  • unsafe で使えるようになるのは、ちょうど5つの操作(主に生ポインタと unsafe な関数の呼び出し)だけです。それ以外はすべて、これまでどおり完全にチェックされます。
  • 生ポインタ(*const T / *mut T)は安全なコードでも作れますが、参照外しには unsafe が必要です。
  • unsafe は、先にルールをチェックする小さな安全な関数で包みましょう。それが正しい理由は // SAFETY: コメントで説明します。
  • unsafe は、低レベルのコード(ハードウェアの操作や C との連携)を作るためにあります。それを使う側のコードは安全なままです。
  • 初心者のうちは、自分で書くことはめったにありません。まずは安全な抽象化を探しましょう。

理解度チェック

1. unsafe ブロックで実際にできるようになるのは何ですか?

2. 安全なコードの中で生ポインタを作ることはできますか?

3. 初心者のうちは、自分で unsafe を書く機会はどれくらいありますか?

4. unsafe なコードを書くと、プログラムにはバグがあることになりますか?

スコア: 0 / 4