はじめに
Unsafe Rustへようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、コンパイル時に強制されるメモリセーフティの保証を迂回することができ、追加の超能力を与える機能である unsafe Rust を探り、それを使用する際のリスクと責任についても理解します。
unsafe Rust
これまでに議論したすべてのコードは、コンパイル時に Rust のメモリセーフティの保証が適用されてきました。しかし、Rust にはこれらのメモリセーフティの保証を適用しない 2 番目の言語が内包されています。それは「unsafe Rust」と呼ばれ、通常の Rust と同じように機能しますが、追加の超能力を与えます。
unsafe Rust が存在するのは、静的解析が本来的に保守的であるためです。コンパイラがコードが保証を守っているかどうかを判断しようとするとき、無効なプログラムをいくつか受け入れるよりも、有効なプログラムをいくつか拒否した方が良いからです。コードは「おそらく」問題ないかもしれませんが、Rust コンパイラに十分な情報がないと自信が持てない場合、コードを拒否します。これらの場合、unsafe コードを使ってコンパイラに「信じてください、私は何をしているか知っています」と伝えることができます。ただし、unsafe Rust を使うことは自責風險です。もし unsafe コードを誤って使うと、メモリセーフティがないために、ヌルポインタの参照解除などの問題が発生する可能性があります。
Rust に unsafe な分身があるもう 1 つの理由は、基盤となるコンピュータハードウェアが本来不健全であることです。Rust が unsafe 操作を許さなければ、特定のタスクを行うことができません。Rust は、オペレーティングシステムと直接対話したり、自作のオペレーティングシステムを書いたりするような、低レベルのシステムプログラミングを許す必要があります。低レベルのシステムプログラミングを行うことは、この言語の目標の 1 つです。では、unsafe Rust を使って何ができるか、そしてどのように行うかを探ってみましょう。
不健全な超能力
unsafe Rust に切り替えるには、unsafeキーワードを使用してから、unsafe コードを含む新しいブロックを開始します。unsafe Rust では、safe Rust ではできない 5 つの操作を行うことができます。これらを「不健全な超能力」と呼んでいます。これらの超能力には、以下の能力が含まれます。
- 生のポインタを参照する
- 不健全な関数またはメソッドを呼び出す
- 可変な静的変数にアクセスまたは変更する
- 不健全なトレイトを実装する
unionのフィールドにアクセスする
重要なことは、unsafeがバローチェッカーをオフにしたり、Rust の他のセーフティチェックのいずれかを無効にしたりしないことです。もし unsafe コードで参照を使う場合、それは依然としてチェックされます。unsafeキーワードは、コンパイラによるメモリセーフティのチェック対象外のこれら 5 つの機能にのみアクセスを与えます。unsafe ブロック内ではある程度のセーフティが保たれます。
また、unsafeはブロック内のコードが必ずしも危険であることや、必ずしもメモリセーフティの問題があることを意味しません。プログラマとして、unsafe ブロック内のコードがメモリにアクセスする方法が妥当であることを保証する必要があります。
人間は誤りやすく、ミスが起こります。しかし、これらの 5 つの不健全な操作をunsafeで注釈付けされたブロック内にすることで、メモリセーフティに関連するエラーは必ず unsafe ブロック内にあることがわかります。unsafe ブロックを小さく保ちましょう。後でメモリバグを調査するときに感謝するでしょう。
できる限り不健全なコードを分離するためには、そのようなコードを安全な抽象化の中に囲み、安全な API を提供することが望ましいです。これについては、後で不健全な関数とメソッドを調べるときに本章で議論します。標準ライブラリの一部は、監査された不健全なコードに対する安全な抽象化として実装されています。不健全なコードを安全な抽象化でラップすることで、unsafeの使用が、unsafeコードで実装された機能を使用したいあなた自身やユーザーのすべての場所に漏れ出すことを防ぎます。なぜなら、安全な抽象化を使うことは安全だからです。
では、5 つの不健全な超能力を順に見ていきましょう。また、不健全なコードに対して安全なインターフェイスを提供するいくつかの抽象化についても見ていきます。
生のポインタを参照する
「浮動参照」の項で、コンパイラが参照が常に有効であることを保証することを述べました。unsafe Rust には、参照に似た 2 つの新しい型である「生のポインタ」があります。参照と同様に、生のポインタは不変または可変で、それぞれ*const Tと*mut Tとして書かれます。アスタリスクは参照解除演算子ではなく、型名の一部です。生のポインタのコンテキストでは、「不変」とは、ポインタが参照解除された後に直接代入できないことを意味します。
参照やスマートポインタとは異なり、生のポインタは:
- 不変ポインタと可変ポインタの両方、または同じ場所に対する複数の可変ポインタを持つことで、借用規則を無視することができます。
- 有効なメモリを指すことが保証されていません。
- ヌルを指すことが許されます。
- 自動的なクリーンアップを実装していません。
Rust がこれらの保証を強制することを選ばないことで、保証されたセーフティを捨てて、より高いパフォーマンスや、Rust の保証が適用されない別の言語やハードウェアとのインターフェイス能力を得ることができます。
リスト 19-1 は、参照から不変と可変の生のポインタを作成する方法を示しています。
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
リスト 19-1:参照から生のポインタを作成する
このコードにunsafeキーワードが含まれていないことに注意してください。私たちは安全なコードで生のポインタを作成することができます。ただし、後ほど見るように、unsafe ブロックの外で生のポインタを参照解除することはできません。
私たちはasを使って不変参照と可変参照をそれぞれ対応する生のポインタ型にキャストすることで生のポインタを作成しました。これらは参照から直接作成されており、有効であることが保証されているため、これらの特定の生のポインタは有効であることがわかります。ただし、任意の生のポインタについてはそのような仮定をすることはできません。
これを示すために、次に有効性が確実ではない生のポインタを作成します。リスト 19-2 は、メモリ内の任意の場所に生のポインタを作成する方法を示しています。任意のメモリを使用することは未定義です。そのアドレスにデータがあるかもしれませんし、ないかもしれません。コンパイラはコードを最適化してメモリアクセスを行わなくするかもしれません。または、プログラムはセグメンテーションフォールトで終了するかもしれません。通常、このようなコードを書く理由はあまりありませんが、可能です。
let address = 0x012345usize;
let r = address as *const i32;
リスト 19-2:任意のメモリアドレスに生のポインタを作成する
思い出してください。私たちは安全なコードで生のポインタを作成することができますが、生のポインタを参照解除して指されているデータを読むことはできません。リスト 19-3 では、unsafeブロックが必要な生のポインタに参照解除演算子*を使用しています。
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
リスト 19-3:unsafeブロック内で生のポインタを参照解除する
ポインタを作成すること自体は害はありません。そのポインタが指す値にアクセスしようとするときに、無効な値と対処することになるかもしれません。
また、リスト 19-1 と 19-3 では、numが格納されている同じメモリ場所を指す*const i32と*mut i32の生のポインタを作成しました。代わりにnumに対して不変参照と可変参照を作成しようとすると、コードはコンパイルされません。なぜなら、Rust の所有権規則は、不変参照が存在する間に可変参照を許さないからです。生のポインタを使えば、同じ場所に対して可変ポインタと不変ポインタを作成し、可変ポインタを通じてデータを変更することができ、データレースが発生する可能性があります。注意してください!
これらのすべての危険があるのに、なぜ生のポインタを使うのでしょうか?主な使い道の 1 つは、C コードとのインターフェイスを行う場合です。「不健全な関数またはメソッドを呼び出す」で見るように。もう 1 つのケースは、借用チェッカーが理解できない安全な抽象化を構築する場合です。不健全な関数を紹介した後、不健全なコードを使用する安全な抽象化の例を見てみましょう。
不健全な関数またはメソッドを呼び出す
unsafe ブロックで実行できる 2 番目の操作は、不健全な関数を呼び出すことです。不健全な関数とメソッドは、通常の関数とメソッドとまったく同じように見えますが、定義の残りの部分の前に追加のunsafeがあります。このコンテキストでのunsafeキーワードは、この関数を呼び出す際に守る必要がある要件があることを示しています。なぜなら、Rust はこれらの要件を満たしていることを保証できないからです。unsafeブロック内で不健全な関数を呼び出すことで、私たちはこの関数のドキュメントを読んでおり、関数の契約を守る責任を負うことを言っています。
ここに、本体で何もしないdangerousという不健全な関数があります。
unsafe fn dangerous() {}
unsafe {
dangerous();
}
私たちは、別のunsafeブロック内でdangerous関数を呼び出さなければなりません。unsafeブロックなしでdangerousを呼び出そうとすると、エラーが発生します。
error[E0133]: call to unsafe function is unsafe and requires
unsafe function or 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
unsafeブロックを使うことで、私たちは Rust に対して、関数のドキュメントを読んでおり、それを適切に使う方法を理解しており、関数の契約を満たしていることを確認したことを主張しています。
不健全な関数の本体は、実質的にunsafeブロックであるため、不健全な関数内で他の不健全な操作を行うには、別のunsafeブロックを追加する必要はありません。
不健全なコードに対する安全な抽象化の作成
関数に不健全なコードが含まれているからといって、その関数全体を不健全としてマークする必要はありません。実際、不健全なコードを安全な関数にラップすることは一般的な抽象化です。例として、標準ライブラリのsplit_at_mut関数を見てみましょう。この関数にはいくつかの不健全なコードが必要です。その実装方法を調べてみましょう。この安全なメソッドは可変スライスに定義されています。このメソッドは 1 つのスライスを取り、引数として与えられたインデックスでスライスを分割することで 2 つにします。リスト 19-4 はsplit_at_mutを使用する方法を示しています。
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]);
リスト 19-4:安全なsplit_at_mut関数の使用
この関数を、安全な Rust のみを使って実装することはできません。リスト 19-5 のような試みはコンパイルされません。簡単のために、split_at_mutを関数として実装し、i32値のスライスに対してのみ、ジェネリック型Tではなく実装します。
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..])
}
リスト 19-5:安全な Rust のみを使ったsplit_at_mutの試みの実装
この関数はまず、スライスの合計長を取得します。次に、引数として与えられたインデックスがスライス内にあるかどうかをチェックすることで、そのインデックスが長さ以下であることをアサートします。このアサーションは、スライスを分割するために長さより大きいインデックスを渡すと、関数がそのインデックスを使用しようとする前にパニックすることを意味します。
次に、タプルで 2 つの可変スライスを返します。1 つは元のスライスの先頭からmidインデックスまでで、もう 1 つはmidからスライスの末尾までです。
リスト 19-5 のコードをコンパイルしようとすると、エラーが発生します。
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:9:31
|
2 | values: &mut [i32],
| - let's call the lifetime of this reference `'1`
...
9 | (&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`
Rust の借用チェッカーは、スライスの異なる部分を借用していることがわかりません。それは、同じスライスから 2 回借用していることのみを知っています。スライスの異なる部分を借用すること自体は基本的に問題ありません。なぜなら、2 つのスライスは重複していないからです。しかし、Rust はこれを知るほど賢くありません。コードが問題ないことを知っていても、Rust が知らない場合、不健全なコードを使う時が来ます。
リスト 19-6 は、split_at_mutの実装を動作させるために、unsafeブロック、生のポインタ、および不健全な関数へのいくつかの呼び出しをどのように使うかを示しています。
use std::slice;
fn split_at_mut(
values: &mut [i32],
mid: usize,
) -> (&mut [i32], &mut [i32]) {
1 let len = values.len();
2 let ptr = values.as_mut_ptr();
3 assert!(mid <= len);
4 unsafe {
(
5 slice::from_raw_parts_mut(ptr, mid),
6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
リスト 19-6:split_at_mut関数の実装における不健全なコードの使用
「スライス型」の項で思い出してください。スライスは、あるデータへのポインタと、スライスの長さで構成されています。私たちはlenメソッドを使ってスライスの長さを取得し[1]、as_mut_ptrメソッドを使ってスライスの生のポインタにアクセスします[2]。この場合、i32値の可変スライスを持っているため、as_mut_ptrは*mut i32型の生のポインタを返します。これを変数ptrに格納しました。
midインデックスがスライス内にあることをアサートし続けます[3]。次に、不健全なコードに入ります[4]。slice::from_raw_parts_mut関数は、生のポインタと長さを取り、スライスを作成します。これを使って、ptrから始まり、mid個の要素からなるスライスを作成します[5]。次に、midを引数としてptrに対してaddメソッドを呼び出して、midから始まる生のポインタを取得します。そして、そのポインタとmid以降の残りの要素数を長さとして使ってスライスを作成します[6]。
slice::from_raw_parts_mut関数は不健全です。なぜなら、生のポインタを取り、このポインタが有効であることを信頼しなければならないからです。生のポインタのaddメソッドも不健全です。なぜなら、オフセット位置も有効なポインタであることを信頼しなければならないからです。したがって、slice::from_raw_parts_mutとaddへの呼び出しの周りにunsafeブロックを置かなければなりませんでした。そうしないと、それらを呼び出すことができませんでした。コードを見て、midがlen以下でなければならないことをアサートすることで、unsafeブロック内で使用されるすべての生のポインタが、スライス内のデータへの有効なポインタであることがわかります。これは、unsafeの許容可能で適切な使用例です。
結果となるsplit_at_mut関数をunsafeとしてマークする必要はなく、安全な Rust からこの関数を呼び出すことができます。この関数は、不健全なコードに対する安全な抽象化を作成しました。この関数の実装は、不健全なコードを安全な方法で使用しています。なぜなら、この関数がアクセスできるデータからの有効なポインタのみを作成するからです。
対照的に、リスト 19-7 のslice::from_raw_parts_mutの使用は、スライスが使用されるときにおそらくクラッシュします。このコードは、任意のメモリ位置を取り、10,000 個の要素からなるスライスを作成します。
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 10000)
};
リスト 19-7:任意のメモリ位置からスライスを作成する
この任意の位置のメモリを所有していないため、このコードが作成するスライスが有効なi32値を含んでいることは保証されていません。valuesを有効なスライスとして使用しようとすると、未定義の動作が発生します。
外部コードを呼び出すための extern 関数の使用
時には、Rust コードが別の言語で書かれたコードと相互作用する必要があります。このために、Rust にはexternというキーワードがあり、これは「外部関数インターフェイス(FFI: Foreign Function Interface)」の作成と使用を容易にします。FFI は、プログラミング言語が関数を定義し、異なる(外部の)プログラミング言語がそれらの関数を呼び出せるようにする方法です。
リスト 19-8 は、C 標準ライブラリのabs関数との統合を設定する方法を示しています。externブロック内で宣言された関数は、常に Rust コードから呼び出すのが不健全です。その理由は、他の言語が Rust の規則と保証を強制しないため、Rust はそれらをチェックできず、セーフティを確保する責任はプログラマにあるからです。
ファイル名:src/main.rs
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!(
"Absolute value of -3 according to C: {}",
abs(-3)
);
}
}
リスト 19-8:別の言語で定義されたextern関数の宣言と呼び出し
extern "C"ブロック内では、呼び出したい別の言語の外部関数の名前とシグネチャを列挙します。"C"の部分は、外部関数が使用する「アプリケーションバイナリインターフェイス(ABI: Application Binary Interface)」を定義します。ABI は、アセンブリレベルで関数を呼び出す方法を定義します。"C" ABI は最も一般的で、C プログラミング言語の ABI に準拠しています。
他の言語から Rust 関数を呼び出す
また、
externを使って、他の言語が Rust 関数を呼び出せるようなインターフェイスを作成することもできます。全体的なexternブロックを作成する代わりに、externキーワードを追加し、関連する関数のfnキーワードの直前に使用する ABI を指定します。また、この関数の名前をマングルしないように Rust コンパイラに伝えるために、#[no_mangle]アノテーションを追加する必要があります。「マングリング」とは、コンパイラが関数に与えた名前を、コンパイルプロセスの他の部分が消費するためにより多くの情報を含む別の名前に変更することです。ただし、その名前は人間が読みやすくなります。各プログラミング言語のコンパイラは名前をマングルする方法が少し異なるため、他の言語で名前付け可能な Rust 関数を作成するには、Rust コンパイラの名前マングリングを無効にする必要があります。次の例では、
call_from_c関数を C コードからアクセスできるようにします。この関数は、共有ライブラリにコンパイルされ、C からリンクされた後に使用できます。#[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); }この
externの使用にはunsafeは必要ありません。
可変静的変数へのアクセスまたは変更
この本では、まだグローバル変数について話していません。Rust はグローバル変数をサポートしていますが、Rust の所有権規則とともに問題があります。2 つのスレッドが同じ可変グローバル変数にアクセスしている場合、データレースが発生する可能性があります。
Rust では、グローバル変数は「静的」変数と呼ばれます。リスト 19-9 は、文字列をスライスとして値に持つ静的変数の宣言と使用の例を示しています。
ファイル名:src/main.rs
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
リスト 19-9:不変静的変数の定義と使用
静的変数は、「定数」の項で議論した定数と似ています。静的変数の名前は、慣例によりSCREAMING_SNAKE_CASEになっています。静的変数は、'static寿命の参照のみを格納できます。これは、Rust コンパイラが寿命を把握できるため、明示的に注釈を付ける必要はありません。不変静的変数へのアクセスは安全です。
定数と不変静的変数の微妙な違いは、静的変数の値がメモリ内の固定アドレスを持つことです。値を使用すると、常に同じデータにアクセスされます。一方、定数は使用するたびにデータを複製することができます。もう 1 つの違いは、静的変数が可変であることができることです。可変静的変数へのアクセスと変更は「不健全」です。リスト 19-10 は、COUNTERという名前の可変静的変数を宣言、アクセス、変更する方法を示しています。
ファイル名:src/main.rs
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {COUNTER}");
}
}
リスト 19-10:可変静的変数への読み書きは不健全です。
通常の変数と同様に、mutキーワードを使用して可変性を指定します。COUNTERから読み書きするすべてのコードは、unsafeブロック内にある必要があります。このコードはコンパイルされ、予想通りCOUNTER: 3が表示されます。なぜなら、これは単一スレッドであるからです。複数のスレッドがCOUNTERにアクセスすると、おそらくデータレースが発生します。
グローバルにアクセス可能な可変データでは、データレースがないことを確認するのは難しいです。これが、Rust が可変静的変数を不健全と見なす理由です。可能な限り、第 16 章で議論した並列処理技術とスレッドセーフなスマートポインタを使用することが好ましいです。そうすることで、コンパイラが異なるスレッドからのデータアクセスが安全に行われることを確認します。
不健全なトレイトの実装
unsafeを使って、不健全なトレイトを実装することができます。トレイトが不健全であるのは、そのメソッドの少なくとも 1 つがコンパイラが検証できない不変条件を持っている場合です。トレイトがunsafeであることを宣言するには、traitの前にunsafeキーワードを追加し、トレイトの実装もunsafeとしてマークします。これは、リスト 19-11 に示すようになります。
unsafe trait Foo {
// メソッドはここに記述します
}
unsafe impl Foo for i32 {
// メソッドの実装はここに記述します
}
リスト 19-11:不健全なトレイトの定義と実装
unsafe implを使うことで、コンパイラが検証できない不変条件を守ることを約束します。
例として、「Send と Sync トレイトによる拡張可能な並列処理」で議論したSendとSyncマーカートレイトを思い出してください。もし私たちの型が完全にSend型とSync型で構成されている場合、コンパイラは自動的にこれらのトレイトを実装します。もし私たちが、生のポインタのようにSendでもSyncでもない型を含む型を実装し、その型をSendまたはSyncとしてマークしたい場合、unsafeを使わなければなりません。Rust は、私たちの型がスレッド間で安全に送信できる、または複数のスレッドからアクセスできるという保証を守っていることを検証できません。したがって、私たちはそれらのチェックを手動で行い、unsafeでそれを示す必要があります。
共用体のフィールドへのアクセス
unsafeのみで機能する最後の操作は、共用体のフィールドへのアクセスです。unionはstructに似ていますが、特定のインスタンスでは一度に宣言されたフィールドのうち 1 つのみが使用されます。共用体は主に C コードの共用体とのインターフェイスに使用されます。共用体のフィールドへのアクセスは不健全です。なぜなら、Rust は共用体インスタンスに現在格納されているデータの型を保証できないからです。共用体に関する詳細は、Rust リファレンスの https://doc.rust-lang.org/reference/items/unions.html で学ぶことができます。
不健全なコードを使用する場合
先ほど説明した 5 つの特権的な機能のいずれかを使うためにunsafeを使うことは、間違っているわけでも、非難されることでもありません。ただし、コンパイラがメモリセーフティを維持するのを助けることができないため、不健全なコードを正しく記述するのは難しくなります。不健全なコードを使う理由がある場合、それを行うことができます。明示的なunsafe注釈があると、問題が発生したときに問題の原因を特定しやすくなります。
まとめ
おめでとうございます!あなたは不健全な Rust の実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行うことができます。