Impl Future around a poll method that returns a ref?

I'm trying to wrap tokio's AsyncBufRead::poll_fill_buf into a Future. This example is about as close as I've gotten, but I'm having trouble getting the compiler to agree.

The core issue seems to be that the lifetime of the buffer, I think, is tied to the lifetime of the Pin, and that's a local variable, and so, that doesn't live long enough. In reality, I know the buffer is part of the AsyncBufRead, and I think it must be guaranteed that that lives long enough since it comes into the function in the Pin?

Is this sort of thing — where the underlying poll returns a & — not possible to wrap in a future?

(tokio does provide an Ext trait that implements some async fns, but they require copying; I'd like the internal buffer of the AsyncBufRead to be the only buffer necessary; sometimes I will parse some data at one point, and need to pass what remains to another, and the internal buffer provides a nice way to carry that data along w/ the Read if I don't completely exhaust the buffer.)

The issue is essentially that when you are inside the poll method, then ignoring pin, you have an

&'b mut &'a mut R

where 'b is the lifetime on self. Now you have to understand that a mutable reference is really more about unique access than mutable access, and unfortunately once 'b ends, you no longer have unique access to the underlying R (someone else has), therefore you can't return a reference into R that lasts for the full 'a lifetime.

To show how it goes wrong, notice that your future doesn't care about being polled to completion twice. For example, you could do this:

#[tokio::main]
async fn main() {
    let mut cursor = Cursor::new(vec![1, 2, 3, 4, 5]);
    
    let mut fill = cursor.fill_buf();
    
    let buf_a: &[u8] = (&mut fill).await.unwrap();
    let buf_b: &[u8] = (&mut fill).await.unwrap();
    
    println!("{:?}", buf_a);
    println!("{:?}", buf_b);
}

This is undefined behavior because inside the second .await, you are creating a mutable reference to R while the immutable reference buf_a still exists and points inside R. This is not allowed since &mut must be unique.

This is not just theoretical. If you transmute the lifetimes and ignore the compiler, the code will actually segfault, as you can see in this playground. Actually it doesn't segfault, let this be a lesson that you should not use transmute.

Regardless, consider what would happen if the poll_fill_buf function on R reallocates the memory it returns on each call? Then the previous buffer would be invalidated, and R is allowed to do this, because calling it requires a &mut R, thus promising that its access is unique.

2 Likes

Thank you; that's a better example. I had a gut feeling the compiler was right¹, but that example shows why it doesn't work much more eloquently.

I think I might send the tokio devs a question about whether AsyncBufRead should be usable in this way (be able to fill and more easily read out of the internal buffer from an async fn; e.g., I think splitting out getting the buffer / consuming the buffer from the task of filling the buffer would make what I'm trying to do very easy, but it would require breaking changes to the trait.

¹it always seems to be in the end :slight_smile:

This is actually something I noticed (in futures variant of the trait) while reviewing this PR. To have a fill_buf future would probably require something similar returning the &mut Self in the pending case.

You could make a future that runs a (non-async) closure on completion instead of yielding the buffer.

Some other things I've noticed: tokio::io::BufReader will let me get at the underlying buffer ­— it has a fn buffer(&self) -> &[u8] method. But BufStream… does not. It's just got a BufReader under the hood, too.

To have a fill_buf future would probably require something similar returning the &mut Self in the pending case.

I was thinking you could,

#[async_trait]
trait AsyncBufRead {
    async fn fill_buf(&mut self) -> Result<(), ?>;
    fn buffer(&self) -> &[u8];
}

That is, side step the whole "Future returning a lifetime" issue that I ran into by just separating out getting the buffer from filling the buffer. I feel like it works better too, b/c it lets code that just wants to know "is there anything for me to parse?" to get at the buffer w/o necessarily doing I/O.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.