A weird result of borrowing checker when using tokio

Consider this example:

let (tx,_rx) = tokio::sync::mpsc::unbounded_channel();
let r = Vec::<i32>::new();
tx.send(& r).unwrap(); // #1

The compiler complains at #1 with that:

r does not live long enough,

borrow might be used here, when tx is dropped and runs the destructor for type tokio::sync::mpsc::UnboundedSender<&Vec<i32>>

I try to get the error by using my struct, the implementation is:

use std::cell::RefCell;


pub struct Wrapper<T>(RefCell<Vec<T>>);
impl<T> Drop for Wrapper<T>{
    fn drop(&mut self) {
        //todo!()
        let d = & mut self.0;
    }
}
pub struct MySend<T>(pub Wrapper<T>);
impl<T> MySend<T>{
    pub fn send(&self,v:T)->Option<T>{
        self.0.0.borrow_mut().push(v);
        None
    }
}

fn main(){
 let my_send = MySend(todo!());
  let r = Vec::<i32>::new();
  my_send.send(&r);  // #2
}

I didn't get a similar error at #2. What is the reason here? Then, a more weird thing is when I mix them as the following, the error that should have been at #1 will disappear

fn main() {
    let (tx,_rx) = tokio::sync::mpsc::unbounded_channel();
    let my_send = MySend(todo!());
    let r = Vec::<i32>::new();
    my_send.send(&r);  // #2
    tx.send(& r).unwrap();  // #1
}

Now, both #1 and #2 do not have that error. Which is very weird. So, my questions are:

  1. why does the send of the library tokio use the r can have that error?
  2. How to make a compiler report that error for my implemented send?
  3. Why does the error disappear in the third snippet code?

Your todo!()s are unconditional exit paths from the function which avoid the borrow error.

4 Likes

If I change pub struct Wrapper<T>(RefCell<Vec<T>>); to pub struct Wrapper<T>(PhantomData<T>);, and use MySend(Wrapper(PhantomData)) to initialize my_send, it seems to also avoid the borrow error.

In that version, T is covariant. The error returns if you make it invariant.

    let my_send = MySend(Wrapper(PhantomData)); // MySend::<&'m Vec<i32>>
    let r = Vec::<i32>::new();
    my_send.send(&r); // Borrow tied to `my_send`, must be at least `'m`
3 Likes

I see the invariant is crucial here. However, consider this variant example:

struct Empty;
impl Drop for Empty{
    fn drop(&mut self) {
        //todo!()
    }
}

pub struct MySend<T>(pub PhantomData<* mut T>,Empty);
impl<T> MySend<T> {
    pub fn send(&self, v: T) -> Option<T> {
        None
    }
}
fn main(){
    let my_send = MySend(PhantomData,Empty);
    let r = Vec::<i32>::new();
    my_send.send(&r); // #2
}

T is still invariant in MySend and Empty has implemented Drop such that MySend should be dropped at the end of the block, however, in this case, the compiler does not complain at #2. What's the reason here? If I change Empty to Empty::<T>, then that error will arise.

By the way, I wonder why the emitted error for the invariance case prefers to report the lifetime is too short. IMO, I consider the invariance will do such things, assuming T is deduced to &'r Vec<i32>, then the receiver should be self: &'l MySend<&'r Vec<i32>>, since the receiver expression has type &'a MySend<&'m Vec<i32>> and &'m Vec<i32> is invariant, we should get an error that &'a MySend<&'m Vec<i32>> cannot be coerced to &'l MySend<&'r Vec<i32>>. Does the compiler prefer to report lifetime error out of considering the error of lifetime is easier to understand than the error of coercion?

Rust’s drop checker is smart enough to see that the Drop implementation of Empty, as part of the “drop glue” of MySend<T> (i.e. the culmination of everything that happens when MySend<T> is dropped), doesn’t have access to any value involving the type T, and hence it allows this type T to contain dangling lifetimes at the time my_send is dropped.

2 Likes

It doesn't, it says the borrowed value (r) doesn't live long enough.

In general, lifetime errors are a best-attempt effort as they are often the result of a constraint satisfaction failure, and the diagnostic must try to find one particular thing to highlight that might have been instrumental in the failure.

In this case, it finds a use of a use of a borrow of r after r has dropped. The borrow could have been shorter if T was covariant, but the use of the invariant borrow can also be avoided by dropping my_send first (e.g. by changing the declaration order). I don't know if it's due to luck or diagnostic logic, but changing the drop order is the more local, more likely to be possible change compared to changing a type declaration (and changing the drop order indeed solves this particular example). So I'd consider it the better advice. It could point out the invariance in addition, I suppose.

So, do you agree with me that this issue can also be subsumed to a "variance" issue, and the compiler can say the receiver expression cannot be coerced to the destination type, right? It is merely that reporting lifetime doesn't live long enough is a more constructive suggestion such that we can fix the compile error according to lifetime.

Again it's not reporting anything about a lifetime not being long enough. But I agree that it could be reported as a variance issue.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.