Kgtkr's Blog

Rustのcatch_unwindでスタックトレースを取得する

2019/12/09
rust

Rustにはcatch_unwindという関数がありpanicをキャッチすることが出来ます。
Rustのパニック機構 - 簡潔なQ

しかしここから得られるエラー情報がかなり少ないです。キャッチして得られるResultのError型はBox<dyn Any>みたいな型ですが、これがほとんどStringまたは&strとなっており(今の所他の型を見ていませんが詳しい方教えて下さい)、確認した所スタックトレース情報やパニックが発生したソースの位置情報は入っていませんでした。例えば以下のコードはpanic testと出力します。

if let Err(e) = panic::catch_unwind(move || {
    panic!("panic test");
}) {
    println!("{}", any_to_string(&*e));
}

fn any_to_string(any: &dyn std::any::Any) -> String {
    if let Some(s) = any.downcast_ref::<String>() {
        s.clone()
    } else if let Some(s) = any.downcast_ref::<&str>() {
        s.to_string()
    } else {
        "Any".to_string()
    }
}

これではスタックトレース情報が取れないのでlazy_staticなどと組み合わせて以下のようにしたら上手くいきました。

lazy_static! {
    pub static ref last_panic_info: RwLock<Option<(String, Backtrace)>> = { RwLock::new(None) };
}
  let default_hook = panic::take_hook();
  panic::set_hook(Box::new(move |panic_info| {
      *self::last_panic_info.write().unwrap() = Some((
          format!("{}", panic_info),
          Backtrace::capture(),
      ));
      default_hook(panic_info);
  }));

  if let Err(_) = panic::catch_unwind(move || {
      panic!("panic test");
  }) {
      let last = self::last_panic_info.read().unwrap();
      let last = last.as_ref().unwrap();
      println!("[catch panic]\ninfo: {}\nbacktrace: {}", last.0, last.1);
  }

lazy_staticで最後のパニック情報を保存しておくグローバル変数を宣言して、set_hookでそこにパニック情報を保存しています。
set_hook内でBacktracecaptureすることでバックトレースも取得することが出来ます。 (#![feature(backtrace)]が必要です)。デフォルトのhookを一度避難させて再度呼び出したりもしています(Rustでデフォルトのパニック表示を損なわずにpanic時に行われる処理を増やす - ncaq)
これを環境変数RUST_BACKTRACE=1を設定して実行すると以下のように出力できます。

[catch panic]
info: panicked at 'panic test', src/main.rs:24:9
backtrace: stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/vsts/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.37/src/backtrace/libunwind.rs:88
      backtrace::backtrace::trace_unsynchronized
             at /Users/vsts/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.37/src/backtrace/mod.rs:66
      std::backtrace::Backtrace::create
             at src/libstd/backtrace.rs:232
   1: std::backtrace::Backtrace::capture
             at src/libstd/backtrace.rs:207
   2: panic_test_rs::main::{{closure}}
             at src/main.rs:18
   3: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:473
   4: std::panicking::begin_panic
   5: panic_test_rs::main::{{closure}}
             at src/main.rs:24
   6: std::panicking::try::do_call
             at /rustc/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libstd/panicking.rs:288
   7: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:80
   8: std::panicking::try
             at /rustc/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libstd/panicking.rs:267
   9: std::panic::catch_unwind
             at /rustc/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libstd/panic.rs:396
  10: panic_test_rs::main
             at src/main.rs:23
  11: std::rt::lang_start::{{closure}}
             at /rustc/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libstd/rt.rs:61
  12: std::rt::lang_start_internal::{{closure}}
             at src/libstd/rt.rs:48
      std::panicking::try::do_call
             at src/libstd/panicking.rs:288
  13: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:80
  14: std::panicking::try
             at src/libstd/panicking.rs:267
      std::panic::catch_unwind
             at src/libstd/panic.rs:396
      std::rt::lang_start_internal
             at src/libstd/rt.rs:47
  15: std::rt::lang_start
             at /rustc/fa0f7d0080d8e7e9eb20aa9cbf8013f96c81287f/src/libstd/rt.rs:61
  16: <panic_test_rs::last_panic_info as core::ops::deref::Deref>::deref

ちなみにRUST_BACKTRACEを設定しないと以下のような出力になります。

[catch panic]
info: panicked at 'panic test', src/main.rs:24:9
backtrace: disabled backtrace