Mehdi Mulani

Building an Emulator with Rust

In an effort to learn Rust, I built a Game Boy emulator in two weeks. Coming from a mobile and web background, and being used to building huge apps with slow build times, it was really fun to work in a new language on a fun project.

My toy emulator running Dr. Mario.

In this post, I'll share my thoughts on how using Rust felt compared to the languages I'm more familiar with (mainly Objective-C and JavaScript).

Enums

Enums in Rust are terrific. In particular, it's really nice that you can associate data with an enum variant and moreso, that the associated data can change with each variant. For instance, I used enums to store all the Game Boy opcodes.

pub enum Opcode {
    Noop,
    Stop,
    Halt,
    Load16(Register, Register, u16),
    Load8(Register, u8),
    LoadReg(Register, Register),
    LoadAddress(Register, u16),
    LoadAddressFromRegisters(Register, Register, Register),
    LoadRegisterIntoMemory(Register, Register, Register),
    ...
}

It was super nice to have all the information about an opcode stored concisely, whereas in Objective-C I might have stored this kind of information through subclassing.

match

Rust's version of switch is called match and it's quite enjoyable to use with enums, as you can destructure so easily. For instance, this is some code from my emulator's fetch-execute cycle.

match opcode {
    Opcode::Noop => (),
    Opcode::Jump(address) => jump_location = Some(address),
    Opcode::JumpCond(flag, set, address) => {
        cycle_cost = 12;
        if self.get_flag(flag) == set {
            jump_location = Some(address);
            cycle_cost = 16;
        }
    }
    ...
}

While something like this is definitely possible in JavaScript, I don't think it would be as concise as it is here.

Always return the last expression

One concept that is a bit hard to explain but extremely intuitive to use is the last expression in any block will be the "return" value for that block. For instance, in this function there is no return statement.

fn read_memory_bit(&self, address: u16, bit: u8) -> bool {
    assert!(bit <= 7);
    (self.read_memory(address) & (1 << bit)) != 0
}

But Rust will automatically return the last line. It gets better, in this function there is again no return statement but there is an if block.

fn cond_memory_bit<T>(&self, address: u16, bit: u8, not_set: T, set: T) -> T {
    if self.read_memory_bit(address, bit) {
        set
    } else {
        not_set
    }
}

In this case, Rust will return either set or not_set, as they are the last expressions to be evaluated. What's even more crazy is that this idea of "last expression in any block is returned automatically" applies to everything, not just functions. For instance, take a look at this bit variable:

let bit = match new_stat_mode_flag {
    0 => LCDCInterruptBit::HBlank,
    1 => LCDCInterruptBit::VBlank,
    2 => LCDCInterruptBit::OAM,
    _ => unreachable!(),
};

bit will be assigned to the right-hand expression of whichever match statement succeeds, and it is so easy to read! In JavaScript, you could do something like this with a dictionary but it would definitely look more hacky. And then in Objective-C, this is usually written as a series of if-else blocks.

Tests inline

In Rust, it is common to write unit tests in the same file, right next to function definitions! For instance, these were some of my opcode tests:

fn opcode(bytes: &[u8]) -> (gba::Opcode, u16, u16) {
    let rom = gba::ROM::from_bytes(bytes.to_vec());
    rom.opcode(0, |address| bytes[address as usize])
}

#[test]
fn test_opcodes() {
    assert_eq!(opcode(&[0x0]), (Opcode::Noop, 1, 4));
}

Having the tests right next to the method felt really good, as it was helpful to see usages of the method right next to its definition. This also made it so easy to add a new unit test, as there was such a low barrier. Compared to JavaScript, where tests are usually in a separate-but-adjacent file, or Objective-C, where tests live in an entirely different build target, Rust tests felt really easy to make.

Very Concise

Overall, I felt like Rust allowed me to be very concise in my coding. My toy emulator takes up only 3,000 lines of code. Though it can only play some simple ROMs, it implements most of the Game Boy system. If I were to have written it in JavaScript or Objective-C, I would expect it to be at least 10,000 lines of code. But more importantly, with Rust, the code still feels easy to follow, and not frustratingly dense.

The best example of this is in the opcode parsing code. There are around 70 different opcodes, and the parsing code generally looks like this:

pub fn opcode(&self, address: u16, reader: impl Fn(u16) -> u8) -> (Opcode, u16, u16) {
    let opcode_value = reader(address);
    match opcode_value {
        0x00 => (Opcode::Noop, 1, 4),
        0x08 => (Opcode::SaveSP(immediate16()), 3, 20),
        0x09 => (Opcode::AddHL(Register::B, Register::C), 1, 8),
        0x19 => (Opcode::AddHL(Register::D, Register::E), 1, 8),
        0x29 => (Opcode::AddHL(Register::H, Register::L), 1, 8),
        0xCB => {
            let cb_instr = immediate8();
            let cycle_count = if (cb_instr & 0x7) == 0x6 {
                if cb_instr >= 0x40 && cb_instr <= 0x7F {
                    12
                } else {
                    16
                }
            } else {
                8
            };
            (self.cb_opcode(cb_instr), 2, cycle_count)
        }
    }
}

Check out the branches of this match statement. While some generate an opcode based merely on the value of opcode_value, the 0xCB block runs a bunch of logic and eventually returns an opcode. And because of the "last expression in a block always returns" rule, you can easily express this kind of logic.

Overall, with Rust, the code can be short-and-simple if possible, or long-and-complicated if necessary. But critically, these two solutions can easily co-exist.

Cargo

Having an official package manager for your programming language is truly a godsend. In JavaScript, there's so much headache from dealing with the competing standards that npm, Yarn, TypeScript, Babel, etc. all inject into your project. And then in Objective-C, there's CocoaPods vs dylibs vs git submodule vs copy-and-paste code.

Suffice to say, being able to specify dependencies in an easy, standardised way was truly enjoyable.

Conclusion

Overall, as you can tell, I really enjoyed writing this emulator in Rust. While I don't get to use it in my day job, I definitely want to find a way to use it more often. Or at the very least, incorporate some of the nice bits into whatever I use.

Back to profile