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
andBufWriter
) - A
Seek
trait for adjusting the read/write offset in a file, etc - A
File
type to represent open files - Types for
Stdin
,Stdout
andStderr
- 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 (recallopen
returns anint
) - 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
, andimpl BufRead
- Has a buffer in RAM and reads in large-ish chunks
- Owns a
#![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 aString
- You can also
write!
to anyT: 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
- In Rust, a
TcpStream
also implements theRead
andWrite
traits. - You create a
TcpStream
with either:TcpStream::connect
- for outbound connectionsTcpListener::accept
- for incoming connectionsTcpListener::incoming
- an iterator over incoming connections
- As before, you might want to wrap your
TcpStream
in aBufReader
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
- When either side does a
Note:
Read
trait has a methodread_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
- There is also
std::net::UdpSocket
IpAddr
is an enum ofIpv4Addr
andIpv6Addr
SocketAddr
is an enum ofSocketAddrV4
andSocketAddrV6
- But TLS, HTTP and QUIC are all third-party crates
Note:
Some current prominent examples of each -
- TLS - RusTLS
- HTTP - hyperium/http
- QUIC - cloudflare/quiche
Failures
- Almost any I/O operation can fail
- Almost all
std::io
APIs returnResult<T, std::io::Error>
std::io::Result<T>
is an alias- Watch out for it in the docs!