Rust におけるパニック
ステップ 1: panic! マクロの呼び出し。
実際には、パニックマクロは 2 つあります。1 つは core で定義され、もう 1 つは std で定義されています。
これは、core 内のコードがパニックできるという事実によるものです。
core は std より前にビルドされますが、
パニックが core に由来するか std に由来するかにかかわらず、実行時には同じ仕組みを使うようにしたいのです。
core における panic! の定義
core の panic! マクロは、最終的に次の呼び出しを行います(library/core/src/panicking.rs 内):
#![allow(unused)]
fn main() {
// NOTE この関数は FFI 境界を越えません。これは Rust から Rust への呼び出しです
extern "Rust" {
#[lang = "panic_impl"]
fn panic_impl(pi: &PanicInfo<'_>) -> !;
}
let pi = PanicInfo::new(
&fmt,
Location::caller(),
/* can_unwind */ true,
/* force_no_backtrace */ false,
);
unsafe { panic_impl(&pi) }
}
これを実際に解決する処理は、複数の間接層を経由します。
-
compiler/rustc_hir/src/weak_lang_items.rsでは、panic_implは シンボルrust_begin_unwindを持つ「weak lang item」として宣言されています。 これはrustc_hir_analysis/src/collect.rsで、実際のシンボル名をrust_begin_unwindに設定するために使われます。panic_implはextern "Rust"ブロック内で宣言されていることに注意してください。 つまり、core はrust_begin_unwindという名前の外部シンボルを呼び出そうとします (リンク時に解決されます)。 -
library/std/src/panicking.rsには、次の定義があります。
#![allow(unused)]
fn main() {
/// core クレートからのパニックのエントリポイント(`panic_impl` lang item)。
#[cfg(not(any(test, doctest)))]
#[panic_handler]
pub fn panic_handler(info: &core::panic::PanicInfo<'_>) -> ! {
...
}
}
特別な panic_handler 属性は、compiler/rustc_passes/src/lang_items.rs 経由で解決されます。
extract_ast 関数は、panic_handler 属性を panic_impl lang item に変換します。
これで、std 内に一致する panic_handler lang item が存在することになります。
この関数は、core 内の extern { fn panic_impl } 定義と同じプロセスを経て、
最終的に rust_begin_unwind というシンボル名になります。
リンク時に、core 内のシンボル参照は
std の定義(Rust ソース内で panic_handler と呼ばれる関数)に解決されます。
したがって、実行時には制御フローが core から std へ渡されます。
これにより、core からのパニックは、
他のパニックが使うものと同じインフラストラクチャ(パニックフック、アンワインドなど)を通ることができます。
std における panic! の実装
ここから、実際のパニック関連ロジックが始まります。
library/std/src/panicking.rs では、
制御は panic_with_hook に渡されます。
このメソッドは、グローバルパニックフックの呼び出しと、二重パニックのチェックを担当します。
最後に、
パニックランタイムによって提供される __rust_start_panic を呼び出します。
__rust_start_panic への呼び出しは非常に奇妙です。*mut &mut dyn PanicPayload が渡され、
usize に変換されます。
この型を分解して見てみましょう。
-
PanicPayloadは内部トレイトです。 これはPanicPayload(ユーザーが提供したペイロード型のラッパー)に対して実装されており、fn take_box(&mut self) -> *mut (dyn Any + Send)というメソッドを持ちます。 このメソッドは、ユーザーが提供したペイロード(T: Any + Send)を受け取り、 それをボックス化し、そのボックスを raw ポインターに変換します。 -
__rust_start_panicを呼び出すとき、手元には&mut dyn PanicPayloadがあります。 しかし、これはファットポインター(usizeの 2 倍のサイズ)です。 これを FFI 境界を越えてパニックランタイムに渡すために、この可変参照への可変参照 (&mut &mut dyn PanicPayload)を取り、それを raw ポインター (*mut &mut dyn PanicPayload)に変換します。 外側の raw ポインターは、Sized型(可変参照)を指しているため、シンポインターです。 したがって、このシンポインターをusizeに変換できます。 これは FFI 境界を越えて渡すのに適しています。
最後に、この usize を指定して __rust_start_panic を呼び出します。
これで、パニックランタイムに入ったことになります。
ステップ 2: パニックランタイム
Rust は 2 つのパニックランタイム、panic_abort と panic_unwind を提供します。
ユーザーはビルド時に Cargo.toml を通じてどちらかを選択します。
panic_abort は非常に単純です。その __rust_start_panic の実装は、予想どおり単に中止します。
panic_unwind は、より興味深いケースです。
その __rust_start_panic の実装では、usize を受け取り、それを
*mut &mut dyn PanicPayload に戻し、逆参照して、&mut dyn PanicPayload に対して
take_box を呼び出します。
この時点で、ペイロードそのものへの raw ポインター
(*mut (dyn Send + Any))があります。つまり、これは panic! を呼び出したユーザーが
提供した実際の値への raw ポインターです。
この時点で、プラットフォーム非依存のコードは終わります。
ここから、プラットフォーム固有のアンワインドロジック(例: unwind)を呼び出します。
このコードは、スタックをアンワインドし、各フレームに関連付けられた「landing pad」
(現在はデストラクタの実行)を実行し、catch_unwind フレームへ制御を移すことを担当します。
すべてのパニックは、プロセスを中止するか、何らかの catch_unwind 呼び出しによって捕捉されることに注意してください。
特に、std の runtime service では、
ユーザーが提供した main 関数の呼び出しは catch_unwind でラップされています。