Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.io

import std.io provides byte streams with explicit partial progress: the stream contracts, helpers that retry until a range is complete, fixed-capacity buffering, and adapters for files, the process streams, and memory. Data moves through std.bytes.Bytes ranges; no operation creates a list[u8]. Writes read a &[u8] view, so a buffer, part of one, or a b"..." literal is written as it is, without a copy.

The module is embedded Plenty code over native transport and terminal queries. It is available on the supported x86-64 Linux GNU target.

Stream contracts

A stream is any pub class or enum whose pub methods match one of these protocols. Generic calls specialize for each concrete type. Eligible interfaces can also use explicit protocol boxes, whose method calls dispatch indirectly; this does not introduce an implicit stream object type.

ProtocolMethodResult
Readerread(self: &mut, buffer: &mut Bytes, start: i64, stop: i64)Result[i64, StreamError]
Writerwrite(self: &mut, source: &[u8])Result[i64, StreamError]
Writerflush(self: &mut)Result[(), StreamError]
Seekerseek(self: &mut, target: SeekFrom)Result[i64, StreamError]
Closerclose(self: &mut)Result[(), StreamError]

One call makes at most one attempt and returns the number of bytes it moved.

  • read fills a prefix of buffer[start:stop] and never changes the buffer’s length. A count below the range length is a short read, not an error. Zero for a nonempty range means end of input.
  • write consumes a prefix of source. A short count is normal. Zero for a nonempty source means the stream accepted nothing; write_all reports that as WriteZero instead of looping. To write part of a buffer, pass &buffer[a:b].
  • An empty range or source returns Ok(0) and says nothing about end of input.
  • An error means the call moved no bytes. A stream that fails after moving some bytes returns the count and reports the failure on the next call.
  • Interrupted means nothing moved and the same call can be repeated.
  • seek returns the new byte offset from the start.

A read range must satisfy 0 <= start <= stop <= buffer.len(). An invalid range traps with read: start/stop outside buffer before touching the stream, even at EOF or for an empty range. read_exact applies the same rule with read_exact: start/stop outside buffer. A write source is a view, whose bounds clamp, so it has no range error. A stream that returns a negative count or more than it was offered is reported as InvalidProgress by every helper and buffer, which then stop using that count.

An implementation declares write with the same source: &[u8] parameter and passes the view, or part of it, on to operations that read one, such as Bytes.extend or another stream’s write:

import std.bytes
import std.io
from std.bytes import Bytes
from std.io import StreamError

pub class Tally:
    pub data: Bytes

    pub def write(self: &mut Tally, source: &[u8]) -> Result[i64, StreamError]:
        match self.data.extend(source):
            case Ok(_):
                Ok(len(source))
            case Err(cause):
                Err(StreamError.Allocation(cause))

    pub def flush(self: &mut Tally) -> Result[(), StreamError]:
        Ok(())

def main() -> Result[(), AllocError | std.bytes.BytesError | std.io.Partial | IoError]:
    mut tally = Tally(std.bytes.empty()?)
    std.io.write_all(&mut tally, b"ab")?
    tail = std.bytes.from_text("cdef")?
    std.io.write_all(&mut tally, &tail[:2])?
    print(tally.data.to_text()?)?
    Ok(())
abcd

A helper names the one protocol it needs, such as [R: Reader]. Protocols cannot be combined in one bound, so close_buffered requires Closer and relies on the buffer’s own use of write and flush.

Errors

StreamErrorMeaning
InterruptedThe native call was interrupted (EINTR); retry
UnexpectedEofInput ended before read_exact filled its range
WriteZeroA writer accepted no bytes of a nonempty range
OutOfRangeA seek target or other stream range is invalid
InvalidProgressA stream reported an impossible byte count
Io(IoError)Any other native failure, including Closed, NotReadable, NotWritable, and System(code)
Allocation(AllocError)A buffer could not be allocated or grown

std.io.classify(error: IoError) performs this mapping for code that wraps another native operation.

Helpers that take several steps keep the progress made before a failure:

TypeContents
Partialcompleted: i64 bytes moved, then cause: StreamError
CopyError.Read(count, cause)The reader failed after count bytes reached the writer
CopyError.Write(count, cause)The writer failed after accepting count bytes
CloseError.Flush(cause)Flushing failed; the stream was closed
CloseError.Close(cause)Flushing succeeded; closing failed
CloseError.FlushAndClose(flush, close)Both failed

Seek and binary-field extent calculations handle checked arithmetic through these declared errors. read_up_to reports an unrepresentable buffer extent as Partial(completed, Allocation(CapacityOverflow)) before growing the buffer. If another copy chunk would make the delivered total exceed i64.MAX, copy_all returns CopyError.Read(completed, OutOfRange) before delivering that chunk. The reader may already have consumed it; copying is not transactional or resumable. Internal cursor arithmetic uses explicit opt-outs only after validating ranges, progress and the applicable byte budget.

These types do not convert to each other. Match the result, or use ? in a function whose error sum lists each of them.

Helpers

FunctionResultContract
read_exact(reader, buffer, start, stop)Result[(), Partial]Fill the whole range. End of input first is UnexpectedEof
write_all(writer, source: &[u8])Result[(), Partial]Deliver all of source. No progress is WriteZero. Does not flush
read_up_to(reader, out, limit: NonNegative[i64])Result[i64, Partial]Append at most limit bytes to out; fewer only at end of input
copy_all(reader, writer, scratch)Result[i64, CopyError]Move bytes until end of input through the whole of scratch. Does not flush

All four retry Interrupted and continue after short progress. They stop at the first other error and return the bytes moved so far. After a failed read_exact, buffer[start:start + completed] holds the bytes read. After a failed read_up_to, out has grown by exactly completed bytes.

read_exact, write_all, and copy_all do not allocate. copy_all needs a nonempty scratch, otherwise CopyError.Read(0, OutOfRange); its length is the transfer size. read_up_to grows out by at most 8192 bytes ahead of the data it has received and removes unused bytes before returning, so memory follows the input actually read, not limit.

copy is not used as a name because the builtin copy already exists.

Bounded buffering

FunctionResult
buffered_reader(inner: R, capacity: Positive[i64])Result[BufReader[R], StreamError]
buffered_writer(inner: W, capacity: Positive[i64])Result[BufWriter[W], StreamError]

Each takes ownership of inner and allocates one buffer of exactly capacity bytes. The buffer never grows. The only failure is allocating it. A computed capacity converts with Positive.try_from.

BufReader[R]ResultContract
read(buffer, start, stop)Result[i64, StreamError]Serve held bytes, otherwise make one inner read
read_until(delimiter, out, limit: Positive[i64])Result[i64, StreamError]Append through the first delimiter, at most limit bytes
buffered()i64Bytes held and not yet consumed
capacity()i64The fixed buffer size

read makes at most one inner read per call. A request at least as large as the capacity, made while the buffer is empty, goes straight to the inner stream. read_until returns the count appended: zero is end of input, and a count whose last byte is not the delimiter means the limit or end of input came first. It retries Interrupted. After an error, bytes already appended stay in out and bytes not yet appended stay in the buffer.

BufWriter[W]ResultContract
write(source)Result[i64, StreamError]Hold source, delivering held bytes first when it does not fit
flush()Result[(), StreamError]Deliver every held byte, then flush the inner stream
pending()i64Bytes held and not yet delivered
capacity()i64The fixed buffer size

Held bytes never exceed the capacity and reach the inner stream in order. A range at least as large as the capacity is passed to the inner stream directly after the held bytes, and may be accepted short. Delivery retries Interrupted and short writes. When delivery fails, the undelivered bytes stay held and the range passed to write is not accepted; a later flush resumes at the first undelivered byte.

Nothing is written when a BufWriter is dropped. Call flush or close_buffered; otherwise held bytes are discarded.

FunctionResultContract
seek_reader(reader, target)Result[i64, StreamError]Discard held input and seek. Current is relative to the bytes consumed
seek_writer(writer, target)Result[i64, StreamError]Deliver held bytes, then seek
close_buffered(writer)Result[(), CloseError]Flush, then close the inner stream even if the flush failed

Both classes expose the wrapped stream as the public field inner, for calls such as reader.inner.close(). Reading or writing inner directly bypasses the buffer. The language cannot move a field out of a consumed value, so there is no method returning the inner stream; exchange it with replace(buffer.inner, other).

Adapters

ConstructorStreamProtocols
open_file(path: &str, mode: &str)Result[FileStream, StreamError]Reader, Writer, Seeker, Closer
open_path(path: &Path, mode: &str)Result[FileStream, StreamError]the same, preserving native path bytes
from_file(file: File)FileStreamthe same
stdin()StdinReader
stdout(), stderr()Stdout, StderrWriter
cursor(data: Bytes)CursorReader, Writer, Seeker

open_path accepts a std.path.Path without decoding its bytes. It has the same modes, permissions, symlink behavior, and ownership as open_file. Embedded NUL is StreamError.Io(IoError.InvalidInput) rather than a truncated filename; an invalid mode is StreamError.Io(IoError.InvalidMode). Owner and path allocations are fallible and occur before opening or truncating. Use std.cli.fileinput for buffered fallible text-line iteration across multiple native paths or stdin.

FileStream owns a File, public as file. read and write each make one native call on the descriptor, with no newline translation or text decoding. seek positions by byte offset and returns the new offset; this differs from the opaque text cookies of File.tell. SeekFrom.Start with a negative offset is OutOfRange; another target the system rejects is Io(IoError.System(code)). flush and sync forward to the file. close reports the native close error once and is Ok when repeated; other operations on a closed file are Io(IoError.Closed). The open mode is enforced: Io(IoError.NotReadable) or Io(IoError.NotWritable). Dropping an unclosed FileStream closes the file and discards any close error.

Stdin, Stdout, and Stderr hold no state and never close the process descriptors. Stdin.read makes one native read. Stdout and Stderr share the process streams used by print and the text writers, so output keeps its order across both; flush delivers buffered stdout. Stderr is unbuffered: each Stderr.write makes one native write on descriptor 2, so a closed or read-only stderr is Io(IoError.System(9)) (EBADF).

Each process stream has is_terminal() -> bool. It asks the OS on each call and returns false for redirected, closed or undetectable streams. This query neither reads nor writes bytes and does not imply ANSI escape support or a terminal width. std.cli.progress uses it to keep redirected stderr clean.

Cursor reads from and writes to an owned buffer, public as data, at position(). Reads stop at the end of the buffer. Writes replace existing bytes and extend the buffer at its end; writing past the end first fills the gap with zeros. seek accepts any nonnegative position. A write can report Allocation; it reserves before changing existing bytes.

The same cursor also provides checked sequential binary fields, bounded owned payload reads, and remaining/require_end validation. These methods share its position while retaining stricter per-field failure atomicity.

Ownership and threads

Transport methods take &mut self, so a stream has one user at a time and a buffer passed to read cannot be the stream’s own storage. A read needs &mut Bytes; a write borrows its source shared. A buffered adapter owns its inner stream, and the inner stream cannot be used through another name while the adapter exists. Sharing one stream between threads requires moving it or placing it in a lock; the module adds no synchronization of its own. Writes to Stdout and Stderr from several threads are each atomic with respect to the process buffer, but their order is unspecified.

Costs

Each read or write on FileStream or Stdin is one system call. Buffered adapters make one bulk copy per call between the caller’s range and the fixed buffer. read_until searches held bytes with Bytes.find, one native scan per call. Seeking a BufReader discards up to capacity bytes already read.

AllocatesDoes not allocate
buffered_reader, buffered_writer: one bufferEvery BufReader and BufWriter method except read_until
read_up_to and read_until, when out lacks capacityread_exact, write_all, copy_all, seek_reader, seek_writer, close_buffered
Cursor.write beyond the buffer’s capacityFileStream, Stdin, Stdout, Stderr operations

Limits

  • Only x86-64 Linux GNU is supported. Nonblocking descriptors are not handled: EAGAIN arrives as Io(IoError.System(11)).
  • Stdout writes pass through the Rust standard library’s buffered stdout, which reports EBADF as success: writes to a closed or read-only stdout succeed and discard their bytes.
  • Streams are bytes only. Text decoding, line iteration over streams, and formatted writes are not provided; use read_until and Bytes.to_text.
  • There are no vectored operations, no read-ahead peeking, and no stream object that erases the concrete class.
  • A class cannot declare which protocols its parameter satisfies. A wrong inner stream is rejected at buffered_reader or buffered_writer with a note naming the missing method, not with a protocol diagnostic.
  • The native calls __io_file_read, __io_file_write, __io_stdin_read, __io_std_write, and __io_file_seek are private transport for the standard library, reached through Bytes.read_file and read_stdin, std.bytes.write_file, Stdout.write/Stderr.write, and FileStream.seek. __io_is_terminal is the private process-stream terminal query.

Acknowledgements