-
Notifications
You must be signed in to change notification settings - Fork 216
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
async: add SPI #347
async: add SPI #347
Conversation
r? @ryankurte (rust-highfive has picked a reviewer for you, use r? to override) |
363: async/i2c: fix lifetimes on transaction() r=ryankurte a=Dirbaio Trying to implement i2c for a shared i2c bus behind an async mutex yielded lots of cursed lifetime errors, because `&'a mut [Operation<'b>]` is invariant on `'b`, not covariant as one would expect... To fix this, the GAT future needs two lifetimes. Also counterintuitively, the future must be `+ 'a`, but NOT `+ 'b`. Then `AddressMode: 'static` is needed because Rust wants annoying `where A: 'a` bounds otherwise. The async SPI PR has the same issue, will fix later. #347 With these fixes, implementing i2c on a mutex works nicely now: ```rust struct SharedI2c<T>(tokio::sync::Mutex<T>); impl<T: ErrorType> ErrorType for SharedI2c<T> { type Error = T::Error; } impl<A: AddressMode, T: I2c<A>> I2c<A> for SharedI2c<T> { type ReadFuture<'a> where Self: 'a, = impl Future<Output = Result<(), Self::Error>> + 'a; fn read<'a>(&'a mut self, address: A, read: &'a mut [u8]) -> Self::ReadFuture<'a> { async move { self.0.lock().await.read(address, read).await } } type WriteFuture<'a> where Self: 'a, = impl Future<Output = Result<(), Self::Error>> + 'a; fn write<'a>(&'a mut self, address: A, write: &'a [u8]) -> Self::WriteFuture<'a> { async move { self.0.lock().await.write(address, write).await } } type WriteReadFuture<'a> where Self: 'a, = impl Future<Output = Result<(), Self::Error>> + 'a; fn write_read<'a>( &'a mut self, address: A, write: &'a [u8], read: &'a mut [u8], ) -> Self::WriteReadFuture<'a> { async move { self.0.lock().await.write_read(address, write, read).await } } type TransactionFuture<'a, 'b> where Self: 'a, 'b: 'a, = impl Future<Output = Result<(), Self::Error>> + 'a; fn transaction<'a, 'b>( &'a mut self, address: A, operations: &'a mut [Operation<'b>], ) -> Self::TransactionFuture<'a, 'b> { async move { self.0.lock().await.transaction(address, operations).await } } } ``` cc `@matoushybl` Co-authored-by: Dario Nieuwenhuis <[email protected]>
Pushed new version
|
New version
I've had LOTS of trouble with the "async closure". The right bound would be something like The workaround is to use the outer This has the nasty implication that it's no longer possible to use It's not possible to add the helper default methods to blocking: let mut buf = [0;64];
device.transaction(|bus: &mut T::Bus| {
bus.write(&[0x42])?;
bus.read(&mut buf)?;
Ok(())
}).unwrap() async: let mut buf = [0;64];
let res: Result<(), T::Error> = device.transaction(|bus: &mut T::Bus| async {
if let Err(e) = bus.write(&[0x42]).await {
return (bus, Err(e));
}
if let Err(e) = bus.read(&mut buf).await {
return (bus, Err(e));
}
(bus, Ok(())
}).await;
res.unwrap() Not pretty, but it works. I'm afraid it's the best we can do with today's async traits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohh wow that gets gnarley with the bus callbacks huh. i'm a little cautious that folks are going to have bad experiences trying to use it but, seems well documented, and given it is -for now- it doesn't seem like there's a better option (and your examples look great!).
in my cursed async experience sometimes things can be simplified by concretising async return types, and using &'static dyn
dispatch, however, only sometimes and i don't really have the time to bash my head on it at the moment...
You can do it with Also, in the long-term, first-class support for async closures will make this even more ergonomic. I've been looking deeper at this. It turns out it is possible to express where
for<'b> F: FnOnce<(&'b mut Bus,)>,
for<'b> <F as FnOnce<(&'b mut Bus,)>>::Output: Future<Output = Result<R, Bus::Error>> + 'b, but there's a few issues:
It's likely this will improve in the future, we can change the trait to take advantage of whatever solution comes up later... |
Rebased on latest master. Update on the "Rust's current trait solver isn't smart enough" issue above: By reducing the Seems the issue is bounds on higher-ranked associated types don't work in general. There's quite a few issues open, closest is maybe rust-lang/rust#23481 . Seems it's blocked on "lazy normalization": rust-lang/rust#60471 . These bounds all work fine with Chalk, so maybe they just plan to just wait for Chalk...? Either way, this is going to take quite a while... I think it's best to take the unfortunate usability hit for now, and not block this. When either Chalk, lazy normalization, or first-class async closures land, we'll be able to remove the hack (with a breaking change, of course...). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! The code is difficult to grasp, though.
I just added a few nitpicks but it looks good to me.
hugely appreciate your effort in exploring these options @Dirbaio! 👍 to landing this once those small changes are applied. |
New version
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, thanks! Let's merge #371 and then this one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, thanks!
bors r+
380: async/spi: replace the "give back the Bus" hack with a raw pointer. r=ryankurte a=Dirbaio The async SPI trait contains a "hack" to workaround limitations in Rust's borrow checker: #347 It turns out the hack doesn't allow many shared bus implementations, for example when using an async Mutex: The `transaction` method gets `&'a mut self`, and the closure wants `&'a mut Self::Bus`. This only works if the Bus is a direct field in `self`. In the mutex case, the Bus has to come from inside the `MutexGuard`, so it'll live for less than `'a`. See https://gist.github.com/kalkyl/ad3075182d610e7b413b8bbe1228ab90 which fails with the following error: ``` error[E0597]: `bus` does not live long enough --> src/shared_[spi.rs:78](http://spi.rs:78/):34 | 63 | fn transaction<'a, R, F, Fut>(&'a mut self, f: F) -> Self::TransactionFuture<'a, R, F, Fut> | -- lifetime `'a` defined here ... 78 | let (bus, f_res) = f(&mut bus).await; | --^^^^^^^^- | | | | | borrowed value does not live long enough | argument requires that `bus` is borrowed for `'a` ... 89 | } | - `bus` dropped here while still borrowed ``` This is an alternative hack. If lifetimes don't work, simply don't use lifetimes at all! - Downside: it needs `unsafe{}` in all callers of transaction (but these should be rare, many users will use the `SpiDeviceExt` helpers. - Upside: it's now possible to write sound shared bus impls. - Upside: it no longer requires the "give back the bus" hack, it's now possible again to use `?` inside the closure for error handling. cc `@kalkyl` Co-authored-by: Dario Nieuwenhuis <[email protected]>
This is an exact mirror of the blocking SPI trait (including the proposed changes in #351 ), except the following differences:
W: 'static
is required everywhere, otherwise complicated lifetime bounds are required in the future GATs.