Skip to content

Input and Output

Readers and writers ​

A stream is one int32. It has nowhere to keep leftover bytes, which is why stdin->readline() is one read per byte. Buffering needs a value you hold.

reader and writer wrap a stream. They don't own the descriptor and they don't close it. The last writer flushes.

echo
std::io::reader $in = std::io::reader(std::io::stdin);
string? $line = guard $in->readline() else ($e) {
    std::io::eprintln($e->message());
    return 1;
}

echo $line == null;
echo
std::io::writer $out = std::io::writer(std::io::stdout);
usize $n = guard $out->write('chevron') else ($e) {
    std::io::eprintln($e->message());
    return 1;
}

usize $z = guard $out->flush() else ($e) {
    std::io::eprintln($e->message());
    return 1;
}

echo $n;

Leftover bytes now have somewhere to sit, so a line is not one syscall per character.

The window ​

write and writeline go into an 8 KiB window and hit the kernel when it fills, on flush, or when the last handle goes. Reads fill the same size of window. readline scans it for a newline and only hits the kernel when the line crosses a fill.

If you wrap stdout, flush before echo. echo goes through printf, the writer through write(2), and an unflushed window is a third place for output to sit.

No ownership ​

A reader or writer copies the fd out of the stream and forgets the stream. Closing stdin because a reader went out of scope would be a hell of a surprise, so they don't. A std::io::file is the type that closes what it opened.

The last writer does flush. A reader does nothing on the way out: unread bytes in the window are simply dropped, and the next reader of that descriptor will not see them. That's the cost of having stolen them from the kernel in the first place.

Live files ​

A std::io::file already has both windows. Wrapping its fd() in a reader or a writer would steal bytes from the file's own window, and the two would disagree about where you are. If you have a std::io::file, use that.

These two are for streams you don't own: stdin, stdout, stderr, or a descriptor someone else handed you.

Pipes own their end ​

pipe_reader and pipe_writer are the two ends of a pipe a child process was started with. Same windows, same result<..., ioerror>, but they do own their end, like a file, and the last handle closes it. Nothing in std::io makes one; std::process hands them out when a stream is .piped.

close() on the writer is how the other side sees the end of its input. writeline ends a line with \n on every platform: a pipe carries bytes rather than a console's idea of a line. Neither end seeks. A pipe has no position, which is why these are their own types rather than a file whose seek always fails.

A write into a pipe whose reader is gone fails with EPIPE as an ordinary ioerror. Stdout is the other case: that ends the program, see Input and Output.

Surface ​

reader(stream)a read window. does not own, does not close
reader::readinto a pointer and a count. 0 is EOF
reader::readlineresult<string?, ioerror>. ok(null) is EOF
reader::readallthe rest of the descriptor, as a string
writer(stream)a write window. does not own, does not close
writer::writeresult<usize, ioerror>, into the window
writer::writelinethe same, then a newline
writer::flushdrain the window. the last handle does this
pipe_reader::read / readline / readalllike reader's, over a pipe end it owns
pipe_reader::closeclose the end. buffered bytes are dropped
pipe_writer::write / writeline / flushlike writer's, over a pipe end it owns
pipe_writer::closeflush, then close: end of input for the other side

ioerror is the same type files use. "{$e}" is message().

Next ​

Echo is a work in progress. Nothing here is a promise of stability.