Here's an exploitation that demonstrates why a second call to b(a)
could be bad
use std::cell::RefCell;
#[derive(Debug)]
pub struct A(i32);
fn run_both<'c>(a: &'c mut A, b: impl Fn(&'c mut A)) {
b(a);
//b(a);
}
fn main() {
let x: RefCell<[Option<&mut A>; 2]> = RefCell::new([None, None]);
let i = RefCell::new(0);
let mut val = A(42);
run_both(&mut val, |r| {
let mut i = i.borrow_mut();
x.borrow_mut()[*i] = Some(r);
*i += 1;
});
let references = x.into_inner();
dbg!(&references);
}
If there were two b(a)
calls in run_both
(and that didn't result in a compilation error) then in main
the references
vector would at the end contain two (active) copies of the same mutable reference, which is something you must not be able to obtain in Rust, as you probably know.
The typical fix is to change the function signature. In this case, lifetime elision does actually do the right thing
fn run_both(a: &mut A, b: impl Fn(&mut A))
{
b(a);
b(a);
}
Now run_both
compiles fine with two calls, but the exploit code above will (successfully) be prevented from compiling.
error[E0521]: borrowed data escapes outside of closure
--> src/main.rs:18:9
|
13 | let x: RefCell<[Option<&mut A>; 2]> = RefCell::new([None, None]);
| - `x` declared here, outside of the closure body
...
16 | run_both(&mut val, |r| {
| - `r` is a reference that is only valid in the closure body
17 | let mut i = i.borrow_mut();
18 | x.borrow_mut()[*i] = Some(r);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `r` escapes the closure body here
The meaning of the elided lifetimes involve so-called hither-ranked trait bounds.
fn run_both(a: &mut A, b: impl Fn(&mut A))
is essentially short for something like
fn run_both<'c>(a: &'c mut A, b: impl for<'d> Fn(&'d mut A))
This allows the closure b
to be called with a mutable reference of shorter lifetime than 'c
. In particular the two calls to b(a)
must happen with references of different lifetime since otherwise the references passed to the two calls would be (rightfully, as demonstrated above) considered to be overlapping and thus conflicting aliasing mutable access to the same thing. And the higher-ranked bound for Fn
allows the callback to be generic over the lifetime 'd
and thus allows those multiple calls with different non-overlapping reference lifetimes.