Skip to content

Write without providing a buffer #746

Description

@Ddystopia

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? 🤭

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions