パニック原因を調査する

Rustプログラムで発生したパニックの原因をデバッグして調べる方法を解説します。

パニック戦略として unwind が選択されている場合、パニックが発生しても std::panic::catch_unwind() などで捕捉してプログラムの実行を継続できますが、実行の中断が遅れるため、デバッガで原因を調べる妨げとなる場合があります。そこで、ここではRust標準ライブラリ関数にブレークポイントを設定することでパニック発生箇所で実行を中断する方法を紹介します。

パニックとは?

パニック (panic) [Rust APIドキュメント] はRustのエラー処理機構の一つで、通常フローでは復旧する手段がない時に用いられます。例えば:

  • 42u32 / 0u32 型の値を求める式ですが、整数のゼロ除算に対して解を与えることはできません。このため、式の評価は失敗し、パニックが発生します。

  • option_value.unwrap()option_valueSome(x) の場合は x を返し、None の場合はパニックします。

  • &array[i] は有効な参照を返しますが、 i が境界外のときは返せる参照が存在しないのでパニックします。

  • assert!(condition)panic!("message") などで明示的にパニックを発生させることもできます。

パニックはバグが原因でプログラムの事前条件が満たされなかった場合のみに発生させるのが慣習で、ユーザが入力したデータのパース処理やネットワーク処理など「失敗する可能性があることが前提」のものには使用しません。

パニックが発生した場合の処理はパニック戦略コンパイルオプションによって変わります。パニック戦略として unwind を指定すればに発生したパニックを呼出し階層内の上位の関数で捕捉し、復旧を試みることができます。しかし、これには実行時・メモリ・コードサイズオーバヘッドが伴い、またパニック発生したということはバグが原因でプログラムが想定していない状態になっていることを意味するため、継続して実行しても正常動作するとは限らないことに注意する必要があります。このため、パニック戦略として abort を選択し、無条件にアボートハンドラに制御を移すのも有効なデザインアプローチです。

  1. Rustプログラムを作成します。ここでは例として以下のバグのあるコードを使用します。

    std::thread::spawn(|| {
        let array: [u16; 6] = std::array::from_fn(|i| i.wrapping_pow(7) as u16);
        let is_sorted = array.windows(2).all(|pair| pair[0] < pair[1]);
        assert!(is_sorted);
    })
    .join()
    .unwrap();
    

  1. このコードをIDEから実行するとパニックが原因で実行が中断されます。しかし、パニックは実際には std::thread::spawn() によって一度捕捉されてから JoinHandle::join() の呼出し元に伝搬され、Result::unwrap() 呼出しによってはじめてアボートに昇格し、ここで実行が中断されるため、この時点の呼び出し履歴を辿っても原因に辿り着くことはできません。

    ../../_images/panic-forwarded.png

  1. ブレークポイント ウィンドウから 新規作成 ‣ 関数でブレーク(F) を選択し、関数名 rust_panic を指定してブレークポイントを作成します。

    ../../_images/rust-panic-breakpoint.png

  1. この状態で再度プログラムを実行すると、パニックが発生した時点でプログラムの実行が停止します。呼び出し履歴を辿ることでパニック発生時のローカル変数の値を観察することができます。この例では、変数 array の内容が昇順に並んでいなかったため assert!(is_sorted) でアサーションに失敗したことが分かります。

    ../../_images/rust-panic-breakpoint-hit.png