The current implementation of async fn
and await!
depend on thread_local!
as a work-around to what appears to be a fundamental shortcoming of the existing generators implementation which also prevents these features from being used within !#[no_std]
projects.
An async fn
is currently implemented as a generator that yields whenever it needs to await!
. This generator takes no arguments and produces a final result as one would expect.
When the generator is evaluated, there is no mechanism to pass the future context into the generator and thus no means by which the await!
macro can determine the context under which it should poll its future. This appears to have been done so that an async fn
can be written without any explicit mention of a context for improved ergonomics.
The current implementation works around this by using a thread_local!
pointer to refer to the topmost context in the call stack which can be used by the await!
macro. This alone prevents the async/await from use in !#[no_std]
projects.
It seems that the solution we currently have is an indicator in the shortcomings of the existing generator implementation. The thread-local pointer is doing work that is already achieved by the call stack and the current semantics of yield
do not seem to entirely match the semantics of the problem they were built to solve.
A co-routine yields when it progress becomes dependent on a resource which is currently unavailable. The current semantics of yield
prevent any information being passed back to the generator when that resource becomes ready.
Some existing suggestions have been made for bi-directional moves to occur upon a yield. However the types at the input and output at every yield point in a generator would need to be identical with a known size and static type (assuming no alloc
) which would be neither ergonomic or practical. It would also require that a generator have a different entry-point for being started from that used to resume it.
Instead, I would propose a change to the internal semantics of borrows within a generator. Currently, borrows to data which has a lifetime bound by the instance of the generator are not allowed to live across a yield statement so as to ensure they remain valid (I suspect this is because the entire generator, and thus the variables within its context, can have its location moved between the yield out of the generator and the resumption of the generator).
I think that this is perhaps unnecessarily strict and instead could be replaced with a rule stating that only borrows to data with a lifetime that is not bound by the generator may exist when a generator is resumed. This would imply that borrows may exist after a yield. Furthermore, this would allow for a reference to a value with a lifetime bound by the generator to be yielded (the existing borrow checker rules should enforce that the generator cannot be resumed until all such borrows are dropped).
This would allow for a substantially more useful implementation of await!
that could be used without any dependency beyond libcore
. The await!
would augment its future such that the result is stored inside the generator rather than being lost to its caller and yield a reference to the augmented future. The implementation of poll
for an async fn
would then expect a trait object &mut Future<Output = ()>
whenever the generator yields. It would poll that future to completion in place of the generator (the final result of that future would end up being stored within the generator itself removing any need for dynamic allocation). After completing the intermediate future, poll
would then resume the generator which would use the result of the future that had been completed.
Edit (2018-09-23): The original content of this post is below. It was changed to better frame the issue and a possible solution.
The current implementation for async/await was implemented using thread-local stroage for ease of implementation, however this precludes the use of this feature in #![no_std]
projects.
I’d like to make a pull-request resolving this issue, but it appears there is no actual github issue relating to this. Should I create an issue before making the pull request?