-
Notifications
You must be signed in to change notification settings - Fork 219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plan for core::future #149
Comments
So I'll caveat all the below with the fact I'm relatively new to rust and so what I say may be incorrect, but following further experimentation I do not believe it is yet possible for embedded_hal to make use of This is not to say that these types can't be used in embedded, far from it - it would be clunky but it is perfectly possible to use them in an embedded context, however, the specific constraints imposed by the design of embedded_hal, namely its use of traits and non static lifetimes, make this at least beyond my capabilities. The easiest way to justify this, is to explain what I did, the problems I faced and how I bodged around them. My first step was to build an "executor" that can run futures in an embedded environment. I don't believe it is possible to implement something like the executors in the standard library as the spawn interface seems to require dynamic memory allocation. However, it is relatively straightforward to write methods that can drive a Next I extended the traits within embedded_hal to use associated types to represent the future return types. I then tried to implement these traits within stm32f4xx_hal, however, immediately ran into an issue when the future needs to borrow a reference to the underlying peripheral for the lifetime of the future. Currently generic associated types are not close to stable, and so I bodged around this by making each of the embedded_hal traits generic over a lifetime parameter, and using this to parameterise the future associated type. The default blocking implementations required some HRTB magic to work, but I managed to make it work. At this point I had a working stm32f4xx_hal and thought I was in the home stretch, however, I quickly discovered that trying to compose these futures was immensely fiddly. Not only does the somewhat hacky approximation to a generic associated type work against you, but I ended up having to use unsafe in order to preserve both the returned future and the peripheral it was borrowing across a It was at this point that I threw in the towel, I realised I was trying to bodge together something that was complicated enough to have warranted inclusion in the language itself. Ultimately for embedded_hal to be able to use the new API it at a minimum needs generic associated types and the ability to resume generators with an argument. In practice these two features are likely to coincide with embedded async and async functions in traits, at which point the implementation becomes relatively trivial. I will leave this issue open, but I won't be working on it further unless something changes. |
@tustvold Thank you for looking into this, and for writing this extensive report! I'd like to add my own experience to this bit:
If I had done this investigation, I would have stopped right here, unless I'd found an alternative to the borrowing. Experience has shown that this kind of borrowing is problematic in practice. In the past, I've implemented my own future-like API (really basic, no thought spent on composing etc.) for the DW1000 driver. A call to The problem starts when you want to build an abstraction that requires the Maybe some language wizard will weigh in here and explain how that's all easy to solve using For the simple examples this doesn't change much, except that they need an additional variable for sending/receiving, and need to assign the driver back to the original variable after completion. The complex application should be able to encapsulate the driver struct in an enum, with one variant for each of the states. Here's what the API looks like today: https://github.com/braun-embedded/rust-dw1000/blob/ceca46f9657fa09cba32b52b3a872b7a7108613e/src/hl.rs @tustvold Do you think this approach could help with a futures-based API? On first glance, it looks like we could get the original struct back from the future via |
Yeah... I lack the experience with Rust to make those sorts of judgement calls :p That being said it did work quite well until I tried to wrap it with a trait.
This is the exact issue that resulted in me throwing in the towel. You can work around it with Pin, as I did, but it is incredibly fiddly and I'm not totally sure what I've written is safe. Generators and by extension async solve this, but at the moment don't support embedded nor traits.
This is possible but unless I'm missing something doesn't compose well. Say for example you wanted to wrap the DW1000 driver in a TDOA abstraction, or wanted to share its SPI bus with another component, neither of these would be trivially possible. I think an approach similar to the one described here where the peripheral is moved into the future and returned on completion might work. I'll have a play around this evening after work and report back. |
That's why I'm here: To share the insights gained from my years-long (and ongoing) series of mistakes :-)
I don't see how wrapping the driver in an abstraction is a problem, as that's the exact use case that this design is supposed to enable. But good point about sharing the SPI bus, I hadn't really considered that. I guess one could create a representation of the shared bus that can move with the driver, similar to Lots of stuff to figure out in this space :-)
Yeah, that looks kind of what I had in mind. Looking forward to see what you come up with! |
So one potential downside of moving the peripheral into the futures is that even the blocking APIs and usages can't borrow the values. The result is
Becomes
It's also worth considering that whilst most HAL types should be lightweight to move, this isn't necessarily a guarantee. Nothing immediately springs to mind as an obvious use case for data storage within the peripheral, as opposed to the returned future, but I'm sure there must be a valid use case... |
Would it be possible to provide multiple methods, one that borrows, one that moves? Maybe in such a case the Not sure if any of that is practical. Just throwing out ideas. |
@tustvold excellent writeup, thanks for exploring this! i've been writing a bunch of non-embedded futures code recently and, as interesting as the concept is, i don't think they're a good option for embedded. it's possible that the ergonomics will improve significantly in the future, but, at this point in time there are a bunch of issues and hidden complexities that ime make complex applications with futures in rust vastly more difficult than just writing naturally async systems. your writeup covers most of the embedded issues, the dynamic allocation and ergonomic pains that can only be solved with gratuitous use of
there are a couple of examples of move-into-future in tokio and it seems the only way to ergonomically use them is to immediately wrap them with mpsc::channels so you can store and move references around. i think this has the same composability issues as type-state based futures.
as above, if the hal is futures-based the consumers will be pushed to be to, and drivers with buffers etc. are often not lightweight. it might be possible to work around this with
@hannobraun i have also been exploring the... limits ... of type-state programming with radio devices, for configuration rather than operating state. thanks for sharing your example ^_^ i had a very brief look at futures for the same |
My thinking was that such buffers and other related state would likely be stored within the future object as opposed to the peripheral itself, but I agree that there is almost certainly some use case that would result in a chunky peripheral that is costly to move.
I can't agree with this more, it currently suffers from the same problem as C++ libraries like Boost.Asio - fantastically powerful but mind bending to use in practice. First-class language support to make this less painful is pretty much a necessity.
And not only this but they don't really result in an abstraction that is particularly compelling - it is more restrictive and harder to reason about than the current API and the signature feature of interrupts isn't prevented by the current API, it is just out of scope and left to the HAL to handle. It's trying to provide a middle ground between bare-metal and an RTOS and I'm not really sure that's a good idea. It's also worth considering that changes to embedded_hal are likely to be incredibly complicated and given the fact that native async will eventually make its way to embedded and traits, we'd likely end up suffering this pain twice. |
@tustvold @ryankurte You talked about "naturally async" and "native async". As someone who's been following the async story from afar, what would that involve? async/await? Generators? Something else?
Any idea what that would look like? Maybe any articles I could read? |
by "naturally async" i just mean code that is inherently non-blocking, the standard i'm not sure what @tustvold means here by "native async", i guess the finalisation of async support (and custom generators etc. to support other uses of them) in rustc? |
@ryankurte I indeed meant the stabilization of async support in rustc, with both embedded support and the ability to use them in traits. |
cc new RFC for this: #172 |
I think y'all are ahead of me on this, but, had a bash at implementing futures over natually async traits, was okay except for futures traits not yet being available, TLS not being available on embedded platforms, and generators using |
now that |
Currently the traits within embedded-hal make use of the nb crate's flavour of async IO, however,
core::future
andcore::task
have recently been standardised and I believe are slated for inclusion in Rust 1.38. As such I was wondering whether migration towards using them would be something the maintainers of this project would be open to?In particular use of
core::task::Waker
opens up the possibility for HAL crates to use interrupts to signal when they are ready to make progress, avoiding the busy waiting that appears to be largely unavoidable with the current interface.I'm aware that I'm not the first person to wonder this, see here, but I couldn't find an issue discussing it. The closest I could find was on the nb crate itself here.
If there is interest in this course of action I'm happy to look into writing a PoC, but I'm acutely aware there is, understandably, some caution around making breaking changes to the traits - see here.
The text was updated successfully, but these errors were encountered: