panic!で回復不能なエラー

時として、コードで悪いことが起きるものです。そして、それに対してできることは何もありません。 このような場面で、Rustにはpanic!マクロが用意されています。panic!マクロが実行されると、 プログラムは失敗のメッセージを表示し、スタックを巻き戻し掃除して、終了します。これが最もありふれて起こるのは、 何らかのバグが検出された時であり、プログラマには、どうエラーを処理すればいいか明確ではありません。

パニックに対してスタックを巻き戻すか異常終了するか

標準では、パニックが発生すると、プログラムは巻き戻しを始めます。つまり、言語がスタックを遡り、 遭遇した各関数のデータを片付けるということです。しかし、この遡りと片付けはすべきことが多くなります。 対立案は、即座に異常終了し、片付けをせずにプログラムを終了させることです。そうなると、プログラムが使用していたメモリは、 OSが片付ける必要があります。プロジェクトにおいて、実行可能ファイルを極力小さくする必要があれば、 Cargo.tomlファイルの適切な[profile]欄にpanic = 'abort'を追記することで、 パニック時に巻き戻しから異常終了するように切り替えることができます。例として、 リリースモード時に異常終了するようにしたければ、以下を追記してください:

[profile.release]
panic = 'abort'

単純なプログラムでpanic!の呼び出しを試してみましょう:

ファイル名: src/main.rs

fn main() {
    panic!("crash and burn");  //クラッシュして炎上
}

このプログラムを実行すると、以下のような出力を目の当たりにするでしょう:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25 secs
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:4
('main'スレッドはsrc/main.rs:2:4の「クラッシュして炎上」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.

panic!の呼び出しが、最後の2行に含まれるエラーメッセージを発生させているのです。 1行目にパニックメッセージとソースコード中でパニックが発生した箇所を示唆しています: src/main.rs:2:4は、src/main.rsファイルの2行目4文字目であることを示しています。

この場合、示唆される行は、自分のコードの一部で、その箇所を見に行けば、panic!マクロ呼び出しがあるわけです。 それ以外では、panic!呼び出しが、自分のコードが呼び出しているコードの一部になっている可能性もあるわけです。 エラーメッセージで報告されるファイル名と行番号が、結果的にpanic!呼び出しに導いた自分のコードの行ではなく、 panic!マクロが呼び出されている他人のコードになるでしょう。panic!呼び出しの発生元である関数のバックトレースを使用して、 問題を起こしている自分のコードの箇所を割り出すことができます。バックトレースがどんなものか、次に議論しましょう。

panic!バックトレースを使用する

別の例を眺めて、自分のコードでマクロを直接呼び出す代わりに、コードに存在するバグにより、 ライブラリでpanic!呼び出しが発生するとどんな感じなのか確かめてみましょう。リスト9-1は、 添え字でベクタの要素にアクセスを試みる何らかのコードです。

ファイル名: src/main.rs

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

リスト9-1: ベクタの境界を超えて要素へのアクセスを試み、panic!の呼び出しを発生させる

ここでは、ベクタの100番目の要素(添え字は0始まりなので添え字99)にアクセスを試みていますが、ベクタには3つしか要素がありません。 この場面では、Rustはパニックします。[]の使用は、要素を返すと想定されるものの、 無効な添え字を渡せば、ここでRustが返せて正しいと思われる要素は何もないわけです。

他の言語(Cなど)では、この場面で欲しいものではないにもかかわらず、まさしく要求したものを返そうとしてきます: メモリがベクタに属していないにもかかわらず、ベクタ内のその要素に対応するメモリ上の箇所にあるものを何か返してくるのです。 これは、バッファー外読み出し(buffer overread; 訳注: バッファー読みすぎとも解釈できるか)と呼ばれ、 攻撃者が、配列の後に格納された読めるべきでないデータを読み出せるように添え字を操作できたら、 セキュリティ脆弱性につながる可能性があります。

この種の脆弱性からプログラムを保護するために、存在しない添え字の要素を読もうとしたら、 Rustは実行を中止し、継続を拒みます。試して確認してみましょう:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
99', /checkout/src/liballoc/vec.rs:1555:10
('main'スレッドは、/checkout/src/liballoc/vec.rs:1555:10の
「境界外番号: 長さは3なのに、添え字は99です」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.

このエラーは、自分のファイルではないvec.rsファイルを指しています。 標準ライブラリのVec<T>の実装です。ベクタvに対して[]を使った時に走るコードは、 vec.rsに存在し、ここで実際にpanic!が発生しているのです。

その次の注釈行は、RUST_BACKTRACE環境変数をセットして、まさしく何が起き、 エラーが発生したのかのバックトレースを得られることを教えてくれています。 バックトレースとは、ここに至るまでに呼び出された全関数の一覧です。Rustのバックトレースも、 他の言語同様に動作します: バックトレースを読むコツは、頭からスタートして自分のファイルを見つけるまで読むことです。 そこが、問題の根源になるのです。自分のファイルを言及している箇所以前は、自分のコードで呼び出したコードになります; 以後は、自分のコードを呼び出しているコードになります。これらの行には、Rustの核となるコード、標準ライブラリのコード、 使用しているクレートなどが含まれるかもしれません。RUST_BACKTRACE環境変数を0以外の値にセットして、 バックトレースを出力してみましょう。リスト9-2のような出力が得られるでしょう。

$ RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10
stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
             at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
   1: std::sys_common::backtrace::_print
             at /checkout/src/libstd/sys_common/backtrace.rs:71
   2: std::panicking::default_hook::{{closure}}
             at /checkout/src/libstd/sys_common/backtrace.rs:60
             at /checkout/src/libstd/panicking.rs:381
   3: std::panicking::default_hook
             at /checkout/src/libstd/panicking.rs:397
   4: std::panicking::rust_panic_with_hook
             at /checkout/src/libstd/panicking.rs:611
   5: std::panicking::begin_panic
             at /checkout/src/libstd/panicking.rs:572
   6: std::panicking::begin_panic_fmt
             at /checkout/src/libstd/panicking.rs:522
   7: rust_begin_unwind
             at /checkout/src/libstd/panicking.rs:498
   8: core::panicking::panic_fmt
             at /checkout/src/libcore/panicking.rs:71
   9: core::panicking::panic_bounds_check
             at /checkout/src/libcore/panicking.rs:58
  10: <alloc::vec::Vec<T> as core::ops::index::Index<usize>>::index
             at /checkout/src/liballoc/vec.rs:1555
  11: panic::main
             at src/main.rs:4
  12: __rust_maybe_catch_panic
             at /checkout/src/libpanic_unwind/lib.rs:99
  13: std::rt::lang_start
             at /checkout/src/libstd/panicking.rs:459
             at /checkout/src/libstd/panic.rs:361
             at /checkout/src/libstd/rt.rs:61
  14: main
  15: __libc_start_main
  16: <unknown>

リスト9-2: RUST_BACKTRACE環境変数をセットした時に表示される、 panic!呼び出しが生成するバックトレース

出力が多いですね!OSやRustのバージョンによって、出力の詳細は変わる可能性があります。この情報とともに、 バックトレースを得るには、デバッグシンボルを有効にしなければなりません。デバッグシンボルは、 --releaseオプションなしでcargo buildcargo runを使用していれば、標準で有効になり、 ここではそうなっています。

リスト9-2の出力で、バックトレースの11行目が問題発生箇所を指し示しています: src/main.rsの4行目です。 プログラムにパニックしてほしくなければ、自分のファイルについて言及している最初の行で示されている箇所が、 どのようにパニックを引き起こす値でこの箇所にたどり着いたか割り出すために調査を開始すべき箇所になります。 バックトレースの使用法を模擬するためにわざとパニックするコードを書いたリスト9-1において、 パニックを解消する方法は、3つしか要素のないベクタの添え字99の要素を要求しないことです。 将来コードがパニックしたら、パニックを引き起こすどんな値でコードがどんな動作をしているのかと、 代わりにコードは何をすべきなのかを算出する必要があるでしょう。

この章の後ほど、「panic!するかpanic!するまいか」節でpanic!とエラー状態を扱うのにpanic!を使うべき時と使わぬべき時に戻ってきます。 次は、Resultを使用してエラーから回復する方法を見ましょう。