この資料について
この資料は「Rust Book 勉強会 #9」用の発表資料です。
- 担当: Yuya Kato (Nayutaya Inc.)
- 範囲: 第19章「Advanced Features」 第1節「Unsafe Rust」
- 原文: Unsafe Rust - The Rust Programming Language
- 日本語版: Unsafe Rust - The Rust Programming Language
unsafe Rust
safe Rustからunsafe Rustへ
本節のテーマはずばり「unsafe」です。
これまで見てきたコードはすべて、コンパイル時に働くメモリ安全機構の枠の中で書かれたものでした。
しかしRustには、これらのメモリ安全機構が強制されない第2の言語「unsafe Rust」が隠されています。
unsafe Rustは普通のRust(safe Rust)のように働きますが、safe Rustにはない「強大な力」(superpowers)を与えてくれます。
なぜunsafe Rustが必要か? (1/2)
なぜsafeなRustの中に、unsafeなRustが必要なのでしょうか?
Rust処理系の開発者はとても賢く、精力的だと思いますが、それでもなおコンパイラではチェックできない事柄は沢山あります。また、静的解析は原理的に保守的です。
コードが「ある保証」を保持しているかどうかコンパイラが決定しようとする際、「なんらかの不正なプログラム」を受け入れるよりも、「合法なプログラム」を拒否する方がより安全です。
人間から見てコードは論理的に問題無い場合でも、コンパイラがそれを理解できない場合、それは拒絶されるとも言えます。
なぜunsafe Rustが必要か? (2/2)
そのような場合にunsafe Rustを使うことになります。unsafeなコードであることを明示し、コンパイラに「私を信じて!コードが何をしているか分かっているよ!」と伝えることができます。
unsafe Rustが存在する別の理由は、コンピュータのハードウェアが本質的にunsafeだからです。Rustでunsafeな処理を行うことができない場合、ある種の処理(例えばOSと直接やり取りしたり、OS自体を記述すること)が行えません。
低レベル(低抽象度)のシステムプログラミングを行えることは、Rustの目標の1つなのです。
unsafeの強大な力
safe Rustをunsafe Rustに切り替えるのは簡単です。unsafe
キーワードとそれに続くコードブロックを書くだけです。
safe Rustでは行えない4つの行動をunsafe Rustでは行うことができます。これは「安全でない強大な力」(unsafe superpowers)と呼ばれます。具体的には以下の4つです。
- 生ポインタの参照を外すこと(Dereferenceすること)
- unsafeな関数を呼ぶこと
- 可変で静的な変数を読み書きすること
- unsafeなトレイトを実装すること
強大な力の前に: unsafeの中のsafe
unsafe
ブロックの中でも借用チェッカやその他の安全性チェックは依然として有効です。unsafe
はこれらのチェックを無効化するものではありません。
そのためunsafe
ブロックの中でも、ある程度の安全性は保たれます。
強大な力の前に: unsafeを小さくする/隔離する
unsafe
ブロックの外側は基本的には安全(safe Rust)であるため、メモリ安全性に関するエラーはすべてunsafe
ブロックの中にあると言えます。不具合の調査を容易にするため、unsafe
ブロックは可能な限り小さくしましょう。
また、unsafeなコードを可能な限り分離するために、unsafeなコードをsafeな抽象の中に閉じ込めましょう。これについては後述します。
それでは4つの「強大な力」を見ていきましょう!
1. 生ポインタの参照を外す
生ポインタ
第4章の「Dangling References」節で、コンパイラは参照が常に有効であることを確認することに触れました。safe Rustで使える「参照」に加え、unsafe Rustでは参照に類似した「生ポインタ」(raw pointers)を使うことができます。
生ポインタは、以下の2つの型で表現されます。
-
*const T
:T
型の不変の生ポインタ。 -
*mut T
:T
型の可変の生ポインタ。
*
は参照外し演算子(dereference operator)ではなく、型名の一部です。また、この文脈における「不変」は、参照外し後に直接代入できないことを意味します。
生ポインタと参照の違い
生ポインタは参照やスマートポインタと異なり、以下の特性を持ちます。
- 同じ場所への不変なポインタと可変なポインタが同時に存在できます。また、複数の可変なポインタも同時に存在できます。(つまり所有権規則をガン無視です)
- 有効なメモリ領域を指していることが保証されません。
- nullである可能性があります。
- 自動的なクリーンアップは実装されていません。
これらの抜け道を作ることで、安全性を諦めてパフォーマンスを得たり、他の言語とのインターフェース能力を獲得することができます。
生ポインタの取得 (1/2)
参照から生ポインタを取得する例を以下に示します。
// Listing 19-1: Creating raw pointers from references
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
上記コードにはunsafe
キーワードが含まれていません。つまり「生ポインタを取得すること」自体はsafe Rustの範疇です。生ポインタは参照外しされない限り、危険ではありません。
生ポインタの取得 (2/2)
前ページの例では、有効な参照から生ポインタにキャストしたため、生ポインタの指すメモリ領域は有効でした。
別の方法として、アドレスを直接して生ポインタを得ることもできます。これもまた、safe Rustの範疇です。
// Listing 19-2: Creating a raw pointer to an arbitrary memory address
let address = 0x012345usize;
let r = address as *const i32;
ただし、ほとんどの場合、上記のアドレスは有効なメモリ領域を指しません。この生ポインタを参照外しした時の挙動は未定義です。
生ポインタの参照外し
生ポインタの取得はsafe Rustでも行えますが、生ポインタの参照外し(Dereference)にはunsafe Rustの「強大な力」が必要です。つまり、unsafe
ブロックで括る必要があります。
その上で「参照外し演算子」(dereference operator)*
を使うことで、生ポインタの参照を外し、実体にアクセスすることができます。
// Listing 19-3: Dereferencing raw pointers within an unsafe block
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);
}
上記のコードで不変と可変の生ポインタを取得、参照外ししていることに注目してください。参照では所有権規則に反するため、コンパイルエラーとなります。
生ポインタを使うのはどんな時?
代表的なケースは、以下の2つです。
- 他のプログラミング言語(C言語など)とインターフェースする場合(次項で詳しく述べます)
- 借用チェッカには理解できない安全な抽象を構成する場合
- (Another case is when building up safe abstractions that the borrow checker doesn’t understand. )
2. unsafeな関数を呼び出す
unsafeな関数
生ポインタの参照外しに続き、unsafe
ブロックが必要になる2つ目の処理は、unsafe関数の呼び出しです。
unsafeな関数は、unsafe
キーワードがfn
の前にあることを除いては普通の関数に見えます。この文脈でのunsafe
キーワードは、関数に「何らかの契約」「前提条件」があることを示します。
その「契約」「条件」について、コンパイラは何ら保証を与えることができません。
unsafe関数をunsafe
ブロック内で呼び出すことで、人間がその「契約」「条件」を守っていると宣言することができます。
unsafe
キーワードが付与された関数の呼び出し
危険なunsafe関数の呼び出し例を以下に示します。
unsafe fn dangerous() {}
unsafe {
dangerous();
}
なおunsafe関数内はunsafe
ブロックと同等です。そのため、他のunsafe関数をそのまま呼び出すことができます。
unsafe
ブロック外でのunsafe関数の呼び出し
もしunsafe
ブロック外で呼び出した場合、以下の様にエラーになります。
error[E0133]: call to unsafe function requires unsafe function or block
-->
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
unsafeコードの安全な抽象化
unsafeなコードを安全なコードでラップすることはよく行われる抽象化の1つです。例として標準ライブラリで提供されているsplit_at_mut
関数を見てみましょう。
この安全な関数は、可変なスライスに定義されています。スライスを1つ取り、引数で与えられた位置でスライスを2つに分割します。以下に使用例を示します。
// Listing 19-4: Using the safe split_at_mut function
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]);
split_at_mut
関数をsafe Rustで実装する (1/2)
split_at_mut
のsafe Rustによる実装例を以下に示します。ただ、残念ながらこのコードはコンパイルエラーとなります。
// Listing 19-5: An attempted implementation of split_at_mut using only safe Rust
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..])
}
この関数はまず、指定された位置がスライスの長さ以下であることを確認します。さもなければパニックします。
続いてmid
の位置でスライスを2つに分割します。ですが、次の通りエラーとなります。
split_at_mut
関数をsafe Rustで実装する (2/2)
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
-->
|
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つのスライスが元のスライスの別々の範囲を指していることを理解することができません。被らない範囲を借用することは、根本的には安全なはずなのに!
split_at_mut
関数をunsafe Rustで実装する
ではunsafe
を使って実装してみましょう。このコードでは生ポインタ、unsafeなslice::from_raw_parts_mut
関数、offset
関数を使っています。
// Listing 19-6: Using unsafe code in the implementation of the split_at_mut function
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))
}
}
unsafeなコードを使用してできあがったsplit_at_mut
関数がunsafe
とマークされていないことに注目しましょう。生ポインタの扱いとunsafe関数の呼び出しは適切に設計されており、このコードは安全にsafe Rustから呼び出すことができます。
slice::from_raw_parts_mut
関数を使用する不適切な例
参考に、slice::from_raw_parts_mut
関数を使用する不適切な例を以下に示します。
このコードが生成するスライスには有効な値が含まれる保証はありません。このスライスを使用した場合は、未定義動作に陥ります。
// Listing 19-7: Creating a slice from an arbitrary memory location
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let slice: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 10000)
};
extern
キーワードが付与された関数の呼び出し
時として、他のプログラミング言語で書かれたコードとRustを連携させる必要があります。代表的な例はC言語との連携でしょう。
このためにRustにはextern
というキーワードがあり、これによりFFI(Foreign Function Interface: 外部関数インターフェース)を容易にします。
以下に、C言語の標準ライブラリに含まれるabs
関数を呼び出す例を示します。extern
ブロック内で宣言された関数はunsafeであり、コンパイラはいかなる規則や保証を強制することができません。これらはプログラマの責任です。
// Listing 19-8: Declaring and calling an extern function defined in another language
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
※ ABIの種類
extern
キーワードには、C
の他、以下のような文字列を指定することができます。
aapcs
、cdecl
、fastcall
、platform-intrinsic
、Rust
、rust-call
、rust-intrinsic
、stdcall
、system
、sysv64
、vectorcall
、win64
参考: External blocks - The Rust Reference
他の言語からRustの関数を呼び出す
先ほどの例はC言語の関数をRustから呼ぶものでした。それとは逆に、extern
キーワードを使ってRustの関数を他の言語から呼び出せるようにすることもできます。
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
ここで重要なのはextern
キーワードと、#[no_mangle]
注釈です。この注釈は、Rustコンパイラに関数名をマングルしないように指示します。
3. 可変で静的な変数を読み込む/書き込む
Rustにおけるグローバル変数
ここまでのところ「グローバル変数」について紹介しませんでした。Rustは確かにグローバル変数をサポートしますが、これは所有権規則上の問題児です。
2つの異なるスレッドが同じ可変なグローバル変数にアクセスした場合、容易にデータ競合を起こします。
Rustでは、グローバル変数は「静的変数」(static variables)と呼ばれます。以下に例を示します。
// Listing 19-9: Defining and using an immutable static variable
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静的変数の名前は、慣習でSCREAMING_SNAKE_CASE
(大文字、アンダースコア区切り)を用い、必ず型アノテーションが必要です。ここでのHELLO_WORLD
は&'static str
型ですが、ライフタイムの'static
は推論されます。
不変の静的変数は定数と似ていますが、常に同じアドレスを指す点が異なります。(定数はコピーされることがあります)
可変な静的変数
先ほどの例の静的変数は不変でしたが、可変の静的変数を作ることもできます。不変の静的変数とは異なり、可変の静的変数は書き込みはおろか、読み込みすらunsafeです。以下に例を示します。
// Listing 19-10: Reading from or writing to a mutable static variable is unsafe
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
この例では1つのスレッドからしかCOUNTER
にアクセスしないため、想定通りに動作します。もし複数スレッドからCOUNTER
にアクセスすると、容易にデータ競合を起こします。そのため、Rustでは可変な静的変数はunsafeとして扱われます。
もし安全に複数のスレッドからアクセスした場合、第16章で紹介した並列性テクニックとスレッド安全なスマートポインタを用いましょう。
4. unsafeなトレイトを実装する
unsafeなトレイトを実装する
最後の「強大な力」は、unsafeなトレイトを実装することです。コンパイラが確かめられない不変条件が1つでも含まれていると、トレイトはunsafeになります。
unsafeなトレイトの例を以下に示します。
// Listing 19-11: Defining and implementing an unsafe trait
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
具体例: Sync
とSend
第16章で紹介したSync
トレイト、Send
トレイトを思い出してください。ある型が完全にSend
とSync
型で構成されている場合、コンパイラはこれらのトレイトを自動的に実装します。
生ポインタなどのSend
やSync
でない型を含んでいる型を実装し、その型をSend
やSync
でマークしたいなら、unsafe
を使用しなければなりません。
残念ながらコンパイラは、その型が複数のスレッド間で安全に送信できたり、安全にアクセスできるという保証を確かめられません。それらは人間が行い、unsafe
でそれを示唆する必要があります。
いつunsafeなコードを使うべきか?
いつunsafeなコードを使うべきか?
これまでに述べた通り、unsafe Rustではsafe Rustにない強大な力を手に入れることができ、そのおかげで多くのことを行うことができます。unsafeコードを使用する理由があるのであれば、そうするべきです。
unsafeコードを使用するのは危険を伴います。正しく安全なunsafeコードを書くのは難しいことですが、不可能ではありません。
例え問題が生じたとしても、問題は「unsafe」と明示されている箇所に存在する可能性が高く、原因の追及の手助けとなります。
用法用量を守って、正しく使いましょう!