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;
#}

リスト19-1: 参照から生ポインタを生成する

このコードにはunsafeキーワードを含めていないことに気付いてください。safeコードで生ポインタを生成できます; もうすぐわかるように、unsafeブロックの外では、生ポインタを参照外しできないだけなのです。

asを使って不変と可変な参照を対応する生ポインタの型にキャストして生ポインタを生成しました。 有効であることが保証される参照から直接生ポインタを生成したので、これらの特定の生ポインタは有効であることがわかりますが、 その前提をあらゆる生ポインタに敷くことはできません。

次に、有効であることが確信できない生ポインタを生成します。リスト19-2は、メモリの任意の箇所を指す生ポインタの生成法を示しています。 任意のメモリを使用しようとすることは未定義です: そのアドレスにデータがある可能性もあるし、ない可能性もあり、 コンパイラがコードを最適化してメモリアクセスがなくなる可能性もあるし、プログラムがセグメンテーションフォールトでエラーになる可能性もあります。 通常、このようなコードを書くいい理由はありませんが、可能ではあります。


# #![allow(unused_variables)]
#fn main() {
let address = 0x012345usize;
let r = address as *const i32;
#}

リスト19-2: 任意のメモリアドレスへの生ポインタを生成する

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-3: unsafeブロック内で生ポインタを参照外しする

ポインタの生成は害を及ぼしません; 無効な値を扱うことに落ち着く可能性のあるポインタが指している値にアクセスしようとする時のみです。

また、リスト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]);
#}

リスト19-4: 安全なsplit_at_mut関数を使用する

この関数を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..])
}

リスト19-5: safe Rustだけを使用したsplit_at_mutの未遂の実装

この関数はまず、スライスの全体の長さを得ます。それから引数で与えられた添え字が長さ以下であるかを確認してスライス内にあることをアサートします。 このアサートは、スライスを分割する添え字よりも大きい添え字を渡したら、その添え字を使用しようとする前に関数がパニックすることを意味します。

そして、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))
    }
}
#}

リスト19-6: split_at_mut関数の実装でunsafeコードを使用する

第4章の「スライス型」節から、スライスはなんらかのデータへのポインタとスライスの長さであることを思い出してください。 lenメソッドを使用してスライスの長さを得て、as_mut_ptrメソッドを使用してスライスの生ポインタにアクセスしています。 この場合、i32値の可変スライスがあるので、as_mut_ptrは型*mut i32の生ポインタを返し、これを変数ptrに格納しました。

mid添え字がスライス内にあるかというアサートを残しています。そして、unsafeコードに到達します: slice::from_raw_parts_mut関数は、生ポインタと長さを取り、スライスを生成します。この関数を使って、 ptrから始まり、midの長さのスライスを生成しています。それからptrmidを引数としてoffsetメソッドを呼び出し、 midで始まる生ポインタを得て、そのポインタとmidの後の残りの要素数を長さとして使用してスライスを生成しています。

関数slice::from_raw_parts_mutは、unsafeです。何故なら、生ポインタを取り、このポインタが有効であることを信用しなければならないからです。 生ポインタのoffsetメソッドもunsafeです。オフセット位置もまた有効なポインタであることを信用しなければならないからです。 故に、slice::from_raw_parts_mutoffsetを呼べるように、その呼び出しの周りにunsafeブロックを置かなければならなかったのです。 コードを観察してmidlen以下でなければならないとするアサートを追加することで、 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)
};
#}

リスト19-7: 任意のメモリアドレスからスライスを生成する

この任意の場所のメモリは所有していなく、このコードが生成するスライスに有効な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));
    }
}

リスト19-8: 他の言語で定義されたextern関数を宣言し、呼び出す

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);
}

リスト19-9: 不変で静的な変数を定義し、使用する

静的変数は、定数に似ています。定数については、第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);
    }
}

リスト19-10: 可変で静的な変数を読んだり、書き込むのはunsafeである

普通の変数同様、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
    // メソッドの実装がここに来る
}
#}

リスト19-11: unsafeなトレイトを定義して実装する

unsafe implを使用することで、コンパイラが確かめられない不変条件を守ることを約束しています。

例として、第16章の「SyncSendトレイトで拡張可能な並行性」節で議論したSyncSendマーカートレイトを思い出してください: 型が完全にSendSync型だけで構成されていたら、コンパイラはこれらのトレイトを自動的に実装します。 生ポインタなどのSendSyncでない型を含む型を実装し、その型をSendSyncでマークしたいなら、 unsafeを使用しなければなりません。コンパイラは、型がスレッド間を安全に送信できたり、 複数のスレッドから安全にアクセスできるという保証を保持しているか確かめられません; 故に、そのチェックを手動で行い、 unsafeでそのように示唆する必要があります。

いつunsafeコードを使用するべきか

unsafeを使って議論したばかりの4つの行動(superpower)のうちの1つを行うのは間違っていたり、認められもしないものではありません。 ですが、unsafeコードを正しくするのは、より巧妙なことでしょう。コンパイラがメモリ安全性を保持する手助けをできないからです。 unsafeコードを使用する理由があるなら、そうすることができ、明示的にunsafe注釈をすることで問題が起きたら、 その原因を追求するのが容易になります。