skip to content

Engine Variants

CRuby runs YARV bytecode, JRuby runs on the JVM without a GVL, and TruffleRuby is a third engine. Interviewers check which engine your answers assume and how code detects it.

on this pageshow

explore

questions

4

In Ruby, what are CRuby (MRI), JRuby and TruffleRuby, and how does CRuby's YARV virtual machine run your source code?

level: juniorimportance: must knowfreq 45%

answer

  1. reference engine written in C
  2. parse, compile, then interpret
  3. RubyVM::InstructionSequence#disasm
  4. JRuby on the JVM
  5. TruffleRuby on GraalVM

basics

~20 s

CRuby, also called MRI, is the reference Ruby written in C: it parses source with Prism, compiles it to YARV bytecode and interprets that bytecode. JRuby runs Ruby on the JVM; TruffleRuby runs it on GraalVM.

solid answer

~40 s

`CRuby` (Matz's Ruby Interpreter, `MRI`) is the reference implementation, written in C, and it is what the `ruby` command usually is. It does not walk the syntax tree: in Ruby 4.0 the Prism parser builds a tree, CRuby's compiler turns it into **YARV** instruction sequences (bytecode), and the stack-based YARV VM executes them. `RubyVM::InstructionSequence.compile("1 + 2").disasm` shows that bytecode. **JRuby** implements the same language on the JVM, so it uses the JVM's JIT and garbage collectors and can call Java classes, but it cannot load CRuby C extensions. **TruffleRuby** runs on GraalVM and compiles hot Ruby code with the Graal compiler. All three report themselves through `RUBY_ENGINE`: `"ruby"`, `"jruby"`, `"truffleruby"`.

code

ruby · 8 lines
ruby
iseq = RubyVM::InstructionSequence.compile("1 + 2")
puts iseq.disasm
# putobject 1
# putobject 2
# opt_plus
# leave

puts RUBY_ENGINE # => "ruby" on CRuby, "jruby" on JRuby

go deeper

for a junior

Recall that CRuby and MRI are the same C engine, and that JRuby runs on the JVM and TruffleRuby on GraalVM. Say RUBY_ENGINE reports which one you are on.

for a middle

Explain the pipeline: Prism parses, the compiler emits YARV instruction sequences, the stack VM runs them, and a JIT is an optional extra layer. Show RubyVM::InstructionSequence#disasm output.

for a senior

Tie engine differences to production: native gems that JRuby cannot load, JVM warm-up for short jobs, and MRI-only APIs such as RubyVM that break on other engines.

for a principal

Frame the engine as a platform choice: CRuby is the default ecosystem target, while JRuby or TruffleRuby buy JVM tooling or peak speed at the cost of gem compatibility and operating a second runtime.

## One language, several engines Ruby is a language with more than one implementation, or **engine**. The one on almost every laptop, CI runner and server is **CRuby**, also called **MRI** (Matz's Ruby Interpreter, after Ruby's creator Yukihiro Matsumoto). It is written in C, new language features land in it first, and each Ruby release such as 4.0.7 is a CRuby release. Two other engines are production-grade: - **JRuby** - an implementation of Ruby on the **JVM** (Java Virtual Machine). - **TruffleRuby** - an implementation on **GraalVM**, a language toolkit built on the JVM. They agree on behaviour largely because of **ruby/spec**, an executable test suite for the Ruby language. CRuby keeps a copy under `spec/ruby`, and JRuby and TruffleRuby run it too, so "what does `Array#flatten` return" has one answer across engines. Each engine implements a particular Ruby language version on its own schedule, so a brand-new CRuby feature may not exist on the others yet. ## How CRuby runs a file CRuby does not interpret your text line by line, and it has not walked the syntax tree for a long time. Loading a file goes through three steps: 1. **Parse.** The **Prism** parser (the default since Ruby 3.4; `--parser=parse.y` selects the older one) reads the source and builds an abstract syntax tree. 2. **Compile.** CRuby's compiler turns that tree into **instruction sequences**, one per file, class body, method and block. These sequences are **YARV bytecode** (YARV stands for "Yet Another Ruby VM"). 3. **Interpret.** The **YARV virtual machine**, a stack-based VM, executes the instructions: `putobject` pushes a value, `opt_plus` adds the top two, `leave` returns. Two things are easy to miss: - The bytecode lives in memory. CRuby compiles each file when it is loaded and does not write a cache to disk on its own; `RubyVM::InstructionSequence#to_binary` exists for tools that want to store it. - A JIT is an extra, optional layer on top of YARV. CRuby 4.0 ships YJIT and the experimental ZJIT, both disabled by default; they compile hot methods to machine code and fall back to the interpreter. You can look at the bytecode yourself with `RubyVM::InstructionSequence`. The whole `RubyVM` namespace is **MRI-specific**: its own documentation says it is not defined on JRuby or TruffleRuby and is meant for debugging and research, not application code. ## JRuby and TruffleRuby in one paragraph each **JRuby** has its own interpreter and compiles hot Ruby code to JVM bytecode, which the JVM's JIT then optimises. It uses the JVM's garbage collectors, can call Java libraries directly, and runs Ruby threads on JVM threads that execute in parallel. The price: it **cannot load CRuby C extensions**, so a gem with native code needs a `java`-platform build or a pure-Ruby alternative, and JVM start-up and warm-up make short scripts slow. **TruffleRuby** is built with the Truffle framework and compiled by GraalVM's Graal compiler, and it aims for high peak speed once code is warm. It installs `ruby`-platform gems and builds their C extensions itself, which makes it closer to CRuby for native gems than JRuby is, at the cost of longer warm-up. ## Side by side | | CRuby (MRI) | JRuby | TruffleRuby | |---|---|---|---| | Written in / hosted on | C, native binary | Java, on the JVM | Java, on GraalVM | | Executes | YARV bytecode, optional YJIT/ZJIT | JVM bytecode via the JVM JIT | Truffle interpreter plus Graal JIT | | `RUBY_ENGINE` | `"ruby"` | `"jruby"` | `"truffleruby"` | | CRuby C extensions | yes | no, `java` platform gems | yes, built from source | | `RubyVM` defined | yes | no | no | ## Seeing the pipeline for yourself The disassembly of `1 + 2` is short enough to read line by line. `putobject 1` and `putobject 2` push the two operands onto the VM's stack, `opt_plus` pops both and pushes their sum, and `leave` returns the value on top of the stack. The `opt_` prefix marks a **specialised instruction**: when both operands are Integers and nobody has redefined `Integer#+`, the VM adds them directly instead of performing a full method call, and it falls back to an ordinary call otherwise. That fast path is one reason redefining core operators is discouraged. Methods, blocks and class bodies each get their own instruction sequence, and `RubyVM::InstructionSequence.of(method(:name))` returns the one behind an existing Ruby method. None of this is portable: it is a window into CRuby's internals, useful for curiosity and performance investigations, and the exact instructions change between releases. ## What interviewers listen for - That "Ruby" in production nearly always means CRuby, and that it is a **bytecode VM**, not a tree-walking interpreter. - That engine differences show up at the edges: native gems, threading, start-up time and MRI-only APIs such as `RubyVM`, not in everyday syntax. - That you can name how code finds out which engine it is on (`RUBY_ENGINE`) instead of guessing.

  • Does CRuby save the compiled YARV bytecode between runs?
    Not by itself. CRuby compiles each file to instruction sequences when it is loaded and keeps them in memory for the life of the process. `RubyVM::InstructionSequence#to_binary` and `.load_from_binary` let a tool serialise and reload them, which boot-time caching tools build on, but plain `ruby app.rb` recompiles every time.
  • Why can the same Gemfile install on CRuby but fail on JRuby?
    Gems that ship a C extension are built for CRuby's C API, and JRuby does not load C extensions. Such a gem needs a `java`-platform variant or a pure-Ruby replacement on JRuby. TruffleRuby, by contrast, installs the `ruby`-platform gem and compiles its extension.
  • How do the engines stay compatible if CRuby is the reference?
    Through ruby/spec, an executable specification of Ruby's behaviour written as examples. CRuby vendors it under `spec/ruby`, and JRuby and TruffleRuby run it too, so core methods and syntax behave the same. Each engine still targets a specific Ruby language version, reported by its `RUBY_VERSION`.

The Ruby language is a recipe and the engines are three kitchens: CRuby cooks on a C stove, JRuby and TruffleRuby in JVM-based kitchens. A gem with a CRuby C extension is a pan made for the C stove; the JRuby kitchen has no burner it fits.

saying these in an interview costs you the question

  • CRuby interprets the syntax tree directly, with no bytecode step
  • JRuby and TruffleRuby run YARV bytecode inside the JVM
  • RubyVM::InstructionSequence works the same on every Ruby engine
  • JRuby can load any gem's CRuby C extension
  • MRI and CRuby are two different Ruby engines
open as a page

In Ruby, how do RUBY_ENGINE, RUBY_ENGINE_VERSION and RUBY_PLATFORM tell code which engine and platform it runs on?

level: middleimportance: should knowfreq 35%

basics

~10 s

RUBY_ENGINE names the engine ("ruby" for CRuby, "jruby", "truffleruby"), RUBY_ENGINE_VERSION is that engine's own release, and RUBY_PLATFORM is the build's CPU-OS string, which is just "java" on JRuby.

open as a page

A CPU-heavy Ruby worker that renders PDFs keeps one core busy on CRuby with eight threads; when would you move it to JRuby or TruffleRuby, and what breaks?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Move a long-running CPU-bound worker to JRuby or TruffleRuby when one process must use many cores, since their threads run Ruby in parallel; expect broken native gems (JRuby), no fork, slower start-up, warm-up and higher memory.

open as a page

In Ruby 3.4 and 4.0, what changed when Prism became CRuby's default parser, and when would you run ruby --parser=parse.y?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Since Ruby 3.4, CRuby parses code with Prism instead of parse.y; programs should behave the same. Run ruby --parser=parse.y to rule out a Prism bug or to keep MRI-only AST tooling such as RubyVM::AbstractSyntaxTree.of working.

open as a page