Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust におけるパニック

ステップ 1: panic! マクロの呼び出し。

実際には、パニックマクロは 2 つあります。1 つは core で定義され、もう 1 つは std で定義されています。 これは、core 内のコードがパニックできるという事実によるものです。 corestd より前にビルドされますが、 パニックが core に由来するか std に由来するかにかかわらず、実行時には同じ仕組みを使うようにしたいのです。

core における panic! の定義

corepanic! マクロは、最終的に次の呼び出しを行います(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) }
}

これを実際に解決する処理は、複数の間接層を経由します。

  1. 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_implextern "Rust" ブロック内で宣言されていることに注意してください。 つまり、core は rust_begin_unwind という名前の外部シンボルを呼び出そうとします (リンク時に解決されます)。

  2. 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 に変換されます。 この型を分解して見てみましょう。

  1. PanicPayload は内部トレイトです。 これは PanicPayload (ユーザーが提供したペイロード型のラッパー)に対して実装されており、 fn take_box(&mut self) -> *mut (dyn Any + Send) というメソッドを持ちます。 このメソッドは、ユーザーが提供したペイロード(T: Any + Send)を受け取り、 それをボックス化し、そのボックスを raw ポインターに変換します。

  2. __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_abortpanic_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 でラップされています。