Rust I/O Traits

There are two kinds of computer:

  • Windows NT based
  • POSIX based (macOS, Linux, QNX, etc)

Rust supports both.

Note:

We're specifically talking about libstd targets here. Targets that only have libcore have very little I/O support built-in - it's all third party crates.

They are very different:

HANDLE CreateFileW(
  /* [in]           */ LPCWSTR               lpFileName,
  /* [in]           */ DWORD                 dwDesiredAccess,
  /* [in]           */ DWORD                 dwShareMode,
  /* [in, optional] */ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  /* [in]           */ DWORD                 dwCreationDisposition,
  /* [in]           */ DWORD                 dwFlagsAndAttributes,
  /* [in, optional] */ HANDLE                hTemplateFile
);

int open(const char *pathname, int flags, mode_t mode);

Abstractions

To provide a common API, Rust offers some basic abstractions:

  • A Read trait for reading bytes
  • A Write trait for writing bytes
  • Buffered wrappers for the above (BufReader and BufWriter)
  • A Seek trait for adjusting the read/write offset in a file, etc
  • A File type to represent open files
  • Types for Stdin, Stdout and Stderr
  • The Cursor type to make a [u8] readable/writable

The Read Trait

https://doc.rust-lang.org/std/io/trait.Read.html

#![allow(unused)]
fn main() {
use std::io::Result;

pub trait Read {
    // One required method
    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
    // Lots of provided methods, such as:
    fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { todo!() }
}
}

Immutable Files

  • A File on POSIX is just an integer (recall open returns an int)
  • Do you need a &mut File to write?
    • No - the OS handles shared mutability internally
  • But the trait requires &mut self...

Implementing Traits on &Type

impl Read for File {

}

impl Read for &File {

}

See the std::io::File docs.

OS Syscalls

  • Remember, Rust is explicit
  • If you ask to read 8 bytes, Rust will ask the OS to get 8 bytes from the device
  • Asking the OS for anything is expensive!
  • Asking the OS for a million small things is really expensive...

Buffered Readers

  • There is a BufRead trait, for buffered I/O devices
  • There is a BufReader struct
    • Owns a R: Read, and impl BufRead
    • Has a buffer in RAM and reads in large-ish chunks
#![allow(unused)]
fn main() {
use std::io::BufRead;

fn print_file() -> std::io::Result<()> {
    let f = std::fs::File::open("/etc/hosts")?;
    let reader = std::io::BufReader::new(f);
    for line in reader.lines() {
        println!("{}", line?);
    }
    Ok(())
}
}

The write! macro

  • You can println! to standard output
  • You can format! to a String
  • You can also write! to any T: std::io::Write
use std::io::Write;

fn main() -> std::io::Result<()> {
    let filling = "Cheese and Jam";
    let f = std::fs::File::create("lunch.txt")?;
    write!(&f, "I have {filling} sandwiches")?;
    Ok(())
}

Networking

End of the Line

  • It's obvious when you've hit the end of a File
  • When do you hit the end of a TcpStream?
    • When either side does a shutdown

Note:

  • Read trait has a method read_to_end()

Binding Ports

  • TcpListener needs to know which IP address and port to bind
  • Rust has a ToSocketAddrs trait impl'd on many things
    • &str, (IpAddr, u16), (&str, u16), etc
  • It does DNS lookups automatically (which may return multiple addresses...)
fn main() -> Result<(), std::io::Error> {
    let listener = std::net::TcpListener::bind("127.0.0.1:7878")?;
    Ok(())
}

More Networking

Note:

Some current prominent examples of each -

Failures

  • Almost any I/O operation can fail
  • Almost all std::io APIs return Result<T, std::io::Error>
  • std::io::Result<T> is an alias
  • Watch out for it in the docs!