패닉
패닉 (Panic)
프로그램이 정상적으로 값을 반환하지 않고 "패닉(panic)"을 일으키는 메커니즘이 있어요. 이번 장에서는 패닉이 무엇이고, 어떤 속성으로 제어하며, 스택을 어떻게 풀어내는지(언와인딩) 살펴볼게요.
출처: Rust Reference
본문
패닉은 함수가 정상적으로 반환하지 못하게 막고 대신 "패닉"을 일으키는, 일반적으로 그 오류를 만난 컨텍스트 안에서 복구될 것으로 기대되지 않는 오류 조건에 대한 응답이에요.
배열 인덱싱이 범위를 벗어나는 것 같은 일부 언어 구조는 자동으로 패닉해요.
패닉 동작을 어느 정도 제어할 수 있게 해주는 언어 기능도 있어요:
- panic handler가 패닉의 동작을 정의해요.
- FFI ABI가 패닉이 어떻게 동작하는지 바꿀 수 있어요.
참고: 표준 라이브러리는
panic!매크로로 명시적으로 패닉을 일으키는 능력을 제공해요.
panic_handler 속성
panic_handler 속성을 함수에 적용하면 패닉의 동작을 정의할 수 있어요.
panic_handler 속성은 fn(&PanicInfo) -> ! 시그니처를 가진 함수에만 적용할 수 있어요.
참고:
PanicInfo구조체는 패닉의 위치에 대한 정보를 담고 있어요.
의존성 그래프 안에는 panic_handler 함수가 정확히 하나 있어야 해요.
아래는 패닉 메시지를 기록한 뒤 스레드를 멈추는 panic_handler 함수예요.
#![no_std]
use core::fmt::{self, Write};
use core::panic::PanicInfo;
struct Sink {
// ..
_0: (),
}
impl Sink {
fn new() -> Sink { Sink { _0: () }}
}
impl fmt::Write for Sink {
fn write_str(&mut self, _: &str) -> fmt::Result { Ok(()) }
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let mut sink = Sink::new();
// logs "panicked at '$reason', src/main.rs:27:4" to some `sink`
let _ = writeln!(sink, "{}", info);
loop {}
}
표준 동작 (Standard behavior)
std는 두 가지 서로 다른 panic handler를 제공해요:
- unwind — 스택을 푸는(unwind) 방식으로, 잠재적으로 복구 가능해요.
- abort — 프로세스를 중단하는 방식으로, 복구 불가능해요.
모든 타깃이 unwind handler를 제공하지는 않아요.
참고: std로 링크할 때 사용되는 panic handler는
-C panicCLI 플래그로 설정할 수 있어요. 대부분의 타깃에서 기본값은 unwind예요.
표준 라이브러리의 패닉 동작은 런타임에 std::panic::set_hook 함수로 수정할 수 있어요.
no_std 바이너리, dylib, cdylib, staticlib을 링크하려면 자신만의 panic handler를 지정해야 해요.
패닉 전략 (Panic strategy)
패닉 전략은 크레이트가 지원하도록 빌드된 패닉 동작의 종류를 정의해요.
참고: 패닉 전략은 rustc에서
-C panicCLI 플래그로 선택할 수 있어요.
바이너리, dylib, cdylib, staticlib을 생성하면서 std로 링크할 때 -C panic CLI 플래그는 어떤 panic handler를 사용할지에도 영향을 줘요.
참고: abort 패닉 전략으로 코드를 컴파일하면, 최적화 프로그램이 Rust 프레임을 가로질러서는 unwind가 불가능하다고 가정할 수 있고, 이는 코드 크기와 런타임 속도 모두 개선으로 이어질 수 있어요.
참고: 서로 다른 패닉 전략을 가진 크레이트를 링크할 때의 제약은 link의 unwinding 부분을 참고해요. 한 가지 의미는, unwind 전략으로 빌드된 크레이트는 abort panic handler를 쓸 수 있지만, abort 전략은 unwind panic handler를 쓸 수 없다는 점이에요.
언와인딩 (Unwinding)
패닉은 복구 가능하거나 복구 불가능할 수 있어요. 다만 (unwinding이 아닌 panic handler를 선택함으로써) 항상 복구 불가능하도록 구성할 수도 있어요. (그 반대는 참이 아니에요. unwind handler가 모든 패닉이 복구 가능하다는 걸 보장하지는 않고, panic! 매크로와 비슷한 표준 라이브러리 메커니즘을 통한 패닉만 복구 가능하다는 걸 보장해요.)
패닉이 발생하면 unwind handler는 C++의 throw가 C++ 프레임을 풀듯이 Rust 프레임을 "언와인딩" 해요. 패닉이 복구 지점(예: 스레드 경계)에 도달할 때까지요. 즉 패닉이 Rust 프레임을 지나갈 때, 그 프레임에 살아 있는 Drop을 구현하는 객체는 그 drop 메서드가 호출돼요. 그래서 정상 실행이 재개될 때 더 이상 접근할 수 없는 객체는 마치 정상적으로 범위를 벗어난 것처럼 "정리"되어 있어요.
참고: 이 리소스 정리 보증이 유지되는 한, "언와인딩"은 실제로 타깃 플랫폼에서 C++이 사용하는 메커니즘을 쓰지 않고도 구현될 수 있어요.
참고: 표준 라이브러리는 패닉에서 복구하기 위한 두 가지 메커니즘을 제공해요.
std::panic::catch_unwind(패닉 중인 스레드 안에서 복구를 가능하게 함)와std::thread::spawn(생성된 스레드를 위해 패닉 복구를 자동으로 설정해 다른 스레드가 계속 실행되게 함)이에요.
FFI 경계를 가로지르는 언와인딩 (Unwinding across FFI boundaries)
적절한 ABI 선언을 사용하면 FFI 경계를 가로질러 언와인딩하는 것이 가능해요. 특정 경우에 유용하지만, 특히 여러 언어 런타임이 관여할 때 미정의 동작의 독특한 기회를 만든다는 점은 주의해야 해요.
잘못된 ABI로 언와인딩하는 것은 미정의 동작이에요:
"C","system"같은 non-unwinding ABI로 선언된 함수 선언이나 포인터를 통해 호출된 외부 함수에서 Rust 코드로 언와인딩을 일으키는 경우. (예: C++로 작성된 그런 함수가 잡히지 않는 예외를 던져서 Rust로 전파되는 경우)- 언와인딩을 지원하지 않는 코드에서 언와인딩하는(
extern "C-unwind"또는 언와인딩을 허용하는 다른 ABI를 가진) Rust extern 함수를 호출하는 경우. 예:-fno-exceptions로 컴파일된 GCC나 Clang 코드.
std::panic::catch_unwind, std::thread::JoinHandle::join을 사용하거나, Rust main() 함수나 스레드 루트를 넘어 전파되게 함으로써 외부 언와인딩 동작(예: C++ 예외)을 잡는 것은 다음 두 동작 중 하나를 갖게 되는데, 어느 쪽이 일어날지는 미지정이에요:
- 프로세스가 중단(abort)돼요.
- 함수가 불투명한 타입을 담은
Result::Err을 반환해요.
참고: Rust 표준 라이브러리의 다른 인스턴스로 컴파일되거나 링크된 Rust 코드는 이 보증의 목적상 "외부 예외"로 간주돼요. 그래서
panic!을 사용하고 한 버전의 Rust 표준 라이브러리에 링크된 라이브러리가, 다른 버전의 표준 라이브러리를 사용하는 애플리케이션에서 호출되면, 그 라이브러리가 자식 스레드 안에서만 사용되더라도 전체 애플리케이션이 중단될 수 있어요.
외부 런타임이 Rust 패닉 페이로드를 처리(dispose)하거나 다시 던지려 할 때 어떤 동작이 일어날지에 대한 보증은 현재 없어요. 즉 Rust 런타임에서 시작된 unwinding은 반드시 프로세스 종료로 이어지거나 같은 런타임에서 잡혀야 해요.
더 알아보기 (Learn more)
panic!매크로와 패닉에서 복구하는catch_unwind, 그리고 패닉 후킹은 std 문서에서 더 자세히 확인할 수 있어요.Result를 사용한 명시적 에러 처리는 패닉 대신 에러를 전파하는 일반적인 방법이에요.