Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add panic implementation docs #521

Merged
merged 7 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
- [Salsa](./salsa.md)
- [Lexing and Parsing](./the-parser.md)
- [`#[test]` Implementation](./test-implementation.md)
- [Panic Implementation](./panic-implementation.md)
- [Macro expansion](./macro-expansion.md)
- [Name resolution](./name-resolution.md)
- [The HIR (High-level IR)](./hir.md)
Expand Down
108 changes: 108 additions & 0 deletions src/panic-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
### Panicking in rust ###

#### Step 1: Invocation of the `panic!` macro.

There are actually two panic macros - one defined in `libcore`, and one defined in `libstd`.
This is due to the fact that code in `libcore` can panic. `libcore` is built before `libstd`,
but we want panics to use the same machinery at runtime, whether they originate in `libcore`
or `libstd`.

##### libcore definition of panic!

The `libcore` `panic!` macro eventually makes the following call (in `src/libcore/panicking.rs`):

```rust
// 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::internal_constructor(Some(&fmt), location);
unsafe { panic_impl(&pi) }
```

Actually resolving this goes through several layers of indirection:

1. In `src/librustc/middle/weak_lang_items.rs`, `panic_impl` is declared as 'weak lang item',
with the symbol `rust_begin_unwind`. This is used in `librustc_typeck/collect.rs`
to set the actual symbol name to `rust_begin_unwind`.

Note that `panic_impl` is declared in an `extern "Rust"` block,
which means that libcore will attempt to call a foreign symbol called `rust_begin_unwind`
(to be resolved at link time)

2. In `src/libstd/panicking.rs`, we have this definition:

```rust
/// Entry point of panic from the libcore crate.
[cfg(not(test))]
[panic_handler]
[unwind(allowed)]
Comment on lines +39 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you are missing # before these lines?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't seem to figure out how to stop the # from being interpreted as the start of a comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think md has comments? Can you elaborate on what the concern is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I added # marks, the lines didn't render in my web browser.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... Ok, we can figure this out in a followup PR. I think we can go ahead and merge without this.

pub fn begin_panic_handler(info: &PanicInfo<'_>) -> ! {
...
}
```

The special `panic_handler` attribute is resolved via `src/librustc/middle/lang_items`.
The `extract` function converts the `panic_handler` attribute to a `panic_impl` lang item.

Now, we have a matching `panic_handler` lang item in the `libstd`. This function goes
through the same process as the `extern { fn panic_impl }` definition in `libcore`, ending
up with a symbol name of `rust_begin_unwind`. At link time, the symbol reference in `libcore`
will be resolved to the definition of `libstd` (the function called `begin_panic_handler` in the
Rust source).

Thus, control flow will pass from libcore to std at runtime. This allows panics from `libcore`
to go through the same infrastructure that other panics use (panic hooks, unwinding, etc)

##### libstd implementation of panic!

This is where the actual panic-related logic begins. In `src/libstd/panicking.rs`,
control passes to `rust_panic_with_hook`. This method is responsible
for invoking the global panic hook, and checking for double panics. Finally,
we call `__rust_start_panic`, which is provided by the panic runtime.

The call to `__rust_start_panic` is very weird - it is passed a `*mut &mut dyn BoxMeUp`,
converted to an `usize`. Let's break this type down:

1. `BoxMeUp` is an internal trait. It is implemented for `PanicPayload`
(a wrapper around the user-supplied payload type), and has a method
`fn box_me_up(&mut self) -> *mut (dyn Any + Send)`.
This method takes the user-provided payload (`T: Any + Send`),
boxes it, and converts the box to a raw pointer.

2. When we call `__rust_start_panic`, we have an `&mut dyn BoxMeUp`.
However, this is a fat pointer (twice the size of a `usize`).
To pass this to the panic runtime across an FFI boundary, we take a mutable
reference *to this mutable reference* (`&mut &mut dyn BoxMeUp`), and convert it to a raw pointer
(`*mut &mut dyn BoxMeUp`). The outer raw pointer is a thin pointer, since it points to a `Sized`
type (a mutable reference). Therefore, we can convert this thin pointer into a `usize`, which
is suitable for passing across an FFI boundary.

Finally, we call `__rust_start_panic` with this `usize`. We have now entered the panic runtime.

#### Step 2: The panic runtime

Rust provides two panic runtimes: `libpanic_abort` and `libpanic_unwind`. The user chooses
between them at build time via their `Cargo.toml`

`libpanic_abort` is extremely simple: its implementation of `__rust_start_panic` just aborts,
as you would expect.

`libpanic_unwind` is the more interesting case.

In its implementation of `__rust_start_panic`, we take the `usize`, convert
it back to a `*mut &mut dyn BoxMeUp`, dereference it, and call `box_me_up`
on the `&mut dyn BoxMeUp`. At this point, we have a raw pointer to the payload
itself (a `*mut (dyn Send + Any)`): that is, a raw pointer to the actual value
provided by the user who called `panic!`.

At this point, the platform-independent code ends. We now call into
platform-specific unwinding logic (e.g `libunwind`). This code is
responsible for unwinding the stack, running any 'landing pads' associated
with each frame (currently, running destructors), and transferring control
to the `catch_unwind` frame.

Note that all panics either abort the process or get caught by some call to `catch_unwind`:
in `src/libstd/rt.rs`, the call to the user-provided `main` function is wrapped in `catch_unwind`.