Rust is a nice language for emulators in my experience, although managing the large global state emulators require while making the borrow checker happy and not damaging performance requires a bit of planning (unless you're willing to litter your code with unsafes).

My strategy in the end is simply to stuff the entire state in a struct that I pass around everywhere, using a more functional style instead of class methods. This way I don't have to borrow anything and I have access to the full state everywhere in the code. It doesn't make for clean OOP-style encapsulation but I found that doing that was too complicated in an emulator, you simply have too many interactions between the various modules of most consoles. For instance the DMA can write data to the GPU which can trigger an IRQ in the interrupt controller which can change the CPU state which can modify the state of a coprocessor which can lead to registers being banked into RAM etc...

Besides, the architecture of an emulator is generally constrained by the underlying hardware, so it's rare that you have to do big refactors. Having leaky interfaces is not much of a problem in practice because you don't really have to worry about "but what if tomorrow I need to emulate a Game Boy Advance with a very different GPU?".

While we're showing off Rust emulator projects I've spent the last few days writing an emulator for the PlayStation's CD-ROM sub-CPU: https://gitlab.com/flio/psx_cd . I hope to be able to integrate it into my PlayStation emulator when it's done, which would save me from having a super hacky high level CD interface like most other PSX emulators out there.

One idea I had in my own modular emulators a while ago was to reduce the "state that's passed around" to the input/output pins of the microchip emulators the higher level system emulator is built from. This pin-state often fits into a single 64-bit integer (at least for typical 8-bit home computers), and another advantage is that the emulator's code structure can be kept very close to the schematics of the real hardware (the wiring between chips can be mapped 1:1 to bits in 64-bit integers).

The emulators are written in C, not Rust, but I bet this approach would also work perfectly with Rust's borrow checker (since there's no shared state in memory the borrow checker shouldn't even "activate"). An emulator is essentially built from functions which take a 64-bit integer as input, and return another 64-bit integer.

https://github.com/floooh/chips