Sync + Send + 'static futures for service providers with interior mutability

Hi team!

I've been busy Rusting. I wanted my futures to be as useful as possible and in the process I came up with an easy to use shortcut which, I believe, is suitable for wider audience. It is a habit I brought from C# to abstract services away behind interfaces traits to make the code modular. For this to work out I needed my services to be easily shared => immutable reference, but able to take advantage of shared resources => interior mutability, the futures safe to send between threads so they can be part of other futures/async IO => Send and Sync.

I wrote Potential for which I'd appreciate your opinion and review. I've also tried to improve async-trait, PR pending, to be able to selectively mark futures Sync with #[future_is[Sync]].

Please do tell me (kindly) if I'm doing something silly. I did consider the implications of using a Mutex in Potential, possible deadlocks, performance penalty... I also paused to think about relying on drop() for application logic (the Lease sends it's item back to the Potential on drop) and factored in some recovery options and docs. Your thoughts?

Last thing bugging me is that I cannot make a sweet async fn that returns a static future when taking in &self reference. See the example in Potential. While it is possible to construct such a function without the async sugar, async fns/blocks will capture the &self reference resulting in a compiler upset over lifetimes. Being able to return 'static futures from &self would be such a neat completion of the async-trait sugar candy story. I understand that async, and async traits especially, are hard. The async fn is represented by a state machine running the future to completion over the await points which have to capture the state... But a future setup before the await point does not necessarily have to be captured as demonstrated in my example. The very much beloved compiler could perhaps somehow detect the setup and roll it inline without the need to capture everything that enters an async block. Possible?

Thank you.

I'm not really sure what your question is.

No, you have to use the impl trait syntax for that.

Hi @alice, regarding async fn sugar for returning static futures from &self, the question is rather hypothetical, if it is feasible or useful to have rust treat the future setup differently - that is run it as part of the calling code / inline - not sure how to word it - instead of deferring it to the first poll. As in that case not all input, specifically here the &self reference wouldn't need to be captured. But the more important questions are about the code I wrote, asking for code review and feedback.Thank you.

The fact that

async fn name<'lifetime> (self: &'lifetime Self, ... )
  -> R
{ ... }

unsugars to:

fn name<'lifetime> (self: &'lifetime Self, ...)
  -> impl 'lifetime + Future<Output = R>
{ async move { ... } }

is by design.


For the returned Future to be 'static, it cannot be referring to self, but to some field of self it has taken ownership of, before becoming a lazily pollable Future:

struct S {
    copyable: i32,
    clonable: Vec<Item>,
    retainable: Arc<SomeField>, // as far as Rust is concerned, this is the same as cloneable
}

impl S {
    fn copyable (self: &'_ Self) -> impl 'static + Future<Output = ...>
    {
        let copy = self.copyable;
        async move {
            ... // does not reference `self` anymore
        }
    }
    fn copyable_wrong (self: &'_ Self) -> impl 'static + Future<Output = ...>
    {
        async move {
            // references `self: &'_ Self`, so the future itself is `'_` rather than `'static`
            let copy = self.copyable;
            ... // does not reference `self` anymore
        } // <- Error, conflicting lifetime requirements...
    }

    fn cloneable (self: &'_ Self) -> impl 'static + Future<Output = ...>
    {
        let clone = self.cloneable.clone();
        async move {
            ... // does not reference `self` anymore
        }
    }

    fn retainable (self: &'_ Self) -> impl 'static + Future<Output = ...>
    {
        let owned_handle = Arc::clone(&self.retainable);
        async move {
            ... // does not reference `self` anymore
        }
    }
}

As you can see, there needs to be some setup code before async move immediately starts, otherwise self: &'_ Self is captured, and thus the returned Future is only allowed to live as long as '_, the lifetime of the borrow over *self.

This means that even if the language wanted to, it would not be possible to have async fn (self: &'_ Self, ...) unsugar to a function that returns an impl 'static + Future....

  • It could be possible if the body of the function did not access self at all, but then:

    • Why have it be a function over self: &'_ Self?

    • More generally, Rust does not want function prototypes / signatures / public APIs to depend on what an actual implementation is, since it can very easily lead to API breakage without the author noticing.


In the case you have some pattern such as the one with retainable, which is quite common:

struct FooFields {
    some_field: (/* ... */),
    // ...
}
impl FooFields {
    /// The yielded future gets to be `'static` since `Arc<Self> :  'static`. 
    async fn foo (self: Arc<Self>, ...) -> R
       // fn foo (self: Arc<Self>, ...) -> impl 'static + Future<Output = R>
    {
        /* body  */
    }
}

pub
struct Foo /* = */ (
    Arc<FooFields>,
);

impl Foo {
    pub
    fn foo (self: &'_ Foo, ...) -> impl 'static + Future<Output = R>
    {
        self.0.clone().foo()
    }
    /// Or:
    pub
    fn foo (self: &'_ Foo, ...) -> impl 'static + Future<Output = R>
    {
        let foo_fields = Arc::clone(&self.0);
        async move {
            foo_fields.foo().await
        }
    }
    /// Or:
    pub
    fn foo (self: &'_ Foo, ...) -> impl 'static + Future<Output = R>
    {
        let foo_fields = Arc::clone(&self.0);
        async move {
            /* inlined body of `FooFields::foo()` */
        }
    }
}

And this last foo() implementation is very common, and yet a bit cumbersome to write.
If that happens to you a lot, then having a helper macro could come in handy:

impl Foo {
    #[static_future(setup = {
        let this = Arc::clone(&self.0);
    })]
    pub
    async fn foo (self: &'_ Foo, ...) -> R
    {
        /* inlined body of `FooFields::foo()` */
    }
}

This hypothetical macro would allow expressing the same semantic information as the explicitly delayed async move { ... } block, but would:

  • reduce a bit the boilerplate of the function signature,

  • avoid the extra rightward drift for the "inlined body",

1 Like

Thanks @Yandros for the evaluation.

Moving the setup outside the function would be doable I think, though a quite unusual... A setup macro at the beginning of the function body or at the boundary could be more user friendly:

impl Foo {
    pub async fn foo (self: &'_ Foo, ...) -> R
    {
        let this = Arc::clone(&self.0);
        async_setup_ready!(); // async setup boundary
        /* inlined body of `FooFields::foo()` */
    }
}

I've just tried to minimize:

use core::future::Future;

fn main() {
    // Surprisingly, this future is static
    let fut = theory(&Useful(true));
    is_static(fut);
    // The future will run
    let fut = theory(&Useful(true));
    futures::executor::block_on(fut);

    // Proposal
    let useful = Useful(true);
    let fut = theory_proposal(&useful);
    is_static(fut);

    // But this is what we want
    let useful = Useful(true);
    let fut = theory(&useful);
    // this fails
    //is_static(fut);
}

fn is_static<T: 'static>(_: T) {}

#[derive(Clone)]
struct Useful(bool);

impl Useful {
    async fn is_useful(&self) -> bool {
        self.0
    }
}

async fn theory(useful: &Useful) -> bool {
    useful.clone().is_useful().await
    // possibly other code
}

fn theory_proposal(useful: &Useful) -> impl 'static + Future<Output = bool> {
    let input = useful.clone();
    async move {
        input.is_useful().await
        // possibly other code
    }
}

I can see the problem clearly now in theory_proposal. The compiler would have no idea what is the intended async setup. I initially thought the code could be split on the first await. But if I created the is_useful() future outside the async block, it would capture a temporary value and fail to compile. Splitting after clone() like this works, but the compiler would not be able to figure that out.

Here is a Playground implementing that:

impl Foo {
    static_future! {
        pub async fn foo (self: &'_ Foo, /* ... */) -> R
        {
            let this = Arc::clone(&self.0);
            async_setup_ready!(); // async setup boundary
            this.r
        }
    }
}

const _: () = {
    fn check (foo: &'_ Foo, /* ... */)
    {
        let fut = foo.foo();
        fn is_static<Fut : 'static> (_: &Fut) {}
        is_static(&fut);
    }
    let _ = check;
};

which, with #[macro_rules_attribute], can be written as:

impl Foo {
    #[macro_rules_attribute(static_future!)]
    pub async fn foo (self: &'_ Foo, /* ... */) -> R
    {
        let this = Arc::clone(&self.0);
        async_setup_ready!(); // async setup boundary
        this.r
    }
}
1 Like

:astonished: Impressed! This peace you've just produced would not only fix my dilemma with async-trait, but it is probably very useful in general. And it is a great lesson in writing macros. Thank you.

1 Like

I've applied the idea to async_trait and it works.

The trait:

#[async_trait]
pub trait TcpService<IO> {
    #[future_is[Send + Sync + 'static]]
    async fn handle(&self, io: Result<IO>, connection: ConnectionInfo) -> Result<()>;
}

And the impl:

#[async_trait]
impl<S, P, IO> TcpService<IO> for SmtpService<S, P, IO>
where
    S: SessionService<SessionInput<IO, Arc<P>>> + Send + Sync + 'static,
    S::StartFuture: Sync + Send,
    S::Session: Sync + Send,
    IO: MayBeTls + Read + Write + Unpin + Sync + Send + 'static,
    P: Parser + Sync + Send + 'static,
{
    #[future_is[Send + Sync + 'static]]
    async fn handle(&self, io: Result<IO>, conn: ConnectionInfo) -> Result<()> {
        let fut = handle_smtp(self.session_service.clone(), self.parser.clone(), conn, io);
        async_setup_ready!();
        fut.await
    }
}

Thanks a lot for your support.

1 Like