Unsafe Rust
ここまでに議論してきたコードは全て、Rustのメモリ安全保証がコンパイル時に強制されていました。しかしながら、 Rustには、これらのメモリ安全保証を強制しない第2の言語が中に隠されています: それはunsafe Rustと呼ばれ、 普通のRustのように動きますが、おまけの強大な力を与えてくれます。
静的解析は原理的に保守的なので、unsafe Rustが存在します。コードが保証を保持しているかコンパイラが決定しようとする際、 なんらかの不正なプログラムを受け入れるよりも合法なプログラムを拒否したほうがいいのです。コードは大丈夫かもしれないけれど、 コンパイラにわかる範囲ではダメなのです!このような場合、unsafeコードを使用してコンパイラに「信じて!何をしているかわかってるよ」と教えられます。 欠点は、自らのリスクで使用することです: unsafeコードを誤って使用したら、 nullポインタ参照外しなどのメモリ非安全に起因する問題が起こることもあるのです。
Rustにunsafeな分身がある別の理由は、根本にあるコンピュータのハードウェアが本質的にunsafeだからです。 Rustがunsafeな処理を行わせてくれなかったら、特定の仕事を行えないでしょう。Rustは、低レベルなシステムプログラミングを許可する必要があります。 直接OSと相互作用したり、独自のOSを書くことさえもそうです。低レベルなシステムプログラミングに取り組むことは、 言語の目標の1つなのです。unsafe Rustでできることとその方法を探究しましょう。
unsafeの強大な力(superpower)
unsafe Rustに切り替えるには、unsafe
キーワードを使用し、それからunsafeコードを保持する新しいブロックを開始してください。
safe Rustでは行えない4つの行動をunsafe Rustでは行え、これはunsafe superpowersと呼ばれます。
そのsuperpowerには、以下の能力が含まれています:
- 生ポインタを参照外しすること
- unsafeな関数やメソッドを呼ぶこと
- 可変で静的な変数にアクセスしたり変更すること
- unsafeなトレイトを実装すること
unsafe
は、借用チェッカーや他のRustの安全性チェックを無効にしないことを理解するのは重要なことです:
unsafeコードで参照を使用しても、チェックはされます。unsafe
キーワードにより、これら4つの機能にアクセスできるようになり、
その場合、コンパイラによってこれらのメモリ安全性は確認されないのです。unsafeブロック内でも、ある程度の安全性は得られます。
また、unsafeは、そのブロックが必ずしも危険だったり、絶対メモリ安全上の問題を抱えていることを意味するものではありません:
その意図は、プログラマとしてunsafe
ブロック内のコードがメモリに合法的にアクセスすることを保証することです。
人間は失敗をするもので、間違いも起きますが、これら4つのunsafeな処理をunsafe
で注釈されたブロックに入れる必要があることで、
メモリ安全性に関するどんなエラーもunsafe
ブロック内にあるに違いないと知ります。unsafe
ブロックは小さくしてください;
メモリのバグを調査するときに感謝することになるでしょう。
unsafeなコードをできるだけ分離するために、unsafeなコードを安全な抽象の中に閉じ込め、安全なAPIを提供するのが最善です。
これについては、後ほどunsafeな関数とメソッドを調査する際に議論します。標準ライブラリの一部は、
検査されたunsafeコードの安全な抽象として実装されています。安全な抽象にunsafeなコードを包むことで、
unsafe
が、あなたやあなたのユーザがunsafe
コードで実装された機能を使いたがる可能性のある箇所全部に漏れ出ることを防ぎます。
安全な抽象を使用することは、安全だからです。
4つのunsafeなsuperpowerを順に見ていきましょう。unsafeなコードへの安全なインターフェイスを提供する一部の抽象化にも目を向けます。
生ポインタを参照外しする
第4章の「ダングリング参照」節で、コンパイラは、参照が常に有効であることを保証することに触れました。
unsafe Rustには参照に類似した生ポインタと呼ばれる2つの新しい型があります。参照同様、
生ポインタも不変や可変になり得て、それぞれ*const T
と*mut T
と表記されます。このアスタリスクは、参照外し演算子ではありません;
型名の一部です。生ポインタの文脈では、不変は、参照外し後に直接ポインタに代入できないことを意味します。
参照やスマートポインタと異なり、生ポインタは:
- 同じ場所への不変と可変なポインタや複数の可変なポインタが存在することで借用規則を無視できる
- 有効なメモリを指しているとは保証されない
- nullの可能性がある
- 自動的な片付けは実装されていない
これらの保証をコンパイラに強制させることから抜けることで、保証された安全性を諦めてパフォーマンスを向上させたり、 Rustの保証が適用されない他の言語やハードウェアとのインターフェイスの能力を得ることができます。
リスト19-1は、参照から不変と可変な生ポインタを生成する方法を示しています。
# #![allow(unused_variables)] #fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; #}
このコードにはunsafe
キーワードを含めていないことに注意してください。safeコードで生ポインタを生成できます;
もうすぐわかるように、unsafeブロックの外では、生ポインタを参照外しできないだけなのです。
as
を使って不変と可変な参照を対応する生ポインタの型にキャストして生ポインタを生成しました。
有効であることが保証される参照から直接生ポインタを生成したので、これらの特定の生ポインタは有効であることがわかりますが、
その前提をあらゆる生ポインタに敷くことはできません。
次に、有効であることが確信できない生ポインタを生成します。リスト19-2は、メモリの任意の箇所を指す生ポインタの生成法を示しています。 任意のメモリを使用しようとすることは未定義です: そのアドレスにデータがある可能性もあるし、ない可能性もあり、 コンパイラがコードを最適化してメモリアクセスがなくなる可能性もあるし、プログラムがセグメンテーションフォールトでエラーになる可能性もあります。 通常、このようなコードを書くいい理由はありませんが、可能ではあります。
# #![allow(unused_variables)] #fn main() { let address = 0x012345usize; let r = address as *const i32; #}
safeコードで生ポインタを生成できるけれども、生ポインタを参照外しして指しているデータを読むことはできないことを思い出してください。
リスト19-3では、unsafe
ブロックが必要になる参照外し演算子の*
を生ポインタに使っています。
# #![allow(unused_variables)] #fn main() { 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-1とリスト19-3では、
num
が格納されている同じメモリ上の場所を両方とも指す*const i32
と*mut i32
の生ポインタを生成したことに注目してください。
代わりにnum
への不変と可変な参照を生成しようとしたら、コードはコンパイルできなかったでしょう。
Rustの所有権規則により、不変参照と可変参照を同時に存在させられないからです。生ポインタなら、
同じ場所への可変なポインタと不変なポインタを生成でき、可変なポインタを通してデータを変更し、データ競合を引き起こす可能性があります。
気を付けてください!
これらの危険がありながら、一体何故生ポインタを使うのでしょうか?主なユースケースの1つは、次の節「unsafeな関数やメソッドを呼ぶ」で見るように、 Cコードとのインターフェイスです。別のユースケースは、借用チェッカーには理解できない安全な抽象を構成する時です。 unsafeな関数を導入し、それからunsafeコードを使用する安全な抽象の例に目を向けます。
unsafeな関数やメソッドを呼ぶ
unsafeブロックが必要になる2番目の処理は、unsafe関数の呼び出しです。unsafeな関数やメソッドも見た目は、
普通の関数やメソッドと全く同じですが、残りの定義の前に追加のunsafe
があります。この文脈でのunsafe
キーワードは、
この関数を呼ぶ際に保持しておく必要のある要求が関数にあることを示唆します。コンパイラには、
この要求を満たしているか保証できないからです。unsafe
ブロックでunsafeな関数を呼び出すことで、
この関数のドキュメンテーションを読み、関数の契約を守っているという責任を取ると宣言します。
こちらは、本体で何もしないdangerous
というunsafeな関数です:
# #![allow(unused_variables)] #fn main() { unsafe fn dangerous() {} unsafe { dangerous(); } #}
個別のunsafe
ブロックでdangerous
関数を呼ばなければなりません。unsafe
ブロックなしでdangerous
を呼ぼうとすれば、
エラーになるでしょう:
error[E0133]: call to unsafe function requires unsafe function or block
(エラー: unsafe関数の呼び出しには、unsafeな関数かブロックが必要です)
-->
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
dangerous
への呼び出しの周りにunsafe
ブロックを挿入することで、コンパイラに関数のドキュメンテーションを読み、
適切に使用する方法を理解したことをアサートし、関数の契約を満たしていると実証しました。
unsafe関数の本体は、実効的にunsafe
ブロックになるので、unsafe関数内でunsafeな別の処理を行うのに、
別のunsafe
ブロックは必要ないのです。
unsafeコードに安全な抽象を行う
関数がunsafeなコードを含んでいるだけで関数全体をunsafeでマークする必要があることにはなりません。
事実、安全な関数でunsafeなコードをラップすることは一般的な抽象化です。例として、
なんらかのunsafeコードが必要になる標準ライブラリの関数split_at_mut
を学び、その実装方法を探究しましょう。
この安全なメソッドは、可変なスライスに定義されています: スライスを1つ取り、引数で与えられた添え字でスライスを分割して2つにします。
リスト19-4は、split_at_mut
の使用法を示しています。
# #![allow(unused_variables)] #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]); #}
この関数をsafe Rustだけを使用して実装することはできません。試みは、リスト19-5のようになる可能性がありますが、コンパイルできません。
簡単のため、split_at_mut
をメソッドではなく関数として実装し、ジェネリックな型T
ではなく、i32
のスライス用に実装します。
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid],
&mut slice[mid..])
}
この関数はまず、スライスの全体の長さを得ます。それから引数で与えられた添え字が長さ以下であるかを確認してスライス内にあることをアサートします。 このアサートは、スライスを分割する添え字よりも大きい添え字を渡したら、その添え字を使用しようとする前に関数がパニックすることを意味します。
そして、2つの可変なスライスをタプルで返します: 1つは元のスライスの最初からmid
添え字まで、
もう一方は、mid
からスライスの終わりまでです。
リスト19-5のコードのコンパイルを試みると、エラーになるでしょう。
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
(エラー: 一度に2回以上、`*slice`を可変で借用できません)
-->
|
6 | (&mut slice[..mid],
| ----- first mutable borrow occurs here
7 | &mut slice[mid..])
| ^^^^^ second mutable borrow occurs here
8 | }
| - first borrow ends here
Rustの借用チェッカーには、スライスの異なる部分を借用していることが理解できないのです; 同じスライスから2回借用していることだけ知っています。2つのスライスが被らないので、 スライスの異なる部分を借用することは、根本的に大丈夫なのですが、コンパイラはこれを知れるほど賢くありません。 プログラマにはコードが大丈夫とわかるのに、コンパイラにはわからないのなら、unsafeコードに手を伸ばすタイミングです。
リスト19-6はunsafe
ブロック、生ポインタ、unsafe関数への呼び出しをしてsplit_at_mut
の実装が動くようにする方法を示しています。
# #![allow(unused_variables)] #fn main() { use std::slice; fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = slice.len(); let ptr = slice.as_mut_ptr(); assert!(mid <= len); unsafe { (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid)) } } #}
第4章の「スライス型」節から、スライスはなんらかのデータへのポインタとスライスの長さであることを思い出してください。
len
メソッドを使用してスライスの長さを得て、as_mut_ptr
メソッドを使用してスライスの生ポインタにアクセスしています。
この場合、i32
値の可変スライスがあるので、as_mut_ptr
は型*mut i32
の生ポインタを返し、これを変数ptr
に格納しました。
mid
添え字がスライス内にあるかというアサートを残しています。そして、unsafeコードに到達します:
slice::from_raw_parts_mut
関数は、生ポインタと長さを取り、スライスを生成します。この関数を使って、
ptr
から始まり、mid
の長さのスライスを生成しています。それからptr
にmid
を引数としてoffset
メソッドを呼び出し、
mid
で始まる生ポインタを得て、そのポインタとmid
の後の残りの要素数を長さとして使用してスライスを生成しています。
関数slice::from_raw_parts_mut
は、unsafeです。何故なら、生ポインタを取り、このポインタが有効であることを信用しなければならないからです。
生ポインタのoffset
メソッドもunsafeです。オフセット位置もまた有効なポインタであることを信用しなければならないからです。
故に、slice::from_raw_parts_mut
とoffset
を呼べるように、その呼び出しの周りにunsafe
ブロックを置かなければならなかったのです。
コードを眺めてmid
がlen
以下でなければならないとするアサートを追加することで、
unsafe
ブロック内で使用されている生ポインタが全てスライス内のデータへの有効なポインタであることがわかります。
これは、受け入れられ、適切なunsafe
の使用法です。
できあがったsplit_at_mut
関数をunsafe
でマークする必要はなく、この関数をsafe Rustから呼び出せることに注意してください。
unsafe
コードを安全に使用する関数の実装で、unsafeコードへの安全な抽象化を行いました。
この関数がアクセスするデータからの有効なポインタだけを生成するからです。
対照的に、リスト19-7のslice::from_raw_parts_mut
の使用は、スライスが使用されるとクラッシュする可能性が高いでしょう。
このコードは任意のメモリアドレスを取り、10,000要素の長さのスライスを生成します:
# #![allow(unused_variables)] #fn main() { use std::slice; let address = 0x012345usize; let r = address as *mut i32; let slice = unsafe { slice::from_raw_parts_mut(r, 10000) }; #}
この任意の場所のメモリは所有していなく、このコードが生成するスライスに有効なi32
値が含まれる保証もありません。
slice
を有効なスライスであるかのように使用しようとすると、未定義動作に陥ります。
extern
関数を使用して、外部のコードを呼び出す
時として、自分のRustコードが他の言語で書かれたコードと相互作用する必要が出てくる可能性があります。このために、
Rustにはextern
というキーワードがあり、これは、
FFI(Foreign Function Interface: 外部関数インターフェイス)の生成と使用を容易にします。
FFIは、あるプログラミング言語に関数を定義させ、異なる(外部の)プログラミング言語にそれらの関数を呼び出すことを可能にする方法です
リスト19-8は、Cの標準ライブラリからabs
関数を統合するセットアップ方法をデモしています。
extern
ブロック内で宣言された関数は、常にRustコードから呼ぶにはunsafeになります。理由は、
他の言語では、Rustの規則や保証が強制されず、コンパイラもチェックできないので、
安全性を保証する責任はプログラマに降りかかるのです。
ファイル名: src/main.rs
extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { // -3の絶対値は、Cによると{} println!("Absolute value of -3 according to C: {}", abs(-3)); } }
extern "C"
ブロック内で他の言語から呼び出した関数の名前とシグニチャを列挙します。"C"
の部分は、
外部関数がどのABI(application binary interface: アプリケーション・バイナリ・インターフェイス)を使用しているか定義します:
ABIは関数の呼び出し方法をアセンブリレベルで定義します。"C"
ABIは最も一般的でCプログラミング言語のABIに従っています。
他の言語からRustの関数を呼び出す
また、
extern
を使用して他の言語にRustの関数を呼ばせるインターフェイスを生成することもできます。extern
ブロックの代わりに、extern
キーワードを追加し、fn
キーワードの直前に使用するABIを指定します。 さらに、#[no_mangle]
注釈を追加してRustコンパイラに関数名をマングルしないように指示する必要もあります。 マングルとは、コンパイラが関数に与えた名前を他のコンパイル過程の情報をより多く含むけれども、人間に読みにくい異なる名前にすることです。 全ての言語のコンパイラは、少々異なる方法でマングルを行うので、Rustの関数が他の言語で名前付けできるように、 Rustコンパイラの名前マングルをオフにしなければならないのです。以下の例では、共有ライブラリにコンパイルし、Cからリンクした後に
call_from_c
関数をCコードからアクセスできるようにしています:# #![allow(unused_variables)] #fn main() { #[no_mangle] pub extern "C" fn call_from_c() { // CからRust関数を呼び出したばかり! println!("Just called a Rust function from C!"); } #}
この
extern
の使用法では、unsafe
は必要ありません。
可変で静的な変数にアクセスしたり、変更する
今までずっと、グローバル変数について語りませんでした。グローバル変数をRustは確かにサポートしていますが、 Rustの所有権規則で問題になることもあります。2つのスレッドが同じ可変なグローバル変数にアクセスしていたら、 データ競合を起こすこともあります。
Rustでは、グローバル変数は、static(静的)変数と呼ばれます。リスト19-9は、 値として文字列スライスのある静的変数の宣言例と使用を示しています。
ファイル名: src/main.rs
static HELLO_WORLD: &str = "Hello, world!"; fn main() { // 名前は: {} println!("name is: {}", HELLO_WORLD); }
静的変数は、定数に似ています。定数については、第3章の「変数と定数の違い」節で議論しました。
静的変数の名前は慣習でSCREAMING_SNAKE_CASE
(直訳
: 叫ぶスネークケース)になり、変数の型を注釈しなければなりません。
この例では&'static str
です。静的変数は、'static
ライフタイムの参照のみ格納でき、
これは、Rustコンパイラがライフタイムを推量できることを意味します; 明示的に注釈する必要はありません。
不変で静的な変数にアクセスすることは安全です。
定数と不変で静的な変数は、類似して見える可能性がありますが、微妙な差異は、 静的変数の値は固定されたメモリアドレスになることです。値を使用すると、常に同じデータにアクセスします。 一方、定数は使用される度にデータを複製させることができます。
定数と静的変数の別の違いは、静的変数は可変にもなることです。可変で静的な変数にアクセスし変更することは、unsafeです。
リスト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); } }
普通の変数同様、mut
キーワードを使用して可変性を指定します。COUNTER
を読み書きするコードはどれも、unsafe
ブロックになければなりません。
シングルスレッドなので、このコードは想定通り、コンパイルでき、COUNTER: 3
と出力します。
複数のスレッドにCOUNTER
にアクセスさせると、データ競合になる可能性が高いでしょう。
グローバルにアクセス可能な可変なデータがあると、データ競合がないことを保証するのは難しくなり、そのため、 Rustは可変で静的な変数をunsafeと考えるのです。可能なら、コンパイラが、データが異なるスレッドからアクセスされることが安全に行われているかを確認するように、 第16章で議論した並行性テクニックとスレッド安全なスマートポインタを使用するのが望ましいです。
unsafeなトレイトを実装する
unsafe
でのみ動く最後の行動は、unsafeなトレイトを実装することです。少なくとも、1つのメソッドにコンパイラが確かめられないなんらかの不変条件があると、
トレイトはunsafeになります。trait
の前にunsafe
キーワードを追加し、トレイトの実装もunsafe
でマークすることで、
トレイトがunsafe
であると宣言できます。リスト19-11のようにですね。
# #![allow(unused_variables)] #fn main() { unsafe trait Foo { // methods go here // メソッドがここに来る } unsafe impl Foo for i32 { // method implementations go here // メソッドの実装がここに来る } #}
unsafe impl
を使用することで、コンパイラが確かめられない不変条件を守ることを約束しています。
例として、第16章の「Sync
とSend
トレイトで拡張可能な並行性」節で議論したSync
とSend
マーカートレイトを思い出してください:
型が完全にSend
とSync
型だけで構成されていたら、コンパイラはこれらのトレイトを自動的に実装します。
生ポインタなどのSend
やSync
でない型を含む型を実装し、その型をSend
やSync
でマークしたいなら、
unsafe
を使用しなければなりません。コンパイラは、型がスレッド間を安全に送信できたり、
複数のスレッドから安全にアクセスできるという保証を保持しているか確かめられません; 故に、そのチェックを手動で行い、
unsafe
でそのように示唆する必要があります。
いつunsafeコードを使用するべきか
unsafe
を使って議論したばかりの4つの行動(強大な力)のうちの1つを行うのは間違っていたり、認められさえもしないものではありません。
ですが、unsafe
コードを正しくするのは、より巧妙なことでしょう。コンパイラがメモリ安全性を保持する手助けをできないからです。
unsafe
コードを使用する理由があるなら、そうすることができ、明示的にunsafe
注釈をすることで問題が起きたら、
その原因を追求するのが容易になります。