Hello, currently Write in embedded_io_async forces the user of the API to provide a &[u8] which is processed by the trait implementer. But often you want it the other way around: the implementer has a buffer lying around, and the user would like a buffer to write into.
I have already met with cases where crates started creating subtraits, or just diverging from embedded_io_async, to get this. For example, picoserve replaced Write with their own trait, which allowed to shrink endpoint sizes (no temporary buffers, which can also get duplicated due to bad async fn codegen). The migration was easy for us: our tcp sockets are working on top of a packet pool, so instead of getting a slice and copying into the packet, we just give the mutable slice to picoserve. embassy_net::tcp::TcpSocket::write_with is basically this too. Also, smoltcp has a similar design where it is up to Device implementer to provide the buffer.
BorrowedCursor and similar are probably getting stabilized soon (FCP is now), maybe we can think together of an additional opt-in api which can use them?
For std is it may be okay to pass &[u8] everywhere because there is plenty of memory, but on no_std there might not even be an allocator, in order to prevent fragmentation and OOMs.
I suggest adding this to the trait, but I don't know what the best api shape is.
trait Write {
async fn write(&mut self, data: &[u8]) -> Result<usize, Self::Error> {
self.write_with_cursor(|mut cursor| {
let amt = cmp::min(cursor.capacity(), data.len());
cursor.append(&data[..amt]);
Ok(amt)
})
.await
}
async fn write_with_cursor<R>(
&mut self,
writer: impl FnOnce(BorrowedCursor<'_, u8>) -> Result<R, Self::Error>,
) -> Result<R, Self::Error> {
let mut data = [0u8; 256];
let mut buf = BorrowedBuf::from(data.as_mut());
let res = writer(buf.unfilled())?;
self.write_all(buf.filled()).await?;
Ok(res)
}
}
The closure returns Result in case user wants to cancel the write in the middle. Whether should it discard the data the user has already written or no I don't know, for the blank impl I assumed they're discarded. In case user won't ever return Err, monomorphisation will optimize the branch away.
Also, doesn't it look kind of cool? 🤭
Hello, currently
Writeinembedded_io_asyncforces the user of the API to provide a&[u8]which is processed by the trait implementer. But often you want it the other way around: the implementer has a buffer lying around, and the user would like a buffer to write into.I have already met with cases where crates started creating subtraits, or just diverging from
embedded_io_async, to get this. For example,picoservereplacedWritewith their own trait, which allowed to shrink endpoint sizes (no temporary buffers, which can also get duplicated due to badasync fncodegen). The migration was easy for us: our tcp sockets are working on top of a packet pool, so instead of getting a slice and copying into the packet, we just give the mutable slice to picoserve.embassy_net::tcp::TcpSocket::write_withis basically this too. Also,smoltcphas a similar design where it is up toDeviceimplementer to provide the buffer.BorrowedCursorand similar are probably getting stabilized soon (FCP is now), maybe we can think together of an additional opt-in api which can use them?alloc::ioandcore::iorust-lang/rust#154046 (comment)core_io_borrowed_bufrust-lang/rust#117693 (comment)For
stdis it may be okay to pass&[u8]everywhere because there is plenty of memory, but onno_stdthere might not even be an allocator, in order to prevent fragmentation and OOMs.I suggest adding this to the trait, but I don't know what the best api shape is.
The closure returns
Resultin case user wants to cancel the write in the middle. Whether should it discard the data the user has already written or no I don't know, for the blank impl I assumed they're discarded. In case user won't ever returnErr, monomorphisation will optimize the branch away.Also, doesn't it look kind of cool? 🤭