trait Foo {
type Bar<'a> where Self: 'a;
fn barify(&self) -> Self::Bar<'_>;
}
Is it possible to have some equivalent to the owning_ref crate where one can package together a F: Foo with an F::Bar<'a> that borrows from it? One option would be to create a struct like
struct FooAndItsBar<F>
where F: Foo
{
foo: Pin<Box<F>>,
bar: F::Bar<'static>,
}
and carefully use mem::transmute to lengthen/shorten bars lifetime, however, this would require F: 'static (although I guess this implicitly assumes that the representation of F::Bar<'a> is identical regardless of 'a. Is that sound, at least pending specialization?)
Is there any way to do this that doesn't impose an F: 'static bound? Or is this unsound in principle?
It's definitely not unsound in principle to have generic owner-and-borrow bundles with lifetimes, because generic async fns do just that; for example,
async fn example<F: Foo>(foo: F) {
let bar = foo.barify();
std::future::ready().await;
// this future now owns foo and bar together
}
However, there's no good way to build on that to being able to access the bar from outside in a way that doesn't look like sending messages to an async task.
(By the way, you should not use owning_ref; it is unsound and unmaintained. I personally favor ouroboros; its approach is to let you declare a struct like your FooAndItsBar rather than assuming the structure is exactly a pair of owner and borrower like owning_ref and yoke do.)
Thanks for the tip on owning_ref, alas Ouroboros doesnt seem to be capable of handling this use case either. The code it expands to similarly won’t compile without an F: ‘static bound.
The only partial solution I have at present is to add a lifetime parameter onto the definition of FooAndItsBar but this is obviously not ideal.
Could you expand on what you mean about the only existing solutions looking like sending messages to an async task?
The compiler does have built-in support for owning-and-borrowing structures, but only when those structures are coroutine states.
In stable Rust, the only form of coroutines you can define are async blocks that produce Futures.
The only ways to interact with a Future are to poll() it for an output (which can only happen once and cannot borrow anything out of the future itself), and to mutate shared state it has access to.
Therefore, the only way to interact with borrowed values inside a Future is to mutate shared state (such as a channel) and then poll the future so it can read the shared state and manipulate the borrowed values on your behalf. This is what I described briefly as "sending messages".
Regarding soundness, it’s safe to assume that compilation of Rust programs (including type layout) generally does not depend on the particular choice of a (free[1]) lifetime argument being used.
If the F: 'static thing is solved, then still: the question of whether (and if so, how) a sound API interface can be created, is hard to answer in advance. APIs for self-referential structs are always surprisingly difficult to get right.[2]
as in, not bound parameters introduced by a for<'a> … binder – as variation of choice of bound lifetimes lifetimes between such types like dyn for<'a, 'b> Trait<'a, 'a> vs dyn for<'a, 'b> Trait<'a, 'b>can turn out to be considered distinct in ways that can affect compilation ↩︎
I speak from experience, there are a lot of (sometimes subtle) soundness issues in generic self-referential struct library solutions that I have been able to come up with over the last few years ↩︎
Yes, the ability to construct an 'unsafe' lifetime feels like exactly the functionality I'd need here. Thanks for pointing me to this. Though, separately, it seems like it would be useful to somehow be able to quantify over e.g. for<'a where F: 'a>