rokdocs
v0.1

3.5. io:: - Reader / Writer / standard I/O

The rok::std::io module provides byte stream abstractions together with standard I/O (stdin/stdout/stderr). There are no separate AsyncRead/AsyncWrite traits. The APIs of this module block the calling OS thread. A stackless coroutine cannot be suspended inside an ordinary function, so a sync call does not become async on its own inside a task; I/O that suspends only the task is provided as per-type *_async methods (sockets 3.2.0, files 3.1.4a).

3.5.0. IoError

struct IoError {
    kind: IoErrorKind;
    code: i32;            // OS errno
    message: str;         // OS message (strerror)
}

enum IoErrorKind {
    Other,                // any other errno
    NotFound,             // ENOENT
    PermissionDenied,     // EACCES, EPERM
    AlreadyExists,        // EEXIST
    NotADirectory,        // ENOTDIR
    IsADirectory,         // EISDIR
    DirectoryNotEmpty,    // ENOTEMPTY
    InvalidInput,         // EINVAL
    BrokenPipe,           // EPIPE
    Interrupted,          // EINTR
    WouldBlock,           // EAGAIN
    TimedOut,             // ETIMEDOUT, time limit
    StorageFull,          // ENOSPC
    CrossesDevices,       // EXDEV
    Unsupported,          // ENOTSUP, ENOSYS
    UnexpectedEof,        // ended before the expected bytes (read_exact, etc.)
    InvalidData,          // data does not match the format (UTF-8, etc.)
}

kind is determined from errno (UnexpectedEof / InvalidData are set by the library); the original value remains in code and the OS's description in message.

3.5.1. AsBytes

trait AsBytes { fn byte_slice(self) -> Slice<u8>; }   // need not be UTF-8
impl AsBytes for str { .. }
impl AsBytes for List<u8> { .. }
impl AsBytes for Slice<u8> { .. }
impl<const N: usize> AsBytes for [u8; N] { .. }   // a view of a copy (b"...", include_bytes!)

The data argument of write functions (fs, net, Writer) accepts AsBytes. AsBytes is defined in rok::core (beside Slice, rok::core::collections::slice) and re-exported as rok::std::io::AsBytes.

3.5.1a. Reader / Writer traits

trait Reader {
    fn read(ref self, buf: List<u8>) throws (IoError) -> usize;   // fills buf[0..n], 0 = end

    // default methods
    fn read_exact(ref self, buf: List<u8>) throws (IoError);      // all of buf; UnexpectedEof if short
    fn read_to_end(ref self) throws (IoError) -> List<u8>;
    fn read_to_str(ref self) throws (IoError) -> str;             // UTF-8 validation
    fn read_line(ref self) throws (IoError) -> str;               // includes `\n`, "" at the end
    fn lines(ref self) -> Lines<Self>;     // implements Iterator<Item = Result<str, IoError>>
    fn bytes(ref self) -> Bytes<Self>;     // implements Iterator<Item = Result<u8, IoError>>
    fn raw_fd(self) -> RawFd;              // the OS descriptor io::copy may hand to the kernel; -1 (default) if none, or if the stream buffers, times out or is non-blocking
}

trait Writer {
    fn write(ref self, buf: List<u8>) throws (IoError) -> usize;
    fn flush(ref self) throws (IoError);

    // default
    fn write_all(ref self, buf: List<u8>) throws (IoError);
    fn write_str(ref self, s: str) throws (IoError);
    fn write_fmt(ref self, s: str) throws (IoError);     // writes the result of an f-string
    fn raw_fd(self) -> RawFd;              // the OS descriptor io::copy may hand to the kernel; -1 (default) if none, or if the stream buffers, times out or is non-blocking
}

trait Seek {
    fn seek(ref self, pos: SeekFrom) throws (IoError) -> u64;

    // default
    fn rewind(ref self) throws (IoError);                         // seek(Start(0))
    fn stream_position(ref self) throws (IoError) -> u64;         // seek(Current(0))
}

Byte buffers are passed as List<u8> until Slice<u8> is available. read fills a buf whose length was set up in advance (List is a handle, so the caller sees the result). Trait methods take ref self; inherent methods of handle types take self.

These three traits are implemented consistently by File, TcpStream, UnixStream and stdin/out/err, so the same code can handle many kinds of streams.

3.5.2. Standard I/O

Next, standard I/O, the most common I/O channel of a process.

fn stdin() -> Stdin;
fn stdout() -> Stdout;
fn stderr() -> Stderr;

struct Stdin { /* read line / bytes / etc. */ }
struct Stdout { /* line-buffered on a tty, block-buffered on a pipe */ }
struct Stderr { /* unbuffered */ }

impl Reader for Stdin { ... }
impl Writer for Stdout { ... }
impl Writer for Stderr { ... }

impl Stdin {
    fn lock(self) -> StdinLock;          // exclusive lock - multithread-safe
    fn read_line(self) throws (IoError) -> str;
    fn lines(self) -> Lines<Stdin>;      // implements Iterator<Item = Result<str, IoError>>
}

stdout() shares the same runtime buffer as print / println, so output order is preserved even when they are mixed. This buffer is flushed when the process ends, when flush() is called, and when the buffer fills. stderr writes immediately without a buffer. stdin() also uses a single shared buffer.

print / println / eprint / eprintln, commonly used with stdout/stderr, live in rok::core::print::*, so they are exposed automatically through the implicit use rok::core::**; without a separate import.

3.5.3. BufReader / BufWriter / LineWriter

Using raw streams directly tends to accumulate system call costs, so the standard library provides the following buffering wrappers.

struct BufReader<R: Reader> { /* internal buffer */ }
struct BufWriter<W: Writer> { /* internal buffer */ }
struct LineWriter<W: Writer> { /* line-buffered */ }

impl<R: Reader> BufReader<R> {
    fn new(inner: R) -> BufReader<R>;
    fn with_capacity(cap: usize, inner: R) -> BufReader<R>;
    fn into_inner(self) -> R;
}

impl<R: Reader> Reader for BufReader<R> { ... }

impl<W: Writer> BufWriter<W> {
    fn new(inner: W) -> BufWriter<W>;
    fn with_capacity(cap: usize, inner: W) -> BufWriter<W>;
    fn into_inner(self) throws (IoError) -> W;     // flushes the remaining buffer, then returns
}
impl<W: Writer> Writer for BufWriter<W> { ... }

impl<W: Writer> LineWriter<W> {                    // flush on every write containing \n
    fn new(inner: W) -> LineWriter<W>;
}
impl<W: Writer> Writer for LineWriter<W> { ... }

BufWriter implements Drop, so it flushes the remaining buffer when leaving a using block. Errors at that point are ignored; to check for errors, call flush() directly before leaving. LineWriter flushes on every write that contains \n.

In short, the main benefit is fewer system calls when small reads are frequent.

3.5.4. copy - io::copy(reader, writer)

Simple copying between streams has its own function.

fn copy<R: Reader, W: Writer>(ref reader: R, ref writer: W) throws (IoError) -> u64;

Internally it uses splice / sendfile / copy_file_range where possible, but from user code a single call is all it takes. Given two descriptors (raw_fd), the kernel path is picked per descriptor kind; otherwise read / write.

3.5.5. Examples

A typical use combining the pieces so far:

use rok::std::io::{stdin, stdout, BufReader, BufWriter};

// line-by-line processing
let stdin = stdin().lock();
for line in stdin.lines() {
    let line = try line.get();       // iterator elements are Result - get() returns to the error flow
    process(line);
}

// buffered writes
let f = try fs::create("output.txt");
let buf = BufWriter::new(f);
for x in xs {
    try buf.write_str(f"{x}\n");
}
try buf.flush();    // explicit flush - avoids a silent failure at release time

// copy
let src = try fs::open("a.txt");
let dst = try fs::create("b.txt");
try io::copy(ref src, ref dst);

Edit this page on GitHub