Description
Summary
This proposal proposes to change the way how async api
does implemented. It makes async api
more clearly for use and extend.
Motivation
Our current approach to implement trait primitives as depends on nb
crate. It was actually on start of this project but after version 1.36
rust already have async traits
and methodology for work with it. So in 2018 edition of rust lang we have language words as async
, await
. The marcosses exported by nb
are conflicted with key words, we need to use r#
prefix for each macros use.
This RFC attempts to change implementation to core types
based implementation. It will be more effective and more conform rust language ideas.
Detailed design
This RFC inspired of PR #13, but after apply external crate I redesign for use core functionality because it will be better than external depends.
For example we have interface for serial:
use core::task::Poll;
/// A serial interface
pub trait Serial {
/// Error type associated to this serial interface
type Error;
/// Reads a single byte
fn read(&mut self) -> Poll<Result<u8, Self::Error>>;
/// Writes a single byte
fn write(&mut self, byte: u8) -> Poll<Result<(), Self::Error>>;
}
It use core::task::Poll
as point of async
run status. It was easy for implement and it isn't depends on nb
or another external crates.
Alternatives
Don't implement this RFC.
Uresolved questions:
- More clear examples of right use
async
code. - How we need to update depend crates?
- Do we need to rewrite
nb
prefer than rewriteasync
api ofembedded-hal
?
Activity
Nemo157 commentedon Nov 27, 2019
There are semantics attached to
Poll::Pending
that did not exist onnb::Error::WouldBlock
, specifically:Having functions that return
core::task::Poll
but do not take acore::task::Context
and arrange to wake the task when necessary is likely to lead to deadlocks if they are used by systems built aroundcore::future::Future
.Nemo157 commentedon Nov 27, 2019
Sure, I just want to make sure this isn't missed. I have done a lot of work on async and have experimented with using it on embedded devices in https://github.com/Nemo157/embrio-rs/ including playing around with alternative trait definitions, just want to offer whatever insights I might have (I am lacking in experience with using the existing embedded ecosystem much, I was only really focused on trying it out with async).
If there's a plan for breaking changes to the trait APIs it'd be best to do them all at once, which may involve adding an additional
core::task::Context
parameter to all the traits, and providing extension traits similar to howfutures
is designed:no111u3 commentedon Nov 27, 2019
It looks as real solution for me. I talk about current PR with @Disasm, he says: we can create clear path for migrate from old
nb api
to newasync api
through default implementation ofasync api
.But it may be freeze migration from old to new api.
tustvold commentedon Dec 2, 2019
So the main problem I ran into when trying to do something similar in #149 was that as futures in rust are driven, the returned
Poll<Result<u8, Self::Error>
needs to borrow a reference to the underlying peripheral. Otherwise it would be possible to, for example, interleave writes to the same peripheral - which the current API prevents you from doing.One can work around this by moving the peripheral itself into the future and only returning it on completion, but this makes for an incredibly obtuse API that makes simple constructs such as loops hard.
It is this precise issue with ergonomics that was one of the motivators behind adding native async support to Rust in the first place - see here.
It may be that someone smarter than myself can find solutions to these problems, but thought I'd clarify them here. Depending on the timelines for generalized associated types and generator resume arguments, it might be worth waiting for them to to bring native async support to traits and
[no_std]
respectively, before making breaking changes that would only need to be followed by more breaking changes.Nemo157 commentedon Dec 2, 2019
The returned
Poll
shouldn't need to borrow the peripheral, it should be higher level APIs likefn read(&mut self) -> impl Future<Output = Result<u8, Self::Error>>;
which borrow the peripheral in the returnedFuture
in order to avoid interleaved reads/writes. ThePoll
returning methods are really low-level building blocks which don't need to provide full compile-time guarantees, the majority of users should be accessing them through theFuture
returning functions.One of the nice things about the split between low-level traits of
Poll
functions and high-level utilities returningFuture
used infutures
is that it avoids any need for GATs. And there are a lot of other issues with attempting to create core traits that return futures, see rust-lang/futures-rs#1365 for a lot of relevant discussion.(Generator resume arguments will not affect any public API related to
Future
/async
, it will only mean thatasync fn
becomes usable onno_std
, at least until some very far off day when generators themselves might be stabilised).therealprof commentedon Dec 2, 2019
@Nemo157 Do you have an example for a
Future
executor which can run on embedded? Still trying to figure out our options; it would be kind of nice if we could spawn new futures anywhere and have the rest of the main loop be a poller for all futures. That would probably require a global future queue where interrupts and other functions can add their futures to?tustvold commentedon Dec 2, 2019
@Nemo157 You are completely correct, I had a brain fart and conflated
Poll<Result<u8, Self::Error>>
andimpl Future<Output = Result<u8, Self::Error>>
. GATs are only needed if trying to provide traits with methods that return futures.So is the idea that driver crates would then provide methods returning futures? How would people compose and run these without the afformentioned
async
support forno_std
? You could provide theblock
,await
, etc... macros that thenb
crate provides for its futures, but moving away from these was one of the motivators for the RFC?Edit: I believe you would still need the macros
nb
provides to aid the writing of the driver crates, and then would additionally need to provide an executor implementation for people to run the futures and possibly further facilities to aid in composing them.Nemo157 commentedon Dec 3, 2019
There is https://docs.rs/embedded-executor/0.3.1/embedded_executor/, I haven't used it myself, but it was developed as part of https://gitlab.com/polymer-kb/polymer.
I think moving to
core::future::Future
only makes sense when intended to be used withasync
. The current implementation issue blocking it being used inno_std
has workarounds (core-futures-tls
using ELF thread-locals,embrio-async
for a re-implemented transform, worst case an unsound copy ofcore-futures-tls
that abandons thread-safety). And if there is consensus and experiments in moving some of the embedded ecosystem toasync
that might provide the incentive to get someone to work on fixing the compiler to support it.tustvold commentedon Dec 3, 2019
@Nemo157 So I'm a little bit confused as to what you are proposing exactly, as far as I can see it these are the following options
block
,await
, etc... macros that are currently provided bynb
nb
crate, but named in such a way as to not collideasync
support comes tono_std
allowing writing and interacting with methods returningcore::future::Future
. A utility macro could be provided to facilitate wrapping theembedded_hal
methods returning poll types intocore::future::Future
types that can then be used with asyncnb
are renamed to not collide with keywordsAnd I would argue moving to
core::task::Poll
only really makes sense if you're going to move to usingcore::future::Future
.baloo commentedon Dec 28, 2019
I got a working proof of concept of async/await on a stm32f429 mcu using #172 (comment) approach.
I used:
core-futures-tls
to use astatic mut
to store theContext
stm32f4xx-hal
to it.Being not too knowledgeable about RFC process, should I share?
therealprof commentedon Dec 28, 2019
Yes, please!
baloo commentedon Dec 28, 2019
baloo/pm-firmware@8ccdfd2
Some remarks:
I only implemented support for UART for now, because that's what I used to debug.
I used git submodules to ensure I you get the same versions of the libs I was running.
I reused the example from embrio-rs found here:
https://github.com/Nemo157/embrio-rs/blob/master/examples/apps/hello/src/lib.rs
22 remaining items