Rust IO Wrapper Types
An IO wrapper is a filter type that wraps a reader and changes the bytes as they pass through. The wrapper implements the Read trait.
A wrapper holds an inner reader
The generic R means it can wrap any reader at all:
struct ShoutReader<R: Read> {
input: R,
}Implement Read for the wrapper
Your read fills the caller's buffer by calling the inner reader's read first. Transform only the bytes that were actually read: buf[..size]. Here size is the count the inner read returned. Then return Ok(size). The caller learns how many bytes arrived. Errors pass through with ?: an inner failure becomes the caller's failure too.
impl<R: Read> Read for ShoutReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let size = self.input.read(buf)?;
for byte in &mut buf[..size] {
*byte = byte.to_ascii_uppercase();
}
Ok(size)
}
}The free methods come along
Because the wrapper implements Read, the free methods work on it too: read_to_string, read_to_end, and wrapping it in a BufReader. Import the trait with use std::io::Read to see them:
let mut reader = ShoutReader { input: "hello rust".as_bytes() };
let mut text = String::new();
reader.read_to_string(&mut text).unwrap();
println!("{text}");A reader that lives in memory
Cursor::new wraps bytes or a Vec<u8>. It implements Read, Write and Seek. So code built for files can run on plain memory. That is handy in tests. The bytes 104 and 105 are the letters h and i:
let mut cursor = Cursor::new(vec![104, 105]);
let mut text = String::new();
cursor.read_to_string(&mut text).unwrap();
println!("{text}");Example
use std::io::{self, Read};
struct ShoutReader<R: Read> {
input: R,
}
impl<R: Read> Read for ShoutReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let size = self.input.read(buf)?;
for byte in &mut buf[..size] {
*byte = byte.to_ascii_uppercase();
}
Ok(size)
}
}
fn main() {
let mut reader = ShoutReader { input: "hello rust".as_bytes() };
let mut text = String::new();
reader.read_to_string(&mut text).unwrap();
println!("{}", text);
}Key points
- A wrapper reader changes the bytes from an inner reader before its callers see them.
- Read is the trait behind every input source: files, text in memory, the keyboard and the network all implement it
- A read that returns Ok(0) means the input is finished.
- A wrapper must pass that through untouched.
- Otherwise, callers loop forever.
In this example, ShoutReader wraps the bytes of the text "hello rust". Its read first asks the inner reader to fill the buffer, and gets back the count of bytes that arrived. It then uppercases exactly those bytes in place — to_ascii_uppercase changes letters and leaves spaces and digits alone. read_to_string is a free method that comes with the Read trait. It keeps calling read until read returns 0. It gathers the result as it goes. The printed line is the transformed text: HELLO RUST.
Note
This is the adapter pattern inside real Rust libraries.
Decoders are wrapper types.
Buffered readers and progress meters are too.
Each one implements Read or Write around an inner one.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why does the wrapper loop over buf[..size] instead of the whole buffer?