29 comments

[ 3.2 ms ] story [ 38.3 ms ] thread
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

In my first attempt at a PlayStation emulator I did something similar to what you describe, with the shared state in a separate struct that's passed around and the rest of the code embedded in member function for the various GPU, CPU, SPU and other classes.

In the end I found that it was more trouble that was worth. I didn't feel like it made the code significantly clearer and if you realized later on that something actually ought to be shared between two modules it required a ton of refactoring.

Also having everything in a single struct that's passed everywhere can make debugging slightly easier. For instance if I encounter a situation where some bogus command is sent to the GPU I catch it in the GPU code and dump the CPU state right there instead of having to notify the condition upstream and add some hook in the higher layers to handle the CPU dumping for instance.

It's probably not the most elegant solution but I find it simple and rather effective.

Ye I agree. Copying a state to not make the memory shared when the actual data is rw shared (all modules should have the same conception of what the state is) seems quite pointless. I guess a pointer to const get function and a pointer to non-const get function would suffice for making it clear which modules could manipulate it.
> using a more functional style instead of class methods

Does Rust even have classes?

`impl` methods with `self` parameters are basically class methods in feel.
Great project! But other than it being 'written in Rust' can someone explain what is interesting about yet another Gameboy Advance emulator using SDL2? I'm guessing its WASM support?

I think we're all capitalising on the HN algorithm gearing towards Rust-related posts which can only be a great thing for those who absolutely love Rust <3. Perhaps I should be a town crier for the Rust project to market all things Rust.

𝔒𝔶𝔢𝔷!, 𝔒𝔶𝔢𝔷!, 𝔒𝔶𝔢𝔷! 𝔄𝔫𝔬𝔱𝔥𝔢𝔯 𝔤𝔯𝔢𝔞𝔱 ℜ𝔲𝔰𝔱 𝔭𝔯𝔬𝔧𝔢𝔠𝔱!

> 𝔒𝔶𝔢𝔷!, 𝔒𝔶𝔢𝔷!, 𝔒𝔶𝔢𝔷! 𝔄𝔫𝔬𝔱𝔥𝔢𝔯 𝔤𝔯𝔢𝔞𝔱 ℜ𝔲𝔰𝔱 𝔭𝔯𝔬𝔧𝔢𝔠𝔱!

What the hell are those? I looked at each letter (Unicode 1D5{12,36,22,37}), and apparently it spells out Oyez?

https://en.wikipedia.org/wiki/Oyez

Oyez (/oʊˈjɛz/, /oʊˈjeɪ/, /oʊˈjɛs/, more rarely with the word stress at the beginning) is a traditional interjection said two or three times in succession to introduce the opening of a court of law, especially in Great Britain. The interjection is also traditionally used by town criers to attract the attention of the public to public proclamations.

Oyez means “Here ye!” Town criers used to say it.
"Listen!" is a better translation.

Edit: Maybe you meant 'hear' not 'here'.

Thank you for pointing it out, even though I'm French I read it as "Onez! Onez!", I thought it was meant to be 1337speak.
I have been long enough on HN to have seen that pattern with CoffeeScript, Clojure, Scala, Ruby, Go, Elixir,....

Now excuse me while I get back to selling shovels.

I notice the same thing happen with other languages on HN. What I find interesting is that Rust seems to always get some variation of the comment: “why is this interesting, this has already been done in other languages”.

I don’t know why the hate, but I would say HN is a community with a lot of developers, and if things are getting voted up, it’s because many of the folks on here might have enjoyed seeing the post/article/project.

Because porting to another langugage is rarely interesting unless there's a performance or resource usage win. For example, if all of wc, ls, grep etc were ported to Rust and they ran far more quickly, that'd be post worthy.

Someone compiled linux with a C++ compiler a few months ago and the compiler found a few bugs. That was interesting and useful.

https://lkml.org/lkml/2018/4/1/116

Of course there is academic interesting (as in, I wrote an emulator in BASH and it actually kinda works) and there is practical interesting (I got the emulator to be 3% faster).

The Rust community really has an interest in showing the world that it is a serious candidate to replace C++. That's why we see a preponderance of "this same problem, but solved in Rust" - they're providing another data point to show that Rust can work in a problem space that had previously been the sole domain of C/C++. In that regard, I would think it interesting.

Case in point, had I not had the fortune of having access to Native Oberon during the university, I would also be an anti-GC voice.

Instead I learned what Oberon allowed me to do, got to dive into the history of programming language research at Xerox PARC, and became a firm believer in GC enabled systems programming languages.

Programming languages landscape lacks dreamers like Alan Kay or Jobs, apparently many only believe in stuff that is demoed in front of them and even if proven wrong, they only care if they can take immediate profit from it and not 5 years down the road.

I think ports to other languages are interesting even if there aren’t practical performance gains. Rust is cool. Other languages are cool too. X, but in Y is cool, at least cool enough to glance at.
What would the shovel be in this case? LLVM?
Algorithms + Data Structures = Programs, as Wirth puts it.
It just sounds like you think everything that existed before you started programming is good and useful and everything after is a fad.
Rust good, C bad.
On the contrary, I have a couple of decades of experience seeing technologies coming and go, like Alan Kay puts it, IT is a fashion industry.

So always take a critic view on whatever is on the cover of IT Vogue.

So another post about Rust. Nice.