RFC 2070: panic-implementation

libs (panic | attributes | no_std)

Summary

Provide a stable mechanism to specify the behavior of panic! in no-std applications.

Motivation

The #![no_std] attribute was stabilized some time ago and it made possible to build no-std libraries on stable. However, to this day no-std applications still require a nightly compiler to be built. The main cause of this is that the behavior of panic! is left undefined in no-std context, and the only way to specify a panicking behavior is through the unstable panic_fmt language item.

This document proposes a stable mechanism to specify the behavior of panic! in no-std context. This would be a step towards enabling development of no-std applications like device firmware, kernels and operating systems on the stable channel.

Detailed design

Constraints

panic! in no-std environments must continue to be free of memory allocations and its API can only be changed in a backward compatible way.

Although not a hard constraint, the cognitive load of the mechanism would be greatly reduced if it mimicked the existing custom panic hook mechanism as much as possible.

PanicInfo

The types std::panic::PanicInfo and std::panic::Location will be moved into the core crate, and PanicInfo will gain a new method:

impl PanicInfo {
    pub fn message(&self) -> Option<&fmt::Arguments> { .. }
}

This method returns Some if the panic! invocation needs to do any formatting like panic!("{}: {}", key , value) does.

fmt::Display

For convenience, PanicInfo will gain an implementation of the fmt::Display trait that produces a message very similar to the one that the standard panic! hook produces. For instance, this program:

use std::panic::{self, PanicInfo};

fn panic_handler(pi: &PanicInfo) {
    println!("the application {}", pi);
}

fn main() {
    panic::set_hook(Box::new(panic_handler));

    panic!("Hello, {}!", "world");
}

Would print:

$ cargo run
the application panicked at 'Hello, world!', src/main.rs:27:4

#[panic_implementation]

A #[panic_implementation] attribute will be added to the language. This attribute can be used to specify the behavior of panic! in no-std context. Only functions with signature fn(&PanicInfo) -> ! can be annotated with this attribute, and only one item can be annotated with this attribute in the whole dependency graph of a crate.

Here's an example of how to replicate the panic messages one gets on std programs on a no-std program:

use core::fmt;
use core::panic::PanicInfo;

// prints: "program panicked at 'reason', src/main.rs:27:4"
#[panic_implementation]
fn my_panic(pi: &PanicInfo) -> ! {
    let _ = writeln!(&MY_STDERR, "program {}", pi);

    abort()
}

The #[panic_implementation] item will roughly expand to:

fn my_panic(pi: &PanicInfo) -> ! {
    // same as before
}

// Generated by the compiler
// This will always use the correct ABI and will work on the stable channel
#[lang = "panic_fmt"]
#[no_mangle]
pub extern fn rust_begin_panic(msg: ::core::fmt::Arguments,
                               file: &'static str,
                               line: u32,
                               col: u32) -> ! {
    my_panic(&PanicInfo::__private_unstable_constructor(msg, file, line, col))
}

Payload

The core version of the panic! macro will gain support for payloads, as in panic!(42). When invoked with a payload PanicInfo.payload() will return the payload as an &Any trait object just like it does in std context with custom panic hooks.

When using core::panic! with formatting, e.g. panic!("{}", 42), the payload will be uninspectable: it won't be downcastable to any known type. This is where core::panic! diverges from std::panic!. The latter returns a String, behind the &Any trait object, from the payload() method in this situation.

Feature gate

The initial implementation of the #[panic_implementation] mechanism as well as the core::panic::Location and core::panic::PanicInfo types will be feature gated. std::panic::Location and std::panic::PanicInfo will continue to be stable except for the new PanicInfo.message method.

Unwinding

The #[panic_implementation] mechanism can only be used with no-std applications compiled with -C panic=abort. Applications compiled with -C panic=unwind additionally require the eh_personality language item which this proposal doesn't cover.

std::panic!

This proposal doesn't affect how the selection of the panic runtime in std applications works (panic_abort, panic_unwind, etc.). Using #[panic_implementation] in std programs will cause a compiler error.

How We Teach This

Currently, no-std applications are only possible on nightly so there's not much official documentation on this topic given its dependency on several unstable features. Hopefully once no-std applications are minimally possible on stable we can have a detailed chapter on the topic in "The Rust Programming Language" book. In the meantime, this feature can be documented in the unstable book.

Drawbacks

Slight deviation from std

Although both #[panic_implementation] (no-std) and custom panic hooks (std) use the same PanicInfo type. The behavior of the PanicInfo.payload() method changes depending on which context it is used: given panic!("{}", 42), payload() will return a String, behind an Any trait object, in std context but it will return an opaque Any trait object in no-std context.

Alternatives

Not doing this

Not providing a stable alternative to the panic_fmt language item means that no-std applications will continue to be tied to the nightly channel.

Two PanicInfo types

An alternative design is to have two different PanicInfo types, one in core and one in std. The difference between these two types would be in their APIs:

// core
impl PanicInfo {
    pub fn location(&self) -> Option<Location> { .. }
    pub fn message(&self) -> Option<&fmt::Arguments> { .. }

    // Not available
    // pub fn payload(&self) -> &(Any + Send) { .. }
}

// std
impl PanicInfo {
    pub fn location(&self) -> Option<Location> { .. }
    pub fn message(&self) -> Option<&fmt::Arguments> { .. }
    pub fn payload(&self) -> &(Any + Send) { .. }
}

In this alternative design the signature of the #[panic_implementation] function would be enforced to be fn(&core::panic::PanicInfo) -> !. Custom panic hooks will continue to use the std::panic::PanicInfo type.

This design precludes supporting payloads in core::panic! but also eliminates the difference between core::PanicInfo.payload() in no-std vs std by eliminating the method in the former context.

Unresolved questions

fmt::Display

Should the Display of PanicInfo format the panic information as "panicked at 'reason', src/main.rs:27:4", as "'reason', src/main.rs:27:4", or simply as "reason".

Unwinding in no-std

Is this design compatible, or can it be extended to work, with unwinding implementations for no-std environments?