← Blog

IronRuby 4.0: Ruby 4.0 on .NET 8

IronRuby is Ruby on the .NET runtime. Microsoft started it in 2007 on top of the Dynamic Language Runtime (DLR), the same foundation as IronPython, and handed it to the community in 2010. The last release came out in 2011. It spoke Ruby 1.9.2 and needed the .NET Framework 4, and since then the project has been mostly quiet.

I have brought it back to life in my fork, largo/ironruby. It builds and runs on .NET 8 on Linux and Windows, and it parses Ruby with prism, the same parser CRuby uses, instead of the hand-ported 1.9 grammar from 2011. RUBY_VERSION is now 4.0.0.

That is why I call it IronRuby 4.0. From now on, IronRuby’s version number follows the Ruby version it implements: IronRuby 4.0 is Ruby 4.0, and it becomes 4.1 when it moves to Ruby 4.1. The last number counts IronRuby’s own releases. The first binary release, 4.0.0-preview1, is out now.

What it looks like

$ ./irb.sh
irb(main):001> RUBY_DESCRIPTION
=> "IronRuby 4.0.0-preview1 (4.0.0) on .NET 8.0.31 [x86_64-linux]"
irb(main):002> def fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2)
=> :fib
irb(main):003> (1..10).map { fib(_1) }
=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
irb(main):004> Point = Struct.new(:x, :y)
=> Point
irb(main):005> case Point.new(3, 4)
irb(main):006*   in [Integer => x, Integer => y]
irb(main):007*     Math.hypot(x, y)
irb(main):008*   end
=> 5.0

That is irb 1.16.0 with reline, the same versions CRuby 4.0 ships, with syntax highlighting, auto-indent, Tab completion and history. In CRuby, io/console is a C extension, so IronRuby implements it in C# over termios.

Modern Ruby syntax works: pattern matching, keyword arguments, endless methods, safe navigation, it and _1, and argument forwarding with ....

case config
in {db: {host: String => host, port: Integer => port}} then "#{host}:#{port}"
in {db: {socket:}} then socket
end

def connect(host:, port: 5432, **opts) = Client.new(host, port, **opts)

users&.filter_map { it.name if it.active? }

What changed

  IronRuby 1.1 (2011) IronRuby 4.0 (2026)
Runtime .NET Framework 4 .NET 8 (Linux, Windows)
Build legacy msbuild dotnet build
Parser hand-ported 1.9 grammar prism
RUBY_VERSION 1.9.2 4.0.0
Standard library Ruby 1.9 Ruby 4.0, vendored
Gems RubyGems 1.3.7 RubyGems 4.0 and Bundler
Execution DLR interpreter and IL plus a method JIT and OSR

prism as the front end

prism is a C library. IronRuby loads libprism through P/Invoke, reads prism’s binary serialization of the syntax tree with a generated C# loader, and maps it onto IronRuby’s existing AST. The rest of the compiler pipeline stays as it is. JRuby uses the same architecture for its Java loader. A Ruby script generates the 152 C# node classes from prism’s own config.yml, so upgrading prism means rebuilding the library and running the generator again.

Syntax that the 1.9-era AST had no node for is lowered into constructs it already knows. Pattern matching, for example, becomes a series of ===, deconstruct and deconstruct_keys checks with local variable bindings.

How compatible is it?

I measure against ruby/spec, the test suite that JRuby, TruffleRuby and the other implementations use. CRuby 4.0.6 is the reference: when IronRuby and a spec disagree, I check what ruby does and change IronRuby, not the spec.

suite examples failing on Linux
spec/language 2934 0
spec/command_line 175 0
spec/security 34 0
spec/library 6398 0
spec/core 23136 21, 20 of them ObjectSpace.each_object

ObjectSpace.each_object needs to walk the heap, which .NET does not offer by default. As in JRuby, it is available behind an option (-X:ObjectSpace).

On Windows, spec/language and spec/security also pass without failures. The rest is rougher there, mostly because of Unix features such as symlinks, fork and signals. There is a macOS build, but it has hardly been tested.

Gems and Rails

igem install rack and ir -S bundle install install gems from rubygems.org over real TLS. CRuby 4.0’s bundled gems, such as rake, minitest, rexml and csv, come with IronRuby. The libraries that CRuby implements in C, such as json, psych, openssl, bigdecimal, zlib and date, are written in C#. I also ported the C parts of msgpack, oj and bcrypt to C#, and nio4r and puma run.

A Rails 8.1 application boots and answers HTTP requests. ActiveSupport, ActionPack, ActionView with ERB templates, ActiveJob and ActiveRecord all load. Two things are missing for a complete Rails app, and both come from C extensions: nokogiri, and a database adapter, since sqlite3, pg, mysql2 and trilogy are all native.

In general, IronRuby cannot load C extensions, because there is no ruby.h to compile against. Many gems fall back to pure Ruby when their extension is missing, and IronRuby uses that fallback.

Performance

Two compilers specialize code on the types a program actually uses, and both are on by default:

Over 50 benchmarks, IronRuby is about 3 times slower than CRuby 4.0.6 as a geometric mean. For pure arithmetic it is faster: fib, mandelbrot, int_arith and float_arith run in 0.27 to 0.74 of CRuby’s time. Exceptions and fibers are the weak spots, because building a backtrace is expensive on .NET and each fiber is a CLR thread.

.NET from Ruby, Ruby from .NET

This is what IronRuby was made for. Ruby code can use .NET types directly:

date = System::DateTime.new(2000, 1, 1)
System::Environment.OSVersion.Platform

In the other direction, a .NET application can embed IronRuby and run Ruby scripts, for example as a plugin or scripting language.

irubyc: packaging applications

As the maintainer of OCRAN, I care about shipping Ruby programs to people who don’t have Ruby installed. irubyc, named after JRuby’s jrubyc, turns a Ruby program into a .NET application:

$ ./irubyc.sh --single-file --self-contained -t out app.rb lib/

The Ruby files are embedded in the assembly as resources, together with IronRuby and the standard library. --single-file produces one executable, and --self-contained bundles the .NET runtime too, so the program runs on a machine without .NET. With --library, irubyc produces a single DLL that a .NET host application can load as a plugin, with IronRuby inside.

irubyc does not compile Ruby to IL yet: the embedded sources are parsed at startup. A prototype for real ahead-of-time compilation, Ruby to IL to a NativeAOT executable, is in Util/aot.

Trying it

The first preview release, IronRuby 4.0.0-preview1, has downloads for Linux x64, Windows x64 and macOS on Apple silicon. They bring their own .NET runtime, so nothing else needs to be installed:

$ tar xzf ironruby-4.0.0-preview1-linux-x64.tar.gz
$ cd ironruby-4.0.0-preview1-linux-x64
$ ./irb.sh
$ ./ir.sh script.rb
$ ./igem.sh install rack

On Windows, unzip the archive and use irb.cmd, ir.cmd and igem.cmd. The macOS build starts, but I have hardly tested it yet. irubyc works from the download too; to compile an application it needs the .NET SDK.

If you would rather build it yourself, the README explains how. You need the .NET SDK, Ruby and a C compiler for prism.

IronRuby 4.0 is still a preview. But it runs today’s Ruby on today’s .NET, and the gap to CRuby gets smaller with every round of ruby/spec. If you try it and something breaks, please open an issue or write to me.