Async read/write
Impact
- Able to abstract over "something readable" and "something writeable"
- Able to use these traits with
dyn Trait
- Able to easily write wrappers that "instrument" other readables/writeables
- Able to author wrappers like SSL, where reading may require reading and writing on the underlying data stream
Design notes
Challenge: Permitting simultaneous reads/writes
The obvious version of the existing AsyncRead
and AsyncWrite
traits would be:
#![allow(unused)] fn main() { #[repr(inline_async)] trait AsyncRead { async fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>; } #[repr(inline_async)] trait AsyncWrite { async fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>; } }
This form doesn't permit one to simultaneously be reading and writing. Moreover, SSL requires changing modes, so that e.g. performing a read may require writing to the underlying socket, and vice versa. (Link?)
Note also that using std::io::Result
would make the traits unusable in #[no_std]
(this is also the case with the regular Read
and Write
traits), which might preclude embedded uses of these traits. These fundamental traits could all be added to alloc
(but not core
, because std::io::Error
depends on Box
).
Variant A: Readiness
One possibility is the design that CarlLerche proposed, which separates "readiness" from the actual (non-async) methods to acquire the data:
pub struct Interest(...); pub struct Ready(...); impl Interest { pub const READ = ...; pub const WRITE = ...; } #[repr(inline)] pub trait AsyncIo { /// Wait for any of the requested input, returns the actual readiness. /// /// # Examples /// /// ``` /// async fn main() -> Result<(), Box<dyn Error>> { /// let stream = TcpStream::connect("127.0.0.1:8080").await?; /// /// loop { /// let ready = stream.ready(Interest::READABLE | Interest::WRITABLE).await?; /// /// if ready.is_readable() { /// let mut data = vec![0; 1024]; /// // Try to read data, this may still fail with `WouldBlock` /// // if the readiness event is a false positive. /// match stream.try_read(&mut data) { /// Ok(n) => { /// println!("read {} bytes", n); /// } /// Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { /// continue; /// } /// Err(e) => { /// return Err(e.into()); /// } /// } /// /// } /// /// if ready.is_writable() { /// // Try to write data, this may still fail with `WouldBlock` /// // if the readiness event is a false positive. /// match stream.try_write(b"hello world") { /// Ok(n) => { /// println!("write {} bytes", n); /// } /// Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { /// continue /// } /// Err(e) => { /// return Err(e.into()); /// } /// } /// } /// } /// } /// ``` async fn ready(&mut self, interest: Interest) -> io::Result<Ready>; } pub trait AsyncRead: AsyncIo { fn try_read(&mut self, buf: &mut ReadBuf<'_>) -> io::Result<()>; } pub trait AsyncWrite: AsyncIo { fn try_write(&mut self, buf: &[u8]) -> io::Result<usize>; }
This allows users to:
- Take
T: AsyncRead
,T: AsyncWrite
, orT: AsyncRead + AsyncWrite
Note that it is always possible to ask whether writes are "ready", even for a read-only source; the answer will just be "no" (or perhaps an error).
Can we convert all existing code to this form?
The try_read
and try_write
methods are basically identical to the existing "poll" methods. So the real question is what it takes to implement the ready
async function. Note that tokio internally already adopts a model very similar to this on many types (though there is no trait for it).
It seems like the torture case to validate this is openssl.
Variant B: Some form of split
Another alternative is to have read/write traits and a way to "split" a single object into separate read/write traits:
#![allow(unused)] fn main() { #[repr(inline_async)] trait AsyncRead { async fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>; } #[repr(inline_async)] trait AsyncWrite { async fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>; } #[repr(inline_async)] trait AsyncBidirectional: AsyncRead + AsyncWrite { async fn split(&mut self) -> (impl AsyncRead + '_, impl AsyncWrite + '_) } }
The challenge here is to figure out exactly how that definition should look. The version I gave above includes the possibility that the resulting readers/writers have access to the fields of self
.
Variant C: Extend traits to permit expressing that functions can both execute
Ranging further out into unknowns, it is possible to imagine extending traits with a way to declare that two &mut self
methods could both be invoked concurrently. This would be generally useful but would be a fundamental extension to the trait system for which we don't really have any existing design. There is a further complication that the read
and write
methods are in distinct traits (AsyncRead
and AsyncWrite
, respectively) and hence cannot
#![allow(unused)] fn main() { #[repr(inline_async)] trait AsyncRead { async fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>; async fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>; } #[repr(inline_async)] trait AsyncWrite { } #[repr(inline_async)] trait AsyncBidirectional: AsyncRead + AsyncWrite { async fn split(&mut self) -> (impl AsyncRead + '_, impl AsyncWrite + '_) } }
Variant D: Implement the AsyncRead
and AsyncWrite
traits for &T
In std
, there are Read
and Write
impls for &File
, and the async-std runtime has followed suit. This means that you can express "can do both AsyncRead + AsyncWrite
" as AsyncRead + AsyncWrite + Copy
, more or less, or other similar tricks. However, it's not possible to do this for any type. Worth exploring.