We can make a case study with this code:
pub struct MyStruct {}
impl Drop for MyStruct {
fn drop(&mut self) {
println!("{:?}", "dropped!");
}
}
extern "Rust" {
fn unknown_api();
}
pub fn f() -> MyStruct {
let a = MyStruct {};
unsafe {
unknown_api();
}
a
}
Playground link
This code is not intended to run, just compile. I've inserted a call to a function Rust doesn't know - the unknown_api
. Rust will have to assume it can panic/unwind, and inserts the necessary cleanup code in the f
function to handle this case.
Enable Release compile, compile the code to MIR and look at the output.
For the f
function we can see that the code to the unknown_api has both a following edge and an "unwind edge" which leads to cleanup. See the unknown_api
line.
fn f() -> MyStruct {
let mut _0: MyStruct; // return place in scope 0 at src/lib.rs:13:15: 13:23
let _1: MyStruct; // in scope 0 at src/lib.rs:14:6: 14:7
let _2: (); // in scope 0 at src/lib.rs:16:6: 16:19
scope 1 {
debug a => _1; // in scope 1 at src/lib.rs:14:6: 14:7
scope 2 {
}
}
bb0: {
StorageLive(_1); // scope 0 at src/lib.rs:14:6: 14:7
StorageLive(_2); // scope 2 at src/lib.rs:16:6: 16:19
_2 = unknown_api() -> [return: bb1, unwind: bb2]; // scope 2 at src/lib.rs:16:6: 16:19
// mir::Constant
// + span: src/lib.rs:16:6: 16:17
// + literal: Const { ty: unsafe fn() {unknown_api}, val: Value(Scalar(<ZST>)) }
}
bb1: {
StorageDead(_2); // scope 2 at src/lib.rs:16:19: 16:20
_0 = const MyStruct { }; // scope 1 at src/lib.rs:18:2: 18:3
// ty::Const
// + ty: MyStruct
// + val: Value(Scalar(<ZST>))
// mir::Constant
// + span: src/lib.rs:18:2: 18:3
// + literal: Const { ty: MyStruct, val: Value(Scalar(<ZST>)) }
StorageDead(_1); // scope 0 at src/lib.rs:19:1: 19:2
return; // scope 0 at src/lib.rs:19:2: 19:2
}
bb2 (cleanup): {
drop(_1) -> bb3; // scope 0 at src/lib.rs:19:1: 19:2
}
bb3 (cleanup): {
resume; // scope 0 at src/lib.rs:13:1: 19:2
}
}
You can also see the asm output of function f
playground::f: # @playground::f
# %bb.0:
pushq %rbx
callq *unknown_api@GOTPCREL(%rip)
# %bb.1:
popq %rbx
retq
movq %rax, %rbx
callq core::ptr::drop_in_place
movq %rbx, %rdi
callq _Unwind_Resume@PLT
ud2
As you can see there is a code size cost to this feature - everything after retq
, the cleanup code that's added, but it's outside the main flow of the code. Code size is partly a runtime cost, but it's hard to quantify and it's not an "instructions executed" cost.
How does it all work? It's platform specific, and there are some links in this file for this implementation: rust/gcc.rs at 1.49.0 · rust-lang/rust · GitHub