Skip to content
CryoCryo home
StdlibSystem

io

Every byte source implements Read and every byte sink implements Write. Because both traits carry substantial defaults, an implementation only has to supply one method to get the whole convenience surface — read_line, read_to_string, write_all, and the rest — for free. IoError is the failure channel throughout.

ItemImport
Read, Write, Seek, SeekFromimport std::io::traits;
Stdin, Stdout, Stderr, stdin(), stdout(), stderr()import std::io::stdio;
BufReader, BufWriter, LineWriterimport std::io::buf;
Cursorimport std::io::cursor;
IoError, IoErrorKindimport std::io::error;
read_fd, write_fd, close_fdimport std::io::fd;

Read

type trait Read {
    read(mut &this, buffer: u8*, length: u64) -> Result<u64, IoError>;
}

read is the only required method. It fills up to length bytes and returns how many it actually got. Zero means EOF. A value less than length means the source had nothing more available at this moment — not necessarily EOF, and it is the caller's decision whether to retry.

Everything below is a default built on that one method.

MethodBehaviour
read_all(buffer: u8*, length: u64)Read until the buffer is full or EOF. Retries on Interrupted.
read_exact(buffer: u8*, length: u64)Read exactly length bytes; a short read at EOF is UnexpectedEof.
read_byte()Result<Option<u8>, IoError>None is EOF.
read_char()Result<Option<u32>, IoError> — one UTF-8 scalar.
read_until(delimiter: u8, out: mut &Array<u8>)Reads through the delimiter, which is included in out.
read_line(out: mut &String)Reads through \n, which is included.
read_to_end(out: mut &Array<u8>)Every remaining byte.
read_to_string(out: mut &String)Every remaining byte as UTF-8.

read_char fails with UnexpectedEof on a partial scalar and InvalidData on a malformed leading byte, a malformed continuation, or a surrogate — surrogates are not valid Unicode scalars.

Two things worth knowing. read_to_end is unbounded: a pathological source like /dev/zero will exhaust memory, so cap it yourself if the input is untrusted. And read_to_string does not validate UTF-8 — you are certifying that the source is well-formed, or accepting invalid continuation bytes inside a String.

read_until and read_line are byte-level, running read(..., 1) so the stop point is exact. On a source where a one-byte read is expensive — a socket, a file — wrap it in a BufReader, whose read_line override is the fast path.

Write

type trait Write {
    write_some(mut &this, bytes: Slice<u8>) -> Result<u64, IoError>;
    flush(mut &this) -> Result<(), IoError>;
}

write_some is the low-level primitive: write up to bytes.length() bytes and report how many actually landed, which may be fewer on a slow sink. Unbuffered writers override flush with a no-op.

write<T>(data: T) is the one you normally call. It delivers every byte, retrying short writes and Interrupted, and dispatches on the payload at compile time: a raw Slice<u8>, a Str, a NUL-terminated string, or a single u8. If the sink repeatedly refuses to make progress it fails with WriteZero rather than spinning.

Seek

type enum SeekFrom { Start(u64); Current(i64); End(i64); }

seek(from: SeekFrom) repositions the cursor and returns the resulting absolute offset. stream_position() reports the current offset without moving, defaulting to seek(Current(0)).

Seeking past the end is permitted, and what happens next is sink-specific: a File leaves a sparse gap the OS zero-fills, while a Cursor zero-fills on the next write. Seeking to a negative absolute offset fails with InvalidInput.

The standard streams

Three thin handles over file descriptors 0, 1, and 2, obtained from stdin(), stdout(), and stderr(). Each implements Read or Write and inherits every default on that trait.

import std::io::stdio;

mut input: Stdin = stdin();
match (input.line()) {
    Result::Ok(Option::Some(text)) => { /* one line, newline stripped */ }
    Result::Ok(Option::None)       => { /* EOF */ }
    Result::Err(e)                 => { /* IoError */ }
}

Each handle carries is_tty() and as_fd(). Stdin adds two conveniences: line() reads one line into an owned String with the newline stripped, and prompt(msg) writes the prompt to stderr — so it doesn't pollute piped stdout — then reads a line. Each handle also has a lock() returning a guard type.

There is no buffering at this layer: stdout() translates to raw write(2) calls. Compose buffering explicitly when you want it.

Buffering

Wrapping a sink in a BufWriter coalesces many small writes into one syscall per buffer-full. The default buffer is 8 KiB, matching stdio's block-buffered default on most Unixes.

import std::io::buf;

mut out: Stdout = stdout();
mut buffered: BufWriter<Stdout> = BufWriter<Stdout>::new(&out);
buffered.write(payload);
buffered.flush()?;

These adapters borrow the inner reader or writer through a pointer. You keep ownership of the underlying sink and remain responsible for dropping it; wrapping and unwrapping are free.

BufWriter::drop flushes pending bytes, so call it before the wrapped sink goes away or buffered writes are lost.

TypeNotes
BufWriter<W>buffered(), capacity(), clear() (discard without writing, for error paths), get_ref().
LineWriter<W>A BufWriter that flushes after any write containing \n. The right choice for interactive stdout on a TTY.
BufReader<R>Pulls in buffer-sized chunks and serves reads from memory. Overrides read_line with a scan-and-copy fast path.

BufReader also exposes the zero-copy parsing pair: fill_buf() returns the filled region as a Slice<u8> (empty at EOF), and consume(amount) advances past however many bytes you actually wanted. available() reports what is buffered and unread.

If you mutate through get_ref(), flush first — otherwise buffered bytes land out of order.

BufStream<S>

The non-blocking counterpart, implementing AsyncRead and AsyncWrite. Unlike the adapters above it owns its transport, because a borrowed transport cannot be held across a suspension point. static of(inner: S) wraps an already-connected transport, and transport() surfaces it for socket options or a negotiated ALPN protocol — not for reading or writing, which would desynchronize the buffers.

Cursor

An in-memory byte buffer that is Read, Write, and Seek. This is how you run code written against the I/O traits against memory instead of a file or socket — the standard way to unit-test a reader or writer, or to build up a payload before handing it to a real sink.

import std::io::cursor;

mut c: Cursor = Cursor::empty();
c.write(header);
c.write(body);
mut bytes: Array<u8> = c.into_inner();

static new(buffer: Array<u8>) takes ownership of an existing buffer with the cursor at 0; static empty() starts fresh. position() / set_position(pos), len(), as_bytes(), and into_inner() (which moves the buffer out and leaves the cursor holding a fresh empty one) round it out.

Reads copy forward and return 0 at the end. Writes overwrite the bytes under the cursor and extend past the end; a write starting beyond the current end zero-fills the gap first, matching the sparse region a File seek-past-end leaves. Reads never error, and writes fail only on allocation failure, surfaced as WriteZero.

IoError

A small closed set of kinds, because callers mostly branch on the kind rather than the text.

NotFound            PermissionDenied    AlreadyExists      ConnectionRefused
ConnectionReset     ConnectionAborted   NotConnected       AddrInUse
AddrNotAvailable    BrokenPipe          Interrupted        InvalidInput
InvalidData         TimedOut            WriteZero          UnexpectedEof
WouldBlock          OutOfMemory         Unsupported        Other

IoError carries the kind plus os_code, the underlying errno (or 0 where not applicable). Branch on the kind; keep the code for diagnostics. describe() -> Str is the standard accessor, and IoErrorKind::label() gives a stable slug like "not_found".

static from_errno(code: i32) does the mapping. Unknown codes fold to Other with the numeric value preserved.

The errno table is gated per OS. from_errno is fed msvcrt's C-runtime errno on Windows and glibc's on Linux; the base codes (1–34) are identical, but the socket range differs — EADDRINUSE is 98 on glibc and 100 on msvcrt — so those are gated. Socket errors on Windows do not pass through here at all; net::sys maps WSAGetLastError() codes directly.

File descriptors

io::fd is the canonical home for the libc calls every fd-owning type needs, so wrappers import from here rather than re-declaring the same externs.

FunctionNotes
read_fd(fd: i32, buffer: u8*, length: u64)read(2), retrying on EINTR. Zero means EOF.
write_fd(fd: i32, bytes: Slice<u8>)write(2), same retry semantics. Short writes are normal.
close_fd(fd: i32)close(2). Swallows the result — nothing sensible can be done with a close failure.

These handle the "retry on Interrupted, map everything else through IoError::from_errno" dance, which is what collapses most Read and Write implementations to a single line.