恐慌

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 panic CLI 标志设置。大多数目标平台的默认值是 unwind

标准库的 panic 行为可以在运行时通过 std::panic::set_hook 函数修改。

链接 no_std 的二进制文件、dylib、cdylib 或 staticlib 将需要指定自己的 panic 处理程序。

恐慌策略

恐慌策略定义了 crate 支持的 panic 行为类型。

注意

rustc 中可以使用 -C panic CLI 标志选择 panic 策略。

生成二进制文件、dylib、cdylib 或 staticlib 并链接 std 时,-C panic CLI 标志也会影响使用哪个恐慌处理程序

注意

使用 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_unwindstd::thread::JoinHandle::join 捕获外部展开操作(例如 C++ 异常),或者让其传播到 Rust main() 函数或线程根之外,将会出现两种行为之一,且具体出现哪种行为是未指定的:

  • 进程中止。
  • 函数返回包含一个不透明类型的 Result::Err

注意

使用不同 Rust 标准库实例编译或链接的 Rust 代码,出于此保证的目的,被视为“外部异常”。因此,一个使用 panic! 并链接到某个版本 Rust 标准库的库,若被使用不同版本标准库的应用调用,即使该库仅在子线程中使用,也可能导致整个应用中止。

当前无法保证外部运行时尝试处理或重新抛出 Rust panic 载荷时的行为。换句话说,源自 Rust 运行时的展开必须要么导致进程终止,要么被同一运行时捕获。