mostly mis-adventures with programming languages...mostly...

Blog Posts


I/O Fundamentals with Rust

Introduction

I've been doubling down on Rust now for nearly 3 years. Some things with the language have only become clear to me in literally in the past couple of months. The notion of traits, for instance, and how to use them, has eluded me. It still does, but maybe the picture is becoming more clear.

Traits are essential. Everything in Rust that's important uses them, even the standard library.

Let's take a look at how we can:

  • read the contents from a named file
  • write those exact contents out to another file
  • output the number of bytes that we wrote

Easy peasy stuff, let's go.

Back to Basics, Just Me and the Rust Compiler

We aren't going to use cargo for this super simple code we're about to write. All we're going to do is use the rustc compiler. To begin, make yourself a folder whereever you do your coding projects. I called mine io-concepts. Then, change into that folder and do touch main.rs (or ni main.rs if in Powershell). This gets you a completely empty file to code in. Next, make a text file in the root of the same folder where you just created main.rs, call it whatever you want. Put some content in it, whatever you want, this is our input file.

Next up what you just created in your editor of choice.

Type in this:

// main.rs

// dependencies
use std::fs::File;
use std::io::{Read, Result, Write};

// main function
fn main() -> Result<()> {
  let mut input_file = File::open("input.txt")?;
  let mut input_data = Vec::new();
  input_file.read_to_end(&mut input_data)?;
  println!("Input file contents represented as bytes: {:?}", input_data);
  let mut output_file = File::create("output.txt")?;
  output_file.write(&input_data)?;
  println!("Wrote: {} bytes", input_data.len() * std::mem::size_of::<u32>());

  Ok(())
}

If you typed that in, compile it by doing rustc main.rs. Resolve any compiler errors that you might receive until there are none left. Run the resulting main.exe (if on Windows) that you'll find in the project root and you should see a vector of bytes along with the number of bytes written to file.

Let's walk through it.

  • We bring the File type from the std::fs module into scope, the File type affords us different methods to open and create files
  • the Read, Result, and Write traits from the std::io module are brought into scope, affording us:
    • the ability to read file contents into a variable type of our choosing
    • the Result type, which is a type alias for the Result<T, E> type available in std::io
    • the ability to write contents back to a file
    • it's important to note that these traits are already implemented for the File type will be using, we don't have to write their implementations from scratch
  • we create a main() function, which is the starting point of our executable file, we make it return a Result<()> type, which will be the unit type () in the success cass or an error type
  • in our main() function we:
    • bind a file handle to a variable named input_file, this variable needs to be mutable
    • bind a new, empty vector to a variable named input_data
    • use the read_to_end method on our input_file handle, passing in a reference to the input_data type, which reads the contents of our file in as bytes
    • output a message, using the println!() macro, confirming we read the file and writing its contents back do stdout
    • bind a file handle to a variable named output_file, this variable needs to be mutable
    • write the input data out, by calling the write() method and passing a mutable reference ot our input_data
    • output a message, again using the println!() macro, confirming the number of actual bytes written
  • terminate by returning an Ok(()) result, wrapping a unit value

You may be wondering why the input_file and output_file variables need to be mutable. In both cases, the acts of reading and writing change the respective files internal state, namely the position/cursor that's used to faciliate the internal location where to read and write to. This need to change state means the variable must be able to be changed, so it must be mutable.

That's it! I hope you've found benefit from this brief refresher on key aspects of file IO in Rust. Check out the std:fs and std::io documentation in the Rust standard library for more!

References

The Rust Standard Library