Arthur Carlsson bio photo

Arthur Carlsson

Java developer but keen on software development in general

Twitter LinkedIn Github GitLab

The last post was nothing more than an introduction to the project. In this post we get down to some initial coding, although it’s not really anything N64 specific. We focus on command line parsing and loading roms.

The Command Line

So what needs to be provided through the command line is two things: the PIF ROM and the cartridge ROM (in the future we might just emulate the PIF ROM as well). These are just binary blobs which needs to be read into memory and used later at runtime. We’ll come back to what roles they play in the Nintento 64 ecosystem in the next part.

I’m thinking the command line should be something like rustn64 --pif mypif.bin myrom.z64 and there are a few ways to parse the command line. You could simply read the raw arguments from the command line using std::argv. This is perfectly fine but usually you want to have some validation. What if there are to few arguments? What if the argument is of the wrong format? Should we be able to handle flags vs. values?

On *nix there is getopt but first, it’s a C library which means we have to interface it through Rust and second, it’s not available on Windows of course. Note that there are crates which already have implemented the interface to getopt in Rust.

There are a number of crates for parsing the command line and the option I’ve gone for is to use the clap crate. In src/main.rs we find the snippet that reads the command line:

let matches = App::new("RustN64")
  .version(env!("CARGO_PKG_VERSION"))
  .author("Arthur Carlsson <[email protected]>")
  .about("A Nintendo 64 emulator written in Rust")
  .arg(Arg::with_name("PIF")
    .help("The PIF ROM file to use")
    .short("p")
    .long("pif")
    .use_delimiter(false)
    .takes_value(true)
    .required(true))
  .arg(Arg::with_name("ROM")
    .help("The ROM file to use")
    .use_delimiter(false)
    .required(true)
    .index(1))
  .get_matches();

Here we specify the application’s name (RustN64), the version (using a neat trick so that by a macro, the version gets picked up at compile time to match the version in Cargo.toml), author and a short description. Then the two arguments are specified where the first one is the PIF file and the second the game cart ROM.

Besides the fact that we get a convenient way of specifying the command line arguments, we also get a help flag for free:

Arthurs-MBP:rustn64 arthurcarlsson$ target/debug/rustn64 -h
RustN64 0.1.0
Arthur Carlsson <[email protected]>
A Nintendo 64 emulator written in Rust

USAGE:
    rustn64 <ROM> --pif <PIF>

FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

OPTIONS:
    -p, --pif <PIF>    The PIF ROM file to use

ARGS:
    <ROM>    The ROM file to use

… and a version flag:

Arthurs-MBP:rustn64 arthurcarlsson$ target/debug/rustn64 -V
RustN64 0.1.0

Reading Files in Rust

I also wanted to show a pretty nice way on how files are read in Rust. Basically most of the file operations return an std::io:Result (std::io::File::open for instance) which takes two generic arguments - the first is the result of the operation and the second is an std::io::Error in case of failure. Note that there is no such thing as exceptions in Rust, you typically return multiple values instead.

The Result type can chain operations together and short circuit if an error occurs. This is what the read_bin() method looks like in src/main.rs:

fn read_bin<P: AsRef<Path>>(path: P) -> Vec<u8> {
  let mut buf = vec![0];

  match File::open(path.as_ref()).and_then(|mut f| f.read_to_end(&mut buf)) {
    Ok(_) => buf,
    Err(e) => {
      println!("Failed to load \"{}\": {}", path.as_ref().display(), e);
      std::process::exit(1);
    }
  }
}

If an error were to occur in either File::open() or File::read_to_end we’d have the same error handler in the match expression. Pretty sweet!

What more is that you can use the try! macro which expands to a match statement that returns early if an error occurs. The code above could be written as follows:

fn main() {
  // ...

  match read_bin(some_path) {
    Ok(buf) => do_something_with_buf(buf)
    Err(e) => panic!("Some error!")
  }
}

fn read_bin<P: AsRef<Path>>(path: P) -> Vec<u8> {
  let mut buf = vec![0];

  try!(File::open(path).and_then(|mut f| f.read_to_end(&mut buf)));

  Ok(buf)
}

This makes for really concise file handling while still maintaining fault tolerance.

Next Part

In the next part we’ll finally get to some more interesting stuff where we’ll have a look at how we will model the CPU. Hopefully we’ll be able to execute the PIF ROM.