Implementation of trait is not general enough when used inside tokio::spawn

I hit this in a reasonably large code base, so tried to strip this down to a somewhat minimal reproduction.

I don't really understand why this breaks inside the spawn block. The full definitions are in the linked playground.

#[tokio::main]
async fn main() {
    // This works fine
    let mut foo = MyWriter { writer: vec![] };
    foo.write(SOME_FOO).await.unwrap();

    // This complains that implementation of `Buf` is not general enough
    tokio::spawn(async move {
        let mut foo = MyWriter { writer: vec![] };
        foo.write(SOME_FOO).await.unwrap();
    })
    .await
    .unwrap();
}

Resulting in

    = note: `&'0 [u8]` must implement `writer::test::Buf`, for any lifetime `'0`...
    = note: ...but `writer::test::Buf` is actually implemented for the type `&'static [u8]`

Ah, we had this sort of issue only recently on the forum. It’s actually related to the Send bound. A terribly annoying compiler bug, not only because it’s unnecessarily struggling to deduce the Send here, but more-so because the error message gives zero indication about it.

Here you can see how the issue comes and goes with a T: Send:

#[tokio::main]
async fn main() {
    // This works fine
    let mut foo = MyWriter { writer: vec![] };
    foo.write(SOME_FOO).await.unwrap();

    send(async move {
        let mut foo = MyWriter { writer: vec![] };
        foo.write(SOME_FOO).await.unwrap();
    });
}

fn send<T>(x: T)
where
    T: Send, // comment out this line and it compiles ...
{
}

One way to fix these quite reliably seems to be to apply some type erasure, and box the right thing, in this case boxing the right Future seems to work well [in the other thread, it was a Stream that could be boxed]:

+ use futures::FutureExt;
…

    tokio::spawn(async move {
        let mut foo = MyWriter { writer: vec![] };
-       foo.write(SOME_FOO).await.unwrap();
+       foo.write(SOME_FOO).boxed().await.unwrap();
    })
    .await
    .unwrap();

If you aren’t using the futures crate (or at least the futures-util part of it), you can of course also do this without the convenience .boxed() method,

    tokio::spawn(async move {
        let mut foo = MyWriter { writer: vec![] };
        (Box::pin(foo.write(SOME_FOO))
            as std::pin::Pin<Box<dyn Future<Output = io::Result<_>> + Send>>)
            .await
            .unwrap();
    })
    .await
    .unwrap();

but the simplicity of .boxed() will of course make it easier - especially when you need to experiment a bit until you find the best place(s) to add it.

3 Likes

I seem to be hitting this same exact issue in my (also rather big) codebase. Could you expand on what exactly is the issue here? And are there any other solutions? My case is pretty hot and I don't think boxing random futures would be acceptable.

I don't really understand the root of the issue either beyond noticing that it's a compiler bug.

Of course it's annoying to introduce boxing and dynamic function calls just for working around it.

I have some ideas of what else to try out, let me get to this later. (Not at home ATM.)

1 Like

Yes, I have actually found a nice solution now.

Trying out a few things, I think I’ve found out that the only actually relevant property of these boxed, type-erased futures/streams – which made them an effective workaround for this compiler bug & error – is that they implement Send unconditionally.

So turns out: a wrapper type struct AlwaysSend<Fut> with an unconditional impl<Fut> Send for AlwaysSend<Fut> (not restricted to Fut: Send) can offer the same effect, even when Fut is still the original future type and some poll method directly delegates to it; without introducing any boxing, or type erasure with dynamic calls; so there should be no overhead at all.

Of course, Send is relevant for soundness :frowning:


But wait!! it suffices to enforce the Fut: Send thing when constructing the wrapper, so there we have a way to encapsulate this workaround wrapper type in a safe API.

[This mirrors the property of dyn Future<…> + Send being always Send itself, but when you construct it, you’ll need to prove the thing you’ve actually put in is Send.]

For simplicity, I’ve just uploaded such a type to crates.io now. The crate even offers a similarly convenient API through its own extension traits FutureExt and StreamExt. So in places where adding a call to .boxed() might help as I’ve explained above, instead a call to .always_send() in the right place(s) should solve your issue equally well :wink:

3 Likes

You really did!! Congrats.

Ah, I finally found a good explanation of the actual underlying issue, too:

rust-lang/rust#102211 (comment)