-
Notifications
You must be signed in to change notification settings - Fork 20
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
[ACP] IO traits in core::io
#293
Comments
demo of moving |
I understand the desire to enforce a common error for types that implement Assuming that we don't go the path of having a |
The problem with giving trait Read {
type Error;
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error>;
}
trait Write {
type Error;
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error>;
}
trait Socket: Read + Write {
// error[E0221]: ambiguous associated type `Error` in bounds of `Self`
fn set_nodelay(&self, nodelay: bool) -> Result<(), Self::Error>;
}
// error[E0221]: ambiguous associated type `Error` in bounds of `S`
fn netboot<S: Socket>(tftp_socket: &mut S) -> Result<(), S::Error> { ... } You end up having to give a new name to the error type at each level of trait definition, which gets out of hand quickly and causes diffs to get really unpleasant when refactoring. trait Socket: Read<Error = Self::SocketError> + Write<Error = Self::SocketError> {
type SocketError;
}
fn netboot_v1<S, E>(tftp_socket: &mut S) -> Result<(), E>
where S: Read<Error = E> + Write<Error = E>,
{ ... }
// forced to change the error type for a minor refactoring
fn netboot_v2<S: Socket>(tftp_socket: &mut S) -> Result<(), S::SocketError> { ... } In principle I would want to define the trait<E> Socket: Read<Error = E> + Write<Error = E> {} And trying to imitate it with type parameters on the trait ( trait Socket<E>: Read<Error = E> + Write<Error = E> { }
/*
error[E0221]: ambiguous associated type `Error` in bounds of `S`
| type Error;
| ---------- ambiguous `Error` from `Read`
| type Error;
| ---------- ambiguous `Error` from `Write`
*/
fn netboot<E, S: Socket<E>>(tftp_socket: &mut S) -> Result<(), S::Error> { ... } |
Well composition is not impossible, it just becomes annoying. You could do something like this trait Socket: Read + Write {
fn set_nodelay(&self, nodelay: bool) -> Result<(), <Self as Read>::Error>;
} Or if it can return both errors, something like this enum SocketError<R, E> {
ReadError(R),
WriteError(E),
}
trait Socket: Read + Write {
fn set_nodelay(&self, nodelay: bool) -> Result<(), SocketError>;
} Other than this point there are a couple of things, I can think of
|
Right, I think that wanting to reuse the same error for pub enum MyError<R, W> {
Read(R),
Parse(ParseError),
Write(W),
} If we added (but didn't enforce) an fn read_parse_write<
R: Read<Error: Into<IoError>>,
W: Write<Error: Into<IoError>>,
>(
read: &mut R,
write: &mut W,
) -> Result<(), IoError>; |
This signature is incorrect, because in a design with separate error types for categories of I/O operations then
I understand this works from the "it typechecks and compiles" perspective, but it's not an API you'd want to work with in real life. The low-level I/O traits don't have enough information to correctly assign semantics to specific error codes -- that requires some higher-level (usually OS-specific) context.
You'd need adapter types, since otherwise there's a lot of thorny questions about trait coherency. For example On the plus side, wrapper structs
The
As written above, I think the proposed traits would sit below the layer at which semantics can be assigned to error codes. You'd need the type itself to tell you what the semantics are. For example, you could imagine something like this: enum IoError {
Read(ReadError),
Write(WriteError),
Other(i32),
}
struct Serial { ... }
impl core_io_traits::IO for Serial { type Error = IoError; }
impl core_io_traits::Read for Serial { ... /* read() returns IoError::Read on error */ } So the trait itself doesn't mandate the structure of the error codes, but the implementation (which is likely to be platform- and consumer-specific) would. |
This feels like it's a design proposal for an ACP that moves Currently, there are no traits in The code structure I want to enable is something like this: // crate that implements a kernel
struct Serial;
struct IoError(i32);
impl core_io_traits::IO for Serial { type Error = IoError; }
impl core_io_traits::Read for Serial { ... }
impl core_io_traits::Write for Serial { ... }
// crate that implements SLIP
fn recv_packet<'a, R: core_io_traits::Read>(r: &mut R, buf: &'a mut [u8]) -> Result<Packet<'a>, R::Error>;
fn send_packet<W: core_io_traits::Write>(w: &mut W, packet: Packet<'_>) -> Result<(), W::Error>; The SLIP code doesn't need to know anything about the structure of the IO errors it might encounter, it just needs to pass them through. There's a huge amount of library code that looks like this -- they need to be able to incrementally send/receive bytes, and can't do anything about errors except pass them along. |
For what it's worth, @programmerjake did mention potentially moving But also, I think that some of the adjacent types, like |
Yes, I'm aware there's broad interest in moving I don't think this proposal blocks or is blocked by moving |
@jmillikin, I haven't really looked in to deep into this, so I might be missing stuff, but what I was proposing was to have
I understand. If this only wants to be a bare bones trait agnostic of any failures, it should be fine. Though I think, it will far from enough for most use cases and the users will still have to have super traits on top of these for them to be usable. Though I am still with @clarfonthey on not having the |
I understand what you're asking for, the problem is trait coherence rules and backwards compatibility. A plausible implementation of impl core_io_traits::Write for Vec<u8> {
type Error = crate::collections::TryReserveError;
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
self.try_reserve(buf.len())?;
self.extend_from_slice(buf);
Ok(buf.len())
}
} The blanket implementation of impl<T, E> std::io::Write for T
where
T: core_io_traits::Write<Error = E>,
E: Into<std::io::Error>,
{
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
core_io_traits::Write::write(self, buf).map_err(Into::into)
}
} And now you're stuck, because the existing implementation of Thus, adapter types are more practical: struct CoreToStd<C>(C);
impl<E, C> std::io::Read for CoreToStd<C>
where
C: core_io_traits::Read<Error = E>,
E: Into<std::io::Error>,
{
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
core_io_traits::Read::read(&mut self.0, buf).map_err(Into::into)
}
}
impl<E, C> std::io::Write for CoreToStd<C>
where
C: core_io_traits::Write<Error = E>,
E: Into<std::io::Error>,
{
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
core_io_traits::Write::write(&mut self.0, buf).map_err(Into::into)
}
fn flush(&mut self) -> Result<(), std::io::Error> {
core_io_traits::Write::flush(&mut self.0).map_err(Into::into)
}
}
Well, that's always the drawback with primitive traits (type-classes) in a nominal type system. The purpose of traits like However, the more generic you make a trait, the less information they provide. A Linux network socket, a WASM linear memory page, and a microcontroller's infrared LED have very little in common, so a This is different from the
As mentioned, the problem is composition in libraries. It should be possible for a library to describe necessary I/O functionality with // it's really nice to not need a three-line `where` for each function that needs to
// read and write to the same IO thingie.
fn handshake<S: Read + Write>(socket: &mut S) -> Result<(), S::Error> { ... } For cases when there are two separate I/O trait hierarchies involved (such as streaming compression), the concrete types are also necessarily separate, so the fn copy<R: Read, W: Write>(r: &mut R, w: &mut W) -> Result<(), CopyError<R::Error, W::Error>> { ... }
enum CopyError<RE, WE> {
ReadError(RE),
WriteError(WE),
} |
Proposal
Problem statement
Now that the basic types for cursor-based IO (
BorrowedBuf
andBorrowedCursor
) are incore::io
, I'd like to propose adding a set of low-level I/O traits tocore::io
that mimicstd::io
, but use cursors into borrowed buffers and are parameterized by error type.I expect this API is big enough that it'll need to be an RFC, but I figured I'd file an ACP first to see if there were any objections to the overall idea / API structure.
Motivating examples or use cases
Being able to write
#![no_std]
libraries that do some sort of I/O is a widespread request. A partial list of use cases includes:std
programs also.#![no_std]
to reduce binary size and have more predictable memory allocation patterns. They still need some way to abstract I/O over files, sockets, character devices (/dev/random
), etc.#![no_std]
builds, but today their streaming interfaces are often behind afeature = "std"
gate.The proposed design may also be useful in code that uses
std
but needs precise control over I/O patterns.Solution sketch
Alternatives
std::io
traits intocore
: Using std::io::{Read, Write, Cursor} in a nostd environment rust#48331std::io::Error
[0], and doesn't seem likely to land in the forseeable future.#![no_std]
libraries to define their own I/O traits (leaving consumers to write the glue code).Read
/Write
traits, in the idiom of Go, and leaving the scatter/gather IO to another day.[0] rust-lang/project-error-handling#11
Links and related work
What happens now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):
Second, if there's a concrete solution:
The text was updated successfully, but these errors were encountered: