恐慌
Rust 提供了一种机制,可以阻止函数正常返回,而是“panic”(恐慌),这是一种对错误条件的响应,这种错误通常在遇到它的上下文中是不可恢复的。
一些语言结构,例如 数组越界索引,会自动触发 panic。
也有语言特性提供了对 panic 行为的控制能力
注意
标准库提供了通过
panic!宏显式触发 panic 的功能。
panic_handler 属性
panic_handler 属性可以应用于函数,以定义 panic 的行为。
panic_handler 属性只能应用于签名形如 fn(&PanicInfo) -> ! 的函数。
注意
PanicInfo结构体包含了关于 panic 发生位置的信息。
在依赖图中必须只有一个 panic_handler 函数。
下面展示了一个 panic_handler 函数,它记录 panic 消息然后停止线程。
#![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 {}
}
标准行为
std 提供了两种不同的 panic 处理程序
unwind— 展开(栈)且潜在可恢复。abort–– 中止(进程)且不可恢复。
并非所有目标平台都提供 unwind 处理程序。
注意
链接
std时使用的 panic 处理程序可以通过-C panicCLI 标志设置。大多数目标平台的默认值是unwind。标准库的 panic 行为可以在运行时通过
std::panic::set_hook函数修改。
链接 no_std 的二进制文件、dylib、cdylib 或 staticlib 将需要指定自己的 panic 处理程序。
恐慌策略
恐慌策略定义了 crate 支持的 panic 行为类型。
注意
在
rustc中可以使用-C panicCLI 标志选择 panic 策略。生成二进制文件、dylib、cdylib 或 staticlib 并链接
std时,-C panicCLI 标志也会影响使用哪个恐慌处理程序。
注意
使用
abort恐慌策略编译代码时,优化器可能会假定 Rust 栈帧之间的展开是不可能的,这可以提高代码大小和运行时速度。
注意
关于链接不同恐慌策略的 crate 的限制,请参阅 link.unwinding。这意味着使用
unwind策略构建的 crate 可以使用abort恐慌处理程序,但abort策略不能使用unwind恐慌处理程序。
展开
Panicking(发生恐慌)可以是可恢复的或不可恢复的,尽管可以通过配置(选择非展开的恐慌处理程序)使其始终不可恢复。(反之则不成立:unwind 处理程序不能保证所有 panic 都是可恢复的,只能保证通过 panic! 宏和类似的标准库机制发生的 panic 是可恢复的。)
当发生 panic 时,unwind 处理程序会“展开”Rust 栈帧,就像 C++ 的 throw 展开 C++ 栈帧一样,直到 panic 到达恢复点(例如在线程边界处)。这意味着随着 panic 遍历 Rust 栈帧,这些栈帧中实现了 Drop 的存活对象将调用其 drop 方法。因此,当正常执行恢复时,不再可访问的对象将像它们正常超出作用域一样被“清理”。
注意
只要资源清理的这一保证得到维护,“展开”就可以通过不使用 C++ 为目标平台所用的实际机制来实现。
注意
标准库提供了两种从 panic 中恢复的机制:
std::panic::catch_unwind(它使得在发生 panic 的线程内恢复成为可能)和std::thread::spawn(它自动为派生的线程设置 panic 恢复,以便其他线程可以继续运行)。
跨 FFI 边界展开
使用适当的 ABI 声明可以实现跨 FFI 边界展开。虽然在某些情况下有用,但这会产生独特的未定义行为机会,尤其是在涉及多个语言运行时的情况下。
使用错误的 ABI 进行展开是未定义行为
- 从通过使用非展开 ABI(例如
"C","system"等)声明的函数声明或指针调用的外部函数中导致展开进入 Rust 代码。(例如,当此类用 C++ 编写的函数抛出未被捕获并传播到 Rust 的异常时,就会发生这种情况。) - 从不支持展开的代码中调用会展开的 Rust
extern函数(使用extern "C-unwind"或其他允许展开的 ABI),例如使用-fno-exceptions编译的 GCC 或 Clang 代码。
使用 std::panic::catch_unwind、std::thread::JoinHandle::join 捕获外部展开操作(例如 C++ 异常),或者让其传播到 Rust main() 函数或线程根之外,将会出现两种行为之一,且具体出现哪种行为是未指定的:
- 进程中止。
- 函数返回包含一个不透明类型的
Result::Err。
注意
使用不同 Rust 标准库实例编译或链接的 Rust 代码,出于此保证的目的,被视为“外部异常”。因此,一个使用
panic!并链接到某个版本 Rust 标准库的库,若被使用不同版本标准库的应用调用,即使该库仅在子线程中使用,也可能导致整个应用中止。
当前无法保证外部运行时尝试处理或重新抛出 Rust panic 载荷时的行为。换句话说,源自 Rust 运行时的展开必须要么导致进程终止,要么被同一运行时捕获。