Skip to content

[ACP] IO traits in core::io #293

Closed as not planned
Closed as not planned
@jmillikin

Description

@jmillikin

Proposal

Problem statement

Now that the basic types for cursor-based IO (BorrowedBuf and BorrowedCursor) are in core::io, I'd like to propose adding a set of low-level I/O traits to core::io that mimic std::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:

  • Embedded systems that have some notion of sequential byte-based I/O via serial, i2c, RNG chips, etc.
  • Binaries that run under an OS in a constrained environment, such as Linux's early userspace, use #![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.
  • Encode/decode libraries that want to incrementally parse input data or generate output data could be usable in #![no_std] builds, but today their streaming interfaces are often behind a feature = "std" gate.

The proposed design may also be useful in code that uses std but needs precise control over I/O patterns.

Solution sketch

// core::io

// The `IoBuf` and `IoBufMut` types are similar to `std::io::IoSlice{,Mut}`, but aren't guaranteed
// to have any particular internal layout. `IoBufMut` is also allowed to contain uninitialized excess
// capacity, making it similar to `Vec` (but with fixed maximum length).
//
// The goal is to enable fast paths for hosted targets, with slight opacity to enable `target_os = "none"`.
//
// Then within `std` there can be cheap conversions of &[IoBuf] / &[IoBufMut] <-> &[IoSlice] / &[IoSliceMut]
// which is safe because `std` knows which OS it's built for.
struct IoBuf<'a>;
struct IoBufMut<'a>; // currently named BorrowedBuf

impl<'a> From<&'a [u8]> for IoBuf<'a>;
impl<'a> From<&'a mut [u8]> for IoBufMut<'a>;
impl<'a> From<&'a mut [MaybeUninit<u8>]> for IoBufMut<'a>;
// core::io::core_io_traits (placerholder name: won't conflict with std::io and also very ugly)

// All traits in this module have a bound on IO, which associates an error type.
trait IO {
  type Error;
}

// ReadCursor tracks how much data was read from a single read* call.
// ReadVecCursor is the same, but for vectored reads.
struct ReadCursor<'a>; // currently named BorrowedCursor
struct ReadVecCursor<'a>;
impl IoBufMut<'_> {
  fn unfilled(&mut self) -> ReadCursor<'_>;
  fn unfilled_vec<'a>(bufs: &'a [&'a mut IoBufMut<'_>]) -> ReadVecCursor<'a>;
}

// WriteVecCursor tracks how much data was written in a vectored write.
//
// See https://github.com/rust-lang/rust/issues/70436 for context on why
// this extra glue is useful.
struct WriteVecCursor<'a>;
impl IoBuf<'_> {
  fn unwritten(&self) -> &[u8];
  fn unwritten_vec<'a>(bufs: &'a [&'a IoBuf<'_>]) -> WriteVecCursor<'a>;
}
// Read traits
//
// Read is like std::io::Read, except read_exact() has no default implementation due to
// the opacity of error types.
//
// ReadAt is like (a subset of) std::os::unix::fs::FileExt, and represents types that can read
// at arbitrary offsets. It has a bound on `IO` rather than `Read` because such types don't
// necessarily have the concept of a "current position" (e.g. a block device, or a ROM chip).
//
// Read sizes are provided via the cursors, so all return types are `Result<(), Self::Error>`

trait Read: IO {
  fn read(&mut self, cursor: &mut ReadCursor) -> Result<(), Self::Error>;
  fn read_exact(&mut self, cursor: &mut ReadCursor) -> Result<(), Self::Error>;
  fn read_vectored(&mut self, cursor: &mut ReadVecCursor) -> Result<(), Self::Error> { ... }
  fn read_vectored_exact(&mut self, cursor: &mut ReadVecCursor) -> Result<(), Self::Error> { ... }
  fn is_read_vectored(&self) -> bool { ... }
}

trait ReadAt: IO {
  fn read_at(&mut self, cursor: &mut ReadCursor, offset: usize) -> Result<(), Self::Error>;
  fn read_exact_at(&mut self, cursor: &mut ReadCursor, offset: usize) -> Result<(), Self::Error>;
  fn read_vectored_at(&mut self, cursor: &mut ReadVecCursor, offset: usize) -> Result<(), Self::Error> { ... }
  fn read_vectored_exact_at(&mut self, cursor: &mut ReadVecCursor, offset: usize) -> Result<(), Self::Error> { ... }
  fn is_read_vectored_at(&self) -> bool { ... }
}
// Write traits -- basically the same story as the read traits, but there's
// no cursor for just plain writes.

pub trait Write: IO {
  fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error>;
  fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error>;
  fn write_vectored(&mut self, cursor: &mut WriteVecCursor) -> Result<(), Self::Error> { ... }
  fn write_all_vectored(&mut self, cursor: &mut WriteVecCursor) -> Result<(), Self::Error> { ... }
  fn is_write_vectored(&self) -> bool { ... }

  fn flush(&mut self) -> Result<(), Self::Error>;
}

pub trait WriteAt: IO {
  fn write_at(&mut self, buf: &[u8], offset: usize) -> Result<usize, Self::Error>;
  fn write_all_at(&mut self, buf: &[u8], offset: usize) -> Result<(), Self::Error>;
  fn write_vectored_at(&mut self, cursor: &mut WriteVecCursor, offset: usize) -> Result<(), Self::Error> { ... }
  fn write_all_vectored_at(&mut self, cursor: &mut WriteVecCursor, offset: usize) -> Result<(), Self::Error> { ... }
  fn is_write_vectored_at(&self) -> bool { ... }
}
// To provide somewhat friendlier UX, a wrapper struct can be used to provide various
// parameter formats. For example the `std::io::Read` trait methods `read` and `read_buf`
// could be represented 1:1 here, without expanding the trait's API.

pub struct Reader<'a, R>;
impl<'a, R> Reader<'a, R> {
  pub fn new(r: &'a mut R) -> Reader<'a, R>;
}

impl<R: Read> Reader<'_, R> {
  fn read(&mut self, buf: &mut [u8]) -> Result<usize, R::Error>;
  fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), R::Error>;

  fn read_buf(&mut self, buf: &mut ReadBuf) -> Result<usize, R::Error>;
  fn read_buf_exact(&mut self, buf: &mut ReadBuf) -> Result<(), R::Error>;
}

// Vectored I/O can also be made a bit more ergonomic by using const-sized
// arrays, which would make the traits non-object-safe.
impl<R: ReadVectored> Reader<'_, R> {
  fn read_vectored<const N: usize>(&mut self, bufs: [&'a mut [u8]; N]) -> Result<usize, R::Error>;
  fn read_buf_vectored(&mut self, buf: &mut ReadVecBuf) -> Result<usize, R::Error>;
  // ...
}

// same for `struct Writer<'a, W>`

Alternatives

  1. Move std::io traits into core: Using std::io::{Read, Write, Cursor} in a nostd environment rust#48331
    • This approach is hard-blocked by the design of std::io::Error[0], and doesn't seem likely to land in the forseeable future.
  2. Do nothing, and hope for a third party to write a library of equivalent traits that can be broadly adopted by the community.
  3. Do nothing, and encourage authors of #![no_std] libraries to define their own I/O traits (leaving consumers to write the glue code).
  4. Something smaller? Such as defining just the Read / Write traits, in the idiom of Go, and leaving the scatter/gather IO to another day.
  5. ?

[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):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions