panic! による回復不能なエラー
コードの中では、ときどき良くないことが起こり、それに対して何もできない場合があります。こうしたケースのために、Rust には panic! マクロがあります。実際にパニックを発生させる方法は 2 つあります。コードがパニックするような操作を行うこと(たとえば、配列の末尾を超えてアクセスすること)と、panic! マクロを明示的に呼び出すことです。どちらの場合でも、プログラム内でパニックが発生します。デフォルトでは、これらのパニックは失敗メッセージを表示し、アンワインドし、スタックをクリーンアップして終了します。環境変数を使うことで、パニックが発生したときに Rust にコールスタックを表示させ、パニックの原因を追跡しやすくすることもできます。
パニックに応じたスタックのアンワインドまたはアボート
デフォルトでは、パニックが発生すると、プログラムは アンワインド を開始します。これは、Rust がスタックをさかのぼりながら、通過する各関数のデータをクリーンアップすることを意味します。しかし、さかのぼってクリーンアップするのはかなりの作業です。そのため Rust では、即座に アボート するという代替手段も選べます。これは、クリーンアップを行わずにプログラムを終了する方法です。
その場合、プログラムが使用していたメモリはオペレーティングシステムによってクリーンアップされる必要があります。プロジェクトで生成されるバイナリをできるだけ小さくしたい場合は、Cargo.toml ファイルの適切な
[profile]セクションにpanic = 'abort'を追加することで、パニック時の動作をアンワインドからアボートに切り替えられます。たとえば、リリースモードでパニック時にアボートしたい場合は、次のように追加します。[profile.release] panic = 'abort'
単純なプログラムで panic! を呼び出してみましょう。
fn main() {
panic!("crash and burn");
}
プログラムを実行すると、次のような出力が表示されます。
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic! の呼び出しによって、最後の 2 行に含まれるエラーメッセージが発生します。最初の行には、パニックメッセージと、パニックが発生したソースコード内の位置が表示されています。src/main.rs:2:5 は、src/main.rs ファイルの 2 行目 5 文字目であることを示しています。
この場合、示されている行は私たちのコードの一部であり、その行に移動すると panic! マクロの呼び出しが見つかります。別のケースでは、panic! の呼び出しが、私たちのコードが呼び出したコードの中にあることもあります。その場合、エラーメッセージで報告されるファイル名と行番号は、最終的に panic! 呼び出しに至った私たちのコードの行ではなく、panic! マクロが呼び出されている誰か別のコードの位置になります。
panic! 呼び出しの発生元となった関数のバックトレースを使うと、問題を引き起こしているコードの箇所を特定できます。panic! のバックトレースの使い方を理解するために、別の例を見てみましょう。今度は、コードが直接マクロを呼び出したのではなく、コード内のバグのためにライブラリから panic! が呼び出される場合です。リスト 9-1 には、有効なインデックス範囲を超えてベクタのインデックスにアクセスしようとするコードがあります。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
ここでは、ベクタの 100 番目の要素にアクセスしようとしています(インデックスは 0 から始まるため、位置としてはインデックス 99 です)が、そのベクタには要素が 3 つしかありません。この状況では、Rust はパニックします。[] は要素を返すことが想定されていますが、無効なインデックスを渡した場合、Rust がここで返せる正しい要素は存在しません。
C では、データ構造の末尾を超えて読み取ろうとすることは未定義動作です。そのデータ構造に属していないにもかかわらず、その要素に対応するメモリ位置にたまたまあるものを取得してしまうかもしれません。これは buffer overread と呼ばれ、攻撃者がインデックスを操作して、本来は許可されるべきではない、そのデータ構造の後ろに格納されているデータを読み取れる場合には、セキュリティ脆弱性につながる可能性があります。
この種の脆弱性からプログラムを保護するために、存在しないインデックスの要素を読み取ろうとすると、Rust は実行を停止し、続行を拒否します。試して確認してみましょう。
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
このエラーは、v のベクタのインデックス 99 にアクセスしようとしている main.rs の 4 行目を指しています。
note: 行は、RUST_BACKTRACE 環境変数を設定することで、何が起きてエラーに至ったのかを正確に示すバックトレースを取得できることを教えてくれます。バックトレース とは、この地点に到達するまでに呼び出されたすべての関数の一覧です。Rust のバックトレースは他の言語と同様に機能します。バックトレースを読む際の要点は、先頭から読み始めて、自分が書いたファイルが現れるまで進むことです。そこが問題の発生源です。その箇所より上の行は、あなたのコードが呼び出したコードです。下の行は、あなたのコードを呼び出したコードです。こうした前後の行には、Rust のコアコード、標準ライブラリのコード、あるいは使用しているクレートが含まれることがあります。RUST_BACKTRACE 環境変数を 0 以外の任意の値に設定して、バックトレースを取得してみましょう。リスト 9-2 に、表示されるものと似た出力を示します。
```console
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
すごい量の出力ですね! 実際に表示される出力は、使用している
オペレーティングシステムやRustのバージョンによって異なる場合があります。この
情報を含むバックトレースを取得するには、デバッグシンボルが有効になっている必要があります。デバッグシンボルは、
ここで行っているように、cargo build や cargo run を --release フラグなしで使用すると、
デフォルトで有効になります。
リスト9-2の出力では、バックトレースの6行目が、問題を引き起こしている プロジェクト内の行、つまり src/main.rs の4行目を指しています。もし プログラムをパニックさせたくないなら、私たちが書いたファイルに言及している最初の行が 指し示す場所から調査を始めるべきです。意図的に パニックするコードを書いたリスト9-1では、パニックを修正する方法は、 ベクターのインデックス範囲を超える要素を要求しないことです。将来、あなたのコードが パニックしたときには、そのパニックを引き起こすためにコードがどの値で どのような操作を行っているのか、そして代わりにコードが何をするべきかを 突き止める必要があります。
この章の後半にある
「panic! するべきか、しないべきか」 節で、
panic! と、エラー条件を処理するために panic! を使うべき場合と使うべきでない場合について
再び取り上げます。次に、Result を使ってエラーから回復する方法を見ていきます。