← Read More

Reading Back What You Wrote in Rust

Published on 2021-06-21

TLDR: An &mut can implement a trait like Write. So if a struct takes ownership of a generic type that implements Write you can provide an &mut instead so the struct doesn't need to take ownership of the data.

This is another very quick Rust tip. I am thoroughly on the Rust hype train these days and I find myself learning really cool little details about Rust that were not immediately obvious to me. This tip may be too basic to be interesting but most of the unparsing examples I saw don't show this kind of usage and it took me an embarassingly long amount time to figure it out.

I was working on a parsing/unparsing library in Rust and I was trying to use the unparser when I came across a problem. Unparsers take ownership of an underlying Write to write the actual data to. This makes total sense. If the unparser required you to borrow the underlying writer you would need to always pass them around together and make sure their lifetimes are compatible; it would be a huge hassle. My problem was that I wanted to read the data I had just written but my unparser owned that data so I couldn't access it anymore.

Take a look at this implementation of an overly simple CSV unparser:

use std::io;
use std::io::Write;

pub struct MyWriter<W: Write> {
    writer: W,
}

impl <W: Write> MyWriter<W> {
    pub fn new(writer: W) -> Self {
        MyWriter { writer }
    }

    pub fn write(&mut self, row: &[&str]) -> io::Result<()> {
        self.writer.write(row.join(", ").as_bytes())?;
        self.writer.write(&[b'\n'])?;
        Ok(())
    }
}

This takes ownership of a Write. Usually you will be writing to something like a file so this works great. We can even return our new MyWriter on it's own:

use std::io;
use std::fs::File;

fn writer_with_headers() -> io::Result<MyWriter<File>> {
    let file = File::create("foo.csv")?;
    let mut writer = MyWriter::new(file);
    writer.write(&["a", "b", "c"])?;
    Ok(writer)
}

But what happens if we want to read the data we just wrote, like for a test?

use std::io;

#[test]
fn test_writing() {
    let v = Vec::new();
    let mut writer = MyWriter::new(v);
    writer.write(&["a", "b", "c"]).unwrap();
    assert_eq!(std::str::from_utf8(&v).unwrap(), "a, b, c\n");
}

We get an error from the compiler:

error[E0382]: borrow of moved value: `v`
  --> src/main.rs:32:36
   |
29 |     let v = Vec::new();
   |         - move occurs because `v` has type `Vec<u8>`, which does not implement the `Copy` trait
30 |     let mut writer = MyWriter::new(v);
   |                                    - value moved here
31 |     writer.write(&["a", "b", "c"]).unwrap();
32 |     assert_eq!(std::str::from_utf8(&v).unwrap(), "a, b, c\n");
   |                                    ^^ value borrowed here after move

We have given ownership of v to writer, we can't use it anymore. I was stumped. How can we test unparsers if it is impossible to read back the data we wrote? I even ended up writing to a temporary file then reading that file back in but that was ugly, and there are times when you need to read back the data at runtime and this impacts performance substantially.

I am sure by now some of you are banging your head against the wall because of how simple the answer really is. What I didn't know is that &mut Vec<u8> also implements Write so we don't need to give the unparser ownership of the Vec<u8> itself to give it ownership of something that implements Write we can give it an &mut Vec<u8> like so:

use std::io;

#[test]
fn test_writing() {
    let mut v = Vec::new();
    {
        // create a new MyWriter and mutably borrow v inside this scope
        let mut writer = MyWriter::new(&mut v);
        writer.write(&["a", "b", "c"]).unwrap();
        // moving out of this scope destroys `writer` and releases our mutable
        // borrow on v so we can read it again on the next line
    }
    assert_eq!(std::str::from_utf8(&v).unwrap(), "a, b, c\n");
}

It turns out library designers really do know what they are doing. Taking ownership of a generic type that implements Write lets you give the unparser ownersip of the underlying Write if you want but it doesn't force you to. This is yet another awesome usage of type bounds with generics. If this is interesting to you you might want to check out my previous post on Rust type bounds with traits.

← Read More