Rust에서의 패닉
1단계: panic! 매크로 호출
실제로 패닉 매크로는 두 개가 있습니다. 하나는 core에 정의되어 있고, 다른 하나는 std에 정의되어 있습니다. 이는 core의 코드에서도 패닉이 발생할 수 있기 때문입니다. core는 std보다 먼저 빌드되지만, 패닉이 core에서 발생하든 std에서 발생하든 런타임에는 동일한 메커니즘을 사용하도록 하고자 합니다.
panic!의 core 정의 (패닉)
core의 panic! 매크로는 결국 다음과 같은 호출을 수행합니다(library/core/src/panicking.rs에 있습니다).
#![allow(unused)]
fn main() {
// NOTE This function never crosses the FFI boundary; it's a Rust-to-Rust call
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() {
/// Entry point of panics from the core crate (`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 언어 항목으로 변환합니다.
이제 std에 대응하는 panic_handler 언어 항목이 있습니다. 이 함수는 core의 extern { fn panic_impl } 정의와 동일한 과정을 거쳐, 최종적으로 rust_begin_unwind라는 심볼 이름을 갖게 됩니다. 링크 시점에 core의 심볼 참조는 std의 정의(Rust 소스에서 panic_handler라는 이름의 함수)로 해석됩니다.
따라서 런타임에 제어 흐름은 core에서 std로 넘어갑니다. 이를 통해 core에서 발생한 패닉도 다른 패닉이 사용하는 것과 동일한 인프라(패닉 훅, 언와인딩 등)를 거치게 됩니다.
panic!의 std 구현 (패닉)
실제 패닉 관련 로직은 여기에서 시작됩니다. library/std/src/panicking.rs에서 제어는 panic_with_hook으로 넘어갑니다. 이 메서드는 전역 패닉 훅을 호출하고 이중 패닉을 검사하는 역할을 합니다. 마지막으로 패닉 런타임이 제공하는 __rust_start_panic을 호출합니다.
__rust_start_panic 호출은 매우 특이합니다. usize로 변환된 *mut &mut dyn PanicPayload가 전달됩니다. 이 타입을 하나씩 분석해 봅시다.
-
PanicPayload는 내부 트레이트입니다. 이 트레이트는PanicPayload(사용자가 제공한 페이로드 타입을 감싸는 래퍼)에 대해 구현되어 있으며,fn take_box(&mut self) -> *mut (dyn Any + Send)메서드를 가지고 있습니다. 이 메서드는 사용자가 제공한 페이로드(T: Any + Send)를 받아 박스에 담고, 그 박스를 원시 포인터로 변환합니다. -
__rust_start_panic을 호출할 때 우리는&mut dyn PanicPayload를 가지고 있습니다. 그러나 이것은 팻 포인터(usize크기의 두 배)입니다. 이를 FFI 경계를 넘어 패닉 런타임에 전달하기 위해, 이 가변 참조에 대한 가변 참조(&mut &mut dyn PanicPayload)를 얻고, 이를 원시 포인터(*mut &mut dyn PanicPayload)로 변환합니다. 바깥쪽 원시 포인터는Sized타입(가변 참조)을 가리키므로 씬 포인터입니다. 따라서 이 씬 포인터를usize로 변환할 수 있으며, 이는 FFI 경계를 넘어 전달하기에 적합합니다.
마지막으로, 이 usize를 인자로 __rust_start_panic을 호출합니다. 이제 패닉 런타임에 진입했습니다.
2단계: 패닉 런타임
Rust는 panic_abort와 panic_unwind라는 두 가지 패닉 런타임을 제공합니다. 사용자는 빌드 시점에 Cargo.toml을 통해 둘 중 하나를 선택합니다.
panic_abort는 매우 단순합니다. 예상하시는 대로 이 런타임의 __rust_start_panic 구현은 그냥 중단(abort)합니다.
panic_unwind는 더 흥미로운 경우입니다.
이 런타임의 __rust_start_panic 구현에서는 usize를 받아 다시 *mut &mut dyn PanicPayload로 변환하고, 이를 역참조한 다음, &mut dyn PanicPayload에 대해 take_box를 호출합니다. 이 시점에서 우리는 페이로드 자체에 대한 원시 포인터(*mut (dyn Send + Any)), 즉 panic!을 호출한 사용자가 제공한 실제 값에 대한 원시 포인터를 가지게 됩니다.
이 시점에서 플랫폼 독립적인 코드는 끝납니다. 이제 플랫폼별 언와인딩 로직(예: unwind)을 호출합니다. 이 코드는 스택을 언와인딩하고, 각 프레임과 연관된 ’랜딩 패드(landing pad)’를 실행하며(현재는 소멸자 실행), 제어를 catch_unwind 프레임으로 넘기는 역할을 합니다.
모든 패닉은 프로세스를 중단시키거나 어떤 catch_unwind 호출에 의해 잡힌다는 점에 유의하십시오. 특히 std의 런타임 서비스에서는 사용자가 제공한 main 함수 호출이 catch_unwind로 감싸여 있습니다.