Reputation: 1566
Consider the following Rust program:
#![feature(generic_associated_types)]
pub trait Func {
type Input<'a>;
type Output;
fn call(self, input: Self::Input<'_>) -> Self::Output;
}
fn invoke<'cx, F>(f: F, ctx: &'cx mut u8)
where F: 'static + Func<Input<'cx> = &'cx u8, Output = u8>,
{
let input = &*ctx;
let out = f.call(input);
*ctx = out;
}
I've used #![feature(generic_associated_types)]
, but I think the question I'm asking is still relevant if you move 'a
from Func::Input
to Func
and use a higher-rank trait bound on invoke
.
This code errors, but I don't think it's unsound:
error[E0506]: cannot assign to `*ctx` because it is borrowed
--> src/lib.rs:15:5
|
10 | fn invoke<'cx, F>(f: F, ctx: &'cx mut u8)
| --- lifetime `'cx` defined here
...
13 | let input = &*ctx;
| ----- borrow of `*ctx` occurs here
14 | let out = f.call(input);
| ------------- argument requires that `*ctx` is borrowed for `'cx`
15 | *ctx = out;
| ^^^^^^^^^^ assignment to borrowed `*ctx` occurs here
First ctx
is reborrowed as input
, which is passed to f.call
and then never used again. f.call
returns a value that does not contain any lifetimes (u8: 'static
), so there is no connection between out
and ctx
.
Likewise, the type of f
contains no lifetimes (F: 'static
), so it cannot hold a reference with lifetime 'cx
. Furthermore, the lifetime 'cx
cannot be safely coerced to 'static
inside call
, so there's no way to "smuggle out" a reference with that lifetime that's accessible beyond the invocation of f.call
. Therefore, I don't see how anything can alias ctx
, and I think assigning to it in the last line should be sound.
Am I missing something? Would accepting this code be unsound? If not, why does Rust fail to take advantage of 'static
bounds in this way?
Upvotes: 1
Views: 221
Reputation: 2654
The code as written is unsound. The lifetime bound of 'static
on F
is completely irrelevant, because the lifetime bounds of F::Input
and F
are two distinct lifetimes, and it's the associated type's lifetime that's causing the error. By declaring F::Input<'ctx> = &'ctx u8
, you are declaring that the immutable borrow lives the length of the mutable one, making the mutable reference unsafe to use.
As @Stargateur mentioned, the thing that can make this work are Higher Ranked Trait bounds:
fn invoke<F>(f: F, ctx: &mut u8)
where F: for<'ctx> Func<Input<'ctx> = &'ctx u8, Output = u8>,
{
let input = ctx;
let out = f.call(input);
*input = out;
}
That is, instead of declaring that the function call is valid for some specific lifetime 'ctx
it is valid for all lifetime's 'ctx
. This way, the compiler can freely pick an appropriate lifetime for the reborrow to make this work.
As a side note, you might think that using two specific lifetimes in the function definition would be able to work, but any attempt to do so results in the compiler failing to choose the appropriate lifetime that makes things work.
Upvotes: 1
Reputation: 59902
The lifetime 'cx
could be 'static
meaning input
can be smuggled elsewhere and be invalidated by *ctx = out
.
There's no way to constrain that a lifetime is strictly less than another, so I don't think adding a "broader" lifetime constraint to a generic type is even considered by the borrow checker.
Upvotes: 1