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.
| Protocol | Method | Result |
|---|---|---|
Reader | read(self: &mut, buffer: &mut Bytes, start: i64, stop: i64) | Result[i64, StreamError] |
Writer | write(self: &mut, source: &[u8]) | Result[i64, StreamError] |
Writer | flush(self: &mut) | Result[(), StreamError] |
Seeker | seek(self: &mut, target: SeekFrom) | Result[i64, StreamError] |
Closer | close(self: &mut) | Result[(), StreamError] |
One call makes at most one attempt and returns the number of bytes it moved.
readfills a prefix ofbuffer[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.writeconsumes a prefix ofsource. A short count is normal. Zero for a nonempty source means the stream accepted nothing;write_allreports that asWriteZeroinstead 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.
Interruptedmeans nothing moved and the same call can be repeated.seekreturns 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
StreamError | Meaning |
|---|---|
Interrupted | The native call was interrupted (EINTR); retry |
UnexpectedEof | Input ended before read_exact filled its range |
WriteZero | A writer accepted no bytes of a nonempty range |
OutOfRange | A seek target or other stream range is invalid |
InvalidProgress | A 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:
| Type | Contents |
|---|---|
Partial | completed: 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
| Function | Result | Contract |
|---|---|---|
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
| Function | Result |
|---|---|
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] | Result | Contract |
|---|---|---|
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() | i64 | Bytes held and not yet consumed |
capacity() | i64 | The 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] | Result | Contract |
|---|---|---|
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() | i64 | Bytes held and not yet delivered |
capacity() | i64 | The 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.
| Function | Result | Contract |
|---|---|---|
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
| Constructor | Stream | Protocols |
|---|---|---|
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) | FileStream | the same |
stdin() | Stdin | Reader |
stdout(), stderr() | Stdout, Stderr | Writer |
cursor(data: Bytes) | Cursor | Reader, 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.
| Allocates | Does not allocate |
|---|---|
buffered_reader, buffered_writer: one buffer | Every BufReader and BufWriter method except read_until |
read_up_to and read_until, when out lacks capacity | read_exact, write_all, copy_all, seek_reader, seek_writer, close_buffered |
Cursor.write beyond the buffer’s capacity | FileStream, Stdin, Stdout, Stderr operations |
Limits
- Only x86-64 Linux GNU is supported. Nonblocking descriptors are not handled:
EAGAINarrives asIo(IoError.System(11)). - Stdout writes pass through the Rust standard library’s buffered stdout,
which reports
EBADFas 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_untilandBytes.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_readerorbuffered_writerwith 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_seekare private transport for the standard library, reached throughBytes.read_fileandread_stdin,std.bytes.write_file,Stdout.write/Stderr.write, andFileStream.seek.__io_is_terminalis the private process-stream terminal query.
Acknowledgements
- Stream protocols follow Go’s
iopackage (Reader,Writer,Seeker,Closer) and Rust’sstd::io::Readandstd::io::Write.