Post

Crystal: The Language I Didn't Know I Needed

A few months ago I was searching “Mario Bros NES online” at 2 AM. Pure nostalgia. As a kid I used to play that game at my grandparents’ house because they had nothing else installed on their PC. I knew all the warps, the infinite lives trick in 3-1, the whole deal.

I found an emulator, played for 15 minutes, died in world 2-3 as always, and closed the tab. But something stuck with me: how hard is it to code one of these?

I’d always been fascinated by the idea of virtual machines, how you can emulate a bunch of machines inside your own hardware. I didn’t think it was black magic, but I’d never actually looked at emulator code before. What does it even mean, in code terms, to emulate a console? A lot of assembly? I had no idea.

So after playing I went down a rabbit hole: YouTube tutorials about the NES, reading the NES Wiki, trying to understand the architecture. And then I started coding the CPU, since that’s what I felt I understood best from my brief research. It helped that I’d built half of a 4-bit CPU using breadboards back in college, so I had a rough idea of what a CPU involves: registers, ALU, flags, the whole thing.

Starting in Ruby

I haven’t touched C or C++ since college, and the YouTube guys building NES emulators are like 100 times smarter than me when it comes to emulation. So I went with Ruby, the language I use every day and know inside out. My goal was to understand how emulation works, not to fight with pointers I haven’t thought about in years.

So I started, and as soon as I had 10 instructions implemented I was hooked. There’s something about emulation that feels like you’re touching hardware without actually touching it. Plus, testing individual instructions by writing tiny 5-instruction assembly programs is a refreshing break from web dev.

After a few days I finished the CPU and moved on to the PPU (Picture Processing Unit). Once that was done (took me about 2 weeks, and that was during the holiday break), I ran Mario, hit Enter to start the game and … 🥁🥁🥁

Nothing.

It was stuck on the loading screen.

I Spent another 2 days debugging until I gave up and went to cook something. When I came back, there it was. World 1-1 had somehow loaded while I was gone and Mario was just standing there. I pressed the arrow key and…

Mario

0.5 FPS. Half a frame per second. Mario was practically a slideshow. That also explained why world 1-1 had taken about 10 minutes to load.

The Search for a Faster Language

So I started looking at alternatives:

  • Go? Cool, but pretty different from what I’m used to.
  • Rust? Same deal.
  • C? The obvious candidate, but that’s exactly what I was trying to avoid.

I kept asking ChatGPT for suggestions. At one point it mentioned Crystal, but I brushed it off because I confused it with Carbon (Google’s supposed C++ replacement, remember that?).

After asking like 700 times, even the AI got tired of throwing random languages at me and circled back to Crystal, this time really hammering the point that it was very similar to Ruby.

So I looked it up and…

Bam #1: It was made in Argentina 🇦🇷. Hold on, what?

Bam #2: The syntax was basically Ruby!

1
2
3
4
5
6
7
# This is valid Crystal AND valid Ruby
def fibonacci(n)
  return n if n <= 1
  fibonacci(n - 1) + fibonacci(n - 2)
end

puts fibonacci(35)

Bam #3: I rewrote everything I had in Crystal, ran the emulator, and it was doing 120 FPS. Mario went from slideshow to creatine. Same code, 240 times faster.

strong

I capped it at 60 FPS and it ran perfectly.

So What’s Crystal?

Crystal is a compiled, statically typed language with Ruby’s syntax. It compiles to native code through LLVM, so you get C-like speeds. Quick rundown:

Static typing with inference

You can add type annotations, but Crystal infers most of them:

1
2
3
4
5
6
7
name = "Matias"
age = 26

# But you can be explicit when you want
def greet(name : String) : String
  "Hello, #{name}!"
end

The compiler catches type errors before your code runs. No more NoMethodError at 3 AM in production.

Compiled to a single binary

No runtime, no bundle exec, no interpreter. You build it and ship it:

1
2
crystal build app.cr --release
./app  # done

C interop out of the box

You can call C libraries directly, which is great for things like SDL2 (graphics), audio libraries, or anything with a C API. I used this a lot for the emulator’s GUI and audio.

Macros instead of runtime metaprogramming

Ruby has define_method, method_missing, all those runtime tricks we love. Crystal swaps most of that for compile-time macros:

1
2
3
4
5
6
7
8
9
10
macro define_getter(name, type)
  def {name} : {type}
    @{name}
  end
end

class User
  define_getter name, String
  define_getter age, Int32
end

Different flavor, same idea. Zero runtime overhead.

What’s great

  • Zero learning curve if you know Ruby. I was productive on day one.
  • It’s fast. My emulator renders frames, processes audio, and emulates a full 6502 CPU at 60 FPS without breaking a sweat.
  • Error messages are helpful. The compiler tells you exactly what’s wrong and where.

What could be better

  • Compile times can be slow for larger projects. You miss Ruby’s instant feedback sometimes.
  • Smaller ecosystem. No Crystal equivalent for every Ruby gem. You’ll sometimes write your own stuff.
  • Windows support is still catching up. Works great on macOS and Linux though.

Wrapping up

After finishing the emulator I spent a few months writing a book about building one from scratch, so anyone can get a taste of what emulation is about and step away from the web dev grind for a bit. If I could do it, you can too. I’ll write about that in another post.

Crystal is a language I plan to keep using for low-level side projects I have coming up, and I’d genuinely recommend it to any Ruby dev who wants to do something closer to the metal, whether that’s emulation, audio, graphics, or anything where you need real performance and direct hardware access but don’t want to leave Ruby’s syntax behind.

Shoutout to the folks at Manas for creating such a cool language, and saving me from having to rewrite everything in C!

You can check out Crystal at crystal-lang.org and their getting started guide. The language reference is excellent.

This post is licensed under CC BY 4.0 by the author.