Inline async for dyn safety

Based on this page from initiative repo.

The core idea is that to make any trait with async methods to work with one future-like coroutine:

  • For all async methods in trait resulting object gets corresponding poll_* method;
  • At the first method call from target trait, we create a single (in a context for a given binding) instance of a state machine which contains all required state for all the async methods:
    • for each method there is one piece of state;
    • these pieces don't intersect => it's UB to poll a different method unless the current polled one is done.
  • Such state machines begin and end their lifecycle at terminal state:
    • Once created, they do nothing unless polled;
    • Given the contract described above has been held, after any of the poll_* methods returned Poll::Ready(..) these automatons proceed to their initial state;
    • In initial state, they must not hold references to self.

This way, we can safely use self between invokations of async methods, and also need to store only one state machine per entire trait, not per its method, also it gives us one associated type in desugaring.

So, we can store state machine's layout in vtable, and then have to "just" tell user a story on how to consume it.

What if the method takes self by value?

trait AsyncTrait {
    async by_value(self);
}

struct Foo {
    bar: String,
    baz: String,
}

impl AsyncTrait for Foo {
    async fn by_value(self) {
        drop(self.bar);
        async_sleep(1).await;
    }
}

At the .await point part of the self became invalid so you can't take a reference of it.

1 Like

Good catch. From outside it should look like we consumed self, and got a future.
So I guess that in this case we make Pin<&mut Self> to appear as Pin<&move Self> in the desugared version of code. And of course, our coroutine has to be dropped after that - there is no reason to keep it around.

And what is this state machine when the async fn is called on a trait object? How much memory does it need to hold?

When we call any of the methods of the trait, the type of returned future needs to somehow poll the state machine => we get a shim over mutable reference to an implicit(AFAIK) instance.

The place for this instance is yet to be decided, but the principle is that we want to return unsized value from the trait object.

Variants for baking storage include boxing, alloca, and inline storage; however, creation of the coroutine state is implicit (good syntax for specifying a place where to return a DST value is yet to be found).

Personally, my bet would use (even not RFCd yet) with clauses to provide capability ObjectPlace: Allocator for creating boxes inside of the referenced allocator, then caller themself decides how to store the object.

Returning an unsized value is a big problem. The idea of the inline async fn was to avoid returning an unsized value, so you brought back all the original problem with all the new problems this approach has (like for example not directly returning an owned Future)

  • Boxing may not be available on #![no_std] and I think it would be controversial to link a language feature as important as async in traits to the stdlib

  • alloca is nowhere near finished: it doesn't support dynamic alignment and can't work in async fns unless they themself begin returning unsized Futures

  • inline storage: I'm not sure what you mean by that but if it's in the parent struct then it's no different from the proposal you linked. So, what are you proposing in addition to that?

How would that work in a #![no_std] context with no allocator support?

I got it, thx.

I just tried to stay be consistent with current style:

async function() -> impl Future - aka invoke to get state machine.

If we diverge, then the linked proposal is what I want.

...I just reinvented the wheel :expressionless:

Sure, but can't the allocation trait itself live in core (like, the trait, not things that require system allocators like Global) ? It's the only trait needed for mechanism to work.

The user still needs to implement it and sometimes that's no an option.

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