When is it safe to make unbounded lifetimes bounded?

In the case of implementing Sink, we are given an unbounded lifetime of &mut self or mut self: Pin<&mut Self>. However, let's say you want to pass on a mutable borrowed reference (which exists as an owned type within a vector) from the poll function into another structure which stores that mutable reference in a VecDeque. If this is passed from the poll function, the borrow checker, in my experience, isn't happy because the closure of poll expires before the mutable borrow. The way to alleviate this unsafely is by doing this

unsafe fn extend_lifetime(&mut self) -> &'a mut self {
    std::mem::transmute(self)
}

Then, under the poll function, I would call:

self.extend_lifetime().send_mut_packet_to_closure_a(self.extend_lifetime().packet_store.get_mut(&index))

then I'd pass that mutable borrow into a closure that requires a lifetime of `a (where it is therefrom stored)

However, is this truly safe if I guarantee that no two mutable borrows will be alive simultaneously? My program's logic already guarantees this; I'm just wanting to pass around mutable borrows of packets around different stages of processing

This is 100% unsound, given the code:

unsafe fn extend_lifetime<'a>(&mut self) -> &'a mut self {
    std::mem::transmute(self)
}

fn poll(&mut self) {
    let this = unsafe { self.extend_lifetime() };
}

you have created two aliasing unique references, this and self (because extend_lifetime's return value does not borrow from the incoming reference the reborrow generated for it only lasts until extend_lifetime returns).

If you want to do lifetime shenanigans to work around borrow checker limitations you must use raw pointers, and in the case of implementing Sink where you are passing that pointer outside of its lifetime then you cannot ever turn that raw pointer back into a reference as you do not have control over when your owner may create a new unique reference to yourself. (Unless this is single-threaded and you are very, very careful about when you are polled versus running the closure, but then implementing a general purpose trait like Sink is not a good idea).

3 Likes

Hmm, perhaps sending around a constant pointer to a heap-pinned data structure would work out?

I am using a ThreadPool for an executor, and as such, I am not sure if my program is using a single thread or multiple for the Future which drives my packet system. Even if I did know, I would want perhaps several threads in the case that there is heavy backpressure in downstream sinks.