A fork of IronRuby — Ruby on the .NET CLR — brought back to life: it builds and runs on .NET 8 and 10, and it parses Ruby with prism, CRuby's own parser, instead of the hand-ported Ruby 1.9 grammar it shipped with in 2011.
IronRuby 4.0 is the first stable release since 1.1.3 in 2011, and 4.0.3 is its latest. The downloads are self-contained: unpack one and run it, there is no .NET to install.
The compatibility target is Ruby 4.0. IronRuby aims to run what CRuby 4.0 runs, with
the same results: the language, the core classes and the standard library are checked
against ruby/spec and against CRuby 4.0 itself, and where IronRuby's behaviour
differs from CRuby 4.0's, that is a bug in IronRuby. Ruby 3.x behaviour that 4.0 changed
(Set as a core class, Hash#inspect, frozen string literals, the error messages) is
not kept.
| Download | ||
|---|---|---|
| Windows x64 | ironruby-4.0.3-win-x64-setup.exe | the installer: the zip below, installed and put on the PATH |
| Windows x64 | ironruby-4.0.3-win-x64.zip | with Windows Forms, WPF and the Notepad sample |
| Linux x64 | ironruby-4.0.3-linux-x64.tar.gz | |
| macOS arm64 | ironruby-4.0.3-osx-arm64.tar.gz | built in CI, not tested there |
SHA256SUMS · release notes · all releases · getting started
▶ Watch the whole launch video (55 s, MP4)
- Ruby 4.0, parsed by prism: pattern matching, endless methods,
it, keyword arguments,...forwarding — the syntax CRuby 4.0 accepts, read by CRuby's own parser. - ruby/spec at CRuby 4.0.6:
spec/language(2,934 examples) andspec/library(6,398) pass with 0 failures;spec/corehas 21 of 23,136, 20 of themObjectSpace.each_object. - A method JIT and OSR, on by default: integer and float loops,
fibandmandelbrotrun in 0.27–0.74× CRuby's time. Over all 50 benchmarks the geomean is 3.25×; calls, strings and arrays are where the work is. - irb 1.18 + reline, RubyGems 4.0.16 and Bundler over real TLS, and Ruby 4.0's standard library and bundled gems.
- Rails 8.1 boots and serves requests through Rack (with nokogiri stubbed).
- .NET is right there:
require "System.Windows.Forms", and Windows Forms and WPF load from the Windows download.irubycturns a Ruby program into one executable, or into a plugin DLL for a .NET host.
Samples/Notepad is Windows Notepad in 1,365 lines of Ruby:
encodings and line endings that survive a save byte for byte, find and replace, go to,
zoom, printing, the status bar. Here it edits its own source. It ships in the Windows
download (Samples\Notepad\notepad.cmd), and its test
drives it in CI with nobody at the keyboard.
$ ./iirb.sh
irb(main):001> RUBY_DESCRIPTION
=> "IronRuby 4.0.3 (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> require "io/console"; IO.console.winsize
=> [44, 80]
irb(main):005> Point = Struct.new(:x, :y)
=> Point
irb(main):006> case Point.new(3, 4)
irb(main):007* in [Integer => x, Integer => y]
irb(main):008* Math.hypot(x, y)
irb(main):009* end
=> 5.0
irb(main):010> 1 / 0
(irb):10:in 'Integer#/': Attempted to divide by zero. (ZeroDivisionError)
from (irb):10:in '<main>'That is irb 1.18.0 and reline 0.6.3 - reline as CRuby 4.0 ships it, irb the newer release
with its startup banner - with syntax highlighting, auto-indent, Tab completion, history and
ls/show_source, running on
IronRuby. io/console is a C extension in CRuby, so IronRuby implements it over termios.
Scripts run the same way, and gems install with real TLS:
$ ./ironruby script.rb # prism front end, vendored stdlib, JIT and OSR on
$ ./igem.sh install rack # or: ./ironruby -S gem install rack
$ ./ironruby -S bundle installThe interpreter is ironruby (ironruby.cmd on Windows), as JRuby's is jruby: it is
RbConfig's ruby_install_name, so the binstubs Bundler and Rails write start
#!/usr/bin/env ironruby - a #! line with "ruby" in it, which Ruby runs as Ruby. (Those
RubyGems writes for an installed gem name the launcher by its path instead, #!<root>/ironruby,
so they only work where that path still does.) ir.sh and ir.cmd are its short name, the
one IronRuby has used since 1.x, and run the same thing.
igem / iirb / irubyc keep the i-prefix so they can sit on PATH beside CRuby's gem
and irb, as JRuby's jgem, jirb and jrubyc do; IronRuby has no launcher named irb or
gem, so those stay CRuby's. The unprefixed gem, irb, bundle, bundler and irubyc
live in Src/StdLib/bin, which is not on the PATH, and are what ironruby -S <name> finds.
On Windows the build also has ironrubyw.exe next to ir.exe, as CRuby has rubyw.exe and
JRuby jrubyw.exe: the same interpreter as a Windows GUI program, which gets no console
window - for Windows Forms and WPF programs; Samples\Notepad\notepad.cmd starts it.
# all of this runs today
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? }The interop IronRuby always had still works: CLR namespaces are modules, List[String]
instantiates a generic, Method#of a generic method, include ILivingCreature implements an
interface, a Ruby subclass overrides virtual members, out parameters come back with the result.
On top of that, .NET APIs now take the Ruby you would write today:
System::Console.WriteLine "hi" # platform assemblies load on demand, no load_assembly
list = System::Collections::Generic::List[Integer].new([3, 1, 4]) # an Array for IEnumerable<int>
list.Sort { |a, b| b <=> a } # a block goes to the trailing delegate parameter
list.ForEach { |x| break if x < 3 } # break ends the CLR call, as it does a Ruby one
using System::Linq # extension methods for the rest of the file, as C#'s `using`
list.where { it > 1 }.order_by { -it }.to_a # => [4, 3]
list.select { it * 2 }.to_a # => [8, 6, 2], LINQ's Select ahead of Enumerable#select
[3, 1].select { it > 1 } # => [3], an Array keeps Array#select and gets no LINQ
Widget.new(title: "Hi", width: 300) # keywords bind parameters by name, or set properties
Greeter.greet("Ann", greeting: "Hello", times: 2)
func = Funcs.Doubler # a Func<int, int>
[1, 2].map(&func) # delegates are callable like lambdas: call, to_proc, >>, curry
ok, n = System::Int32.TryParse("42")
case Point.new(1, 2) # a C# record
in {x: 1, y:} then y # members by their Ruby or CLR name
end
min, max = Stats.MinMax(values) # a ValueTuple destructures like an Array
System::Threading::Tasks::Task.Run { 6 * 7 }.value # => 42
System::Runtime::CompilerServices::RuntimeHelpers.GetSubArray(array, 1..-2) # a Ruby range for System.Range
class Doctor < Human # a CLR class without a parameterless constructor
def initialize(name)
super("Dr. #{name}") # super runs the CLR base constructor
end
end
System::Math::PI # const and static readonly fields are constants
command.Parameters.AddWithValue("$name", "Ann") # a Ruby String for an Object parameter is a System.Stringusing a CLR namespace works like C#'s using directive and like a refinement: its extension
methods are visible from that point to the end of the file, class body or eval string, and
nowhere else. using System::Linq::Enumerable brings in one static class's (C#'s using static).
Inside it, calls resolve as they did under the older using_clr_extensions, which activates a
namespace for the whole runtime and is deprecated: an extension method of a CLR type comes ahead
of the Ruby methods the type picks up from Enumerable (list.select is LINQ's), a CLR method
group gets them as more overloads (list.count(pred), the instance member winning an exact tie),
and a Ruby object such as an Array, whose ancestors don't include IEnumerable[T], gets none.
Moving from one to the other means changing that one line.
Also: a Method converts to a delegate, IComparable<T> types are Comparable,
IAsyncEnumerable<T> is Enumerable, C# 8 default interface methods are callable and run for
Ruby implementations, include System::IComparable works although the name is also a generic,
and Method#parameters names a CLR method's parameters. See
Src/IronRuby.Tests/Runtime/ClrBinderTests.cs,
ClrProtocolTests.cs and
ClrRuntimeInteropTests.cs for the
exact behaviour.
IronRuby's version 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 for that Ruby version (4.0.0, 4.0.1, ...) and is not CRuby's patch level; the release notes say which CRuby release the specs were checked against. The last release of the original project was 1.1.3, for Ruby 1.9.2, in 2011.
IRONRUBY_VERSION is the full version (4.0.3, or 4.0.0-preview1 for a pre-release),
RUBY_ENGINE_VERSION the same without the pre-release suffix, and RUBY_VERSION the Ruby
version (4.0.0).
| before | after | |
|---|---|---|
| Runtime | .NET Framework 4 | .NET 8 (Linux, macOS, Windows) |
| Build | legacy msbuild, 12 configurations | SDK-style dotnet build |
| Parser | hand-ported 1.9 grammar (~13k lines) | prism — the parser CRuby uses |
RUBY_VERSION |
1.9.2 |
4.0.0 |
| Big integers | Microsoft.Scripting.Math |
System.Numerics |
| Integer | Int32, then BigInteger | Int32 → Int64 → BigInteger |
| Execution | DLR interpreter, then IL | + a method JIT and OSR that specialize on observed types |
| REPL | irb from 2011 | irb 1.18.0 + reline, on a termios io/console |
| Gems | RubyGems 1.3.7 (2010) | RubyGems 4.0.16 + Bundler, installing over real TLS; gem install rails resolves without compiling C |
Ruby source is parsed by libprism.so through P/Invoke, decoded from prism's binary
serialization by a generated C# loader, and mapped onto IronRuby's existing AST — so the
whole AstGenerator/DLR compilation pipeline works unchanged. It is the only parser: the
hand-ported 1.9 grammar and its lexer have been removed. This is the same architecture
JRuby uses for its Java loader:
source ──▶ libprism ──▶ binary AST ──▶ PrismLoader ──▶ PrismAstBridge ──▶ IronRuby AST ──▶ DLR
(pm_serialize_parse) (generated) (lowering)
Src/Prism/generate.rb reads prism's config.yml and emits 152 typed node classes plus the
deserializer, so upgrading prism is: rebuild the native library, re-run the generator, fix
what the compiler flags. The loader version-checks the serialization format at runtime.
Modern syntax with no equivalent in the 1.9-era AST is lowered rather than rejected:
- pattern matching →
===/deconstruct/deconstruct_keystests with capture bindings (guards, find patterns,**nil, pins, alternations,=>andin) - keyword arguments → trailing optional hash + a prologue (missing-keyword
ArgumentError, defaults,**rest) - safe navigation → nil-guarded temporary
it/_1→ explicit block parameters,...→*rest, &blockforwarding
See Src/Prism/README.md for the details and the known gaps.
Measured with ruby/spec at CRuby 4.0.6, Util/parallel-sweep.sh:
| suite | examples | failing |
|---|---|---|
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, which needs -X:ObjectSpace |
| IronRuby's own C# test suite | ~1470 | 19 known |
CRuby 4.0.9 is the oracle: where a spec and IronRuby disagree, the behaviour is checked
against ruby and IronRuby is changed, not the spec. The aim is Ruby 4.0 compatibility
rather than compatibility with any earlier Ruby: a difference from CRuby 4.0 that a
program can observe is a bug worth a report, specs or not.
The same tree runs on Windows, where spec/language and spec/security are also at
zero; see Windows for how to build it and what is still missing there.
The Ruby 4.0 standard library is vendored in Src/StdLib/ruby/4.0; Src/StdLib/ironruby
holds what CRuby implements as C extensions, written in C# (Src/Libraries) or Ruby, and is
the only other library directory on the load path. A compatibility prelude
(Src/StdLib/ironruby/ruby4.rb) supplies what MRI provides
natively — Process.clock_gettime, Random, ObjectSpace::WeakMap, ruby2_keywords,
pattern-matching support classes and core methods from Ruby 2.x-4.x; Rational and Complex
are Ruby ports of CRuby 4.0's rational.c and complex.c, and Pathname is CRuby 4.0's own
pathname_builtin.rb. Ripper is implemented
on prism, the way CRuby 4.0 implements it, and io/console over termios, so the current
irb and reline run unmodified.
What is left is mostly what .NET cannot do: fork, a controlling TTY, setproctitle,
C-extension APIs (fiddle, mkmf compiling), and heap walking (ObjectSpace.each_object
is opt-in behind -X:ObjectSpace, as JRuby's is behind -X+O).
The libraries IronRuby implements in C# - bigdecimal, json, psych, openssl,
zlib, stringio, strscan, digest, date, etc, fcntl, io/console, prism -
are advertised to RubyGems as default gems, with gemspecs in
Src/StdLib/ruby/gems/4.0.0/specifications/default generated by
Util/gen-default-gemspecs.rb. Without them the resolver
does not know the library is already there: gem install activesupport used to download
bigdecimal's C sources and try to compile them. This is what JRuby does for the same
reason. The versions in the table are checked against the running interpreter's own
VERSION constants when the gemspecs are generated, so they cannot drift.
CRuby 4.0's pure-Ruby bundled gems - rake, rexml, minitest, test-unit, csv,
logger, matrix, net-imap/-smtp/-pop/-ftp, drb, racc, ostruct and the rest -
ship as real installed gems in IronRuby's own gem home, Src/StdLib/ruby/gems/4.0.0, at the
versions CRuby 4.0.9 bundles, so require "rexml/document" and ir -S rake work the same on
every machine, with or without a CRuby next to IronRuby.
Util/install-bundled-gems.rb installs them from a CRuby
source tarball's gems/*.gem and lists the ones left out and why (the C extensions IronRuby
implements itself, typeprof and debug). Two come at another version than CRuby's, from
rubygems.org and pinned by SHA-256: rbs 4.0.0.dev.5, the release IronRuby's rbs_extension
is built from (below), and repl_type_completor 0.1.16, since releases before 0.1.14 refuse rbs 4 -
so irb completes with its TypeCompletor, as on CRuby. IronRuby never uses the gems of a CRuby
installed on the same machine: a gem it needs is installed for IronRuby, with igem install
or Bundler.
A C extension is never built: there is no ruby.h to compile against. Such a gem is left alone -
its .rb files load, a real require of the .so raises LoadError, and the pure-Ruby
fallback most of them carry is used instead.
rbs (Ruby's type signatures, which ruby-lsp, steep and rdoc 8 load) has no such fallback,
so IronRuby provides its extension, rbs_extension: the installed gem's own Ruby loads on top
of the rbs C parser itself - the gem's src/, built unmodified into librbs by
Util/build-librbs.sh - with the glue that builds the Ruby objects in
C# (Src/Libraries/Rbs). The parser is tied to one rbs release, currently 4.0.0.dev.5, and
require "rbs" under any other version raises a LoadError saying so.
A Rails 8.1 application boots and serves HTTP requests through Rack:
RAILS GET /hello -> 200 "Hello world from Rails 8.1.3.1 on ironruby 4.0.0"
RAILS GET /hello/Iron -> 200 "Hello IronRuby from Rails 8.1.3.1 on ironruby 4.0.0"
ActiveSupport, ActiveModel, ActionDispatch, ActionController, ActionView (ERB templates through the real resolver), ActiveJob, ActiveRecord and railties all install and load. Two things stop a complete Rails app, and neither is a Ruby-level bug:
- nokogiri is a C extension (libxml2 + gumbo) and a hard dependency of
rails-html-sanitizer->loofah->actionview, and ofrails-dom-testing->actionpack. Everything above was measured with a load-only stub in its place; the sanitize helpers, the dom assertions and the HTML error page need a real implementation. - ActiveRecord has no adapter:
sqlite3,pg,mysql2andtrilogyare all native.
Two compilers specialize on the types a program actually uses. Both are on by default
(-X:NoJIT, -X:NoOSR turn them off), and both fall back to the generic path rather than
guess:
- Method JIT — a hot method body is replaced by a copy with unboxed
int/long/doublelocals, CLR arithmetic, no per-call scope, and direct calls for self-recursion. One integer compare of a global method-table version guards redefinition, override, singleton methods,prepend,includeandalias; refinements switch it off; overflow and division by zero deopt to the generic body. - OSR — a loop that has taken enough back edges is replaced while it runs by a type-specialized copy over the same locals tuple, so there is no state to materialize. This is what reaches a hot loop inside a block that is only ever entered once, which no invocation-counting JIT can see. If the types change, the copy is rebuilt.
Util/bench/run.sh (50 benchmarks, Release, ratio to CRuby 4.0.6 — lower is better):
| ratio | |
|---|---|
float_arith, cmp_branch, int_arith, mandelbrot, fib, while_loop |
0.27 – 0.74 (faster than CRuby) |
| calls, ivars, string and array work | 2 – 4 |
| geomean over all 50 | 3.25 |
raise_rescue, fiber_switch |
150+ (backtrace construction; a CLR thread per Fiber) |
Build with -c Release and run with IR_CONFIG=Release ./ir.sh: the optimized build is
~1.8x faster than the default Debug build across the whole suite. See
Util/bench/README.md.
IRONRUBY_STARTUP_TRACE=1 prints, when the process exits, where its startup went: milestones
since the process started, and for the Ruby files it loaded the time spent parsing them
(prism), transforming them to the DLR tree, compiling them for the interpreter and running
their own code, slowest files first.
irubyc is IronRuby's answer to JRuby's jrubyc: it turns a Ruby program into a .NET
application that runs without the source tree.
$ ./irubyc.sh -t /tmp/out app.rb lib/
irubyc: wrote /tmp/out/app
irubyc: run it with /tmp/out/app/app
$ /tmp/out/app/app --some --argsIt generates a C# host, compiles it with the .NET SDK, and embeds every .rb file in the
resulting assembly as a managed resource; a PlatformAdaptationLayer overlay serves
those resources to the runtime, so the main script, require and load read them out of
the assembly. No .rb file is written next to the executable. The output directory also
gets IronRuby's assemblies, libprism and a copy of the standard library (including
whatever igem has installed), so require "json" and require "some_gem" work.
Be clear about what it does not do: irubyc does not emit IL for the Ruby code. The
embedded sources are still parsed and compiled by IronRuby at startup, exactly as
ir app.rb would, so a compiled application starts no faster than the same script run
from a source tree — what you get is a single deployable directory, not AOT-compiled Ruby.
Real AOT exists as a prototype in Util/aot: it ports .NET
Framework's expression-tree compiler onto PersistedAssemblyBuilder (.NET 10), so Ruby
compiles to a saved assembly that runs with no parsing at all, and it builds IronRuby under
NativeAOT. It is not wired into irubyc yet, and has not been run against ruby/spec.
Options, with jrubyc's names kept where they mean the same thing:
-t, --target DIR |
where to write the application directory (default .) |
-d, --dir BASEDIR |
base directory the embedded paths are relative to |
-p, --prefix PREFIX |
prefix prepended to every embedded path |
--main FILE |
which script to run (default: the first file given) |
-o, --output NAME |
name of the application |
--exe / --dll |
native launcher (default), or just <name>.dll for dotnet <name>.dll |
--self-contained [RID] |
bundle the .NET runtime too — runs with no .NET installed |
--single-file [RID] |
produce one executable file instead of a directory (combine with --self-contained) |
--library |
produce one class library (<name>.dll) that a .NET host loads as a plugin |
--cs FILE |
C# source compiled into the --library (repeatable) |
--reference DLL |
an assembly the host provides: compiled against, not bundled (repeatable) |
--framework TFM |
target framework (default: the one the running IronRuby was built for) |
--no-stdlib |
leave out the vendored Ruby 4.0 tree and the gems (30 MB → 19 MB) |
--keep, --verbose |
keep the generated C# project / narrate every step |
$ ./irubyc.sh --single-file -t /tmp/out app.rb lib/
$ ls /tmp/out/app
app
$ ./irubyc.sh --single-file --self-contained -t /tmp/out app.rb lib/ # no .NET needed to run itThis uses the .NET SDK's own single-file bundler. Everything the directory layout has next
to the assembly — IronRuby's DLLs, the native libprism and sqlite libraries, and the
standard library — goes into the one file, and the first run extracts it to a per-app cache
(~/.net/<app>/…, or $DOTNET_BUNDLE_EXTRACT_BASE_DIR). Later runs reuse the cache. Because
the extracted layout is exactly the directory layout, nothing in IronRuby needs to know it
was bundled: require, load_assembly, eval and native libraries all behave the same.
On Linux, for the demo above: 32 MB framework-dependent, 99 MB self-contained. The
file is for the platform it was built on — the bundle carries that machine's native
libraries — so irubyc refuses a RID for another OS or CPU instead of producing a file that
cannot parse Ruby on its target.
Sizes, for a three-file demo app on Linux: 30 MB framework-dependent (18 MB of that is
the standard library, 12 MB IronRuby, the DLR and libprism), 101 MB with
--self-contained, which then runs on a box with no .NET at all. irubyc itself needs the
.NET SDK; the application it produces does not. It links whichever build of IronRuby is
running it, so IR_CONFIG=Release ./irubyc.sh … produces a Release-linked application.
Startup, best of seven on an idle Linux box, Debug build: ./ir.sh -e '' 1.58 s, the demo
app from its source tree 1.96 s, the same app compiled 1.77 s — the small difference is the
shell wrapper and reading the sources from memory rather than the disk, not compiled Ruby.
The embedded tree behaves like a read-only directory at <app>/src: require,
require_relative, load, File.read, Dir.glob and Dir.entries all see it, because
those go through the DLR's PlatformAdaptationLayer, which is what the overlay replaces.
What does not: File.exist? and the rest of FileTest answer false for an embedded
file, because they stat the real file system directly rather than ask the platform layer;
__FILE__ and $0 name a path that has no directory behind it; and anything that shells
out to ruby or re-execs $0 will not find an interpreter.
$ ./irubyc.sh --library -o Plugin -t /tmp/out --cs Plugin.cs \
--reference Host.Api.dll plugin.rb
irubyc: wrote /tmp/out/Plugin/Plugin.dllFor a host that loads plugins — typically one that Assembly.LoadFiles every DLL in a
plugin directory — --library makes one
class library carrying IronRuby, the DLR, libprism and sqlite, the standard library and
the Ruby files. .NET has no single-file mode for a library, so it unpacks itself: IronRuby,
the natives and Lib/ are one zip embedded as a resource, and a generated bootstrap extracts
it on first use and resolves IronRuby from there. The --cs files are compiled into the
DLL; they reach IronRuby through a generated IronRuby.Embedded.RubyHost:
var engine = RubyHost.CreateEngine(); // stdlib, prelude and RubyGems booted, prism parser
var scope = engine.CreateScope();
scope.SetVariable("plugin", this);
dynamic ruby = RubyHost.Run(engine, "plugin.rb", scope); // an embedded file; returns its last valueCreateEngine makes the DLL's own assembly and every --reference visible to Ruby, so
Host::Api::SomeType resolves; CreateEngine(runtime => ..., language => ...)
adjusts the setups (runtime.PrivateBinding = true lets Ruby call protected CLR members, which
IronRuby otherwise allows only from a Ruby subclass). RubyHost.RunMain runs the main file,
RubyHost.ExtractDirectory says where the payload went.
- First use extracts. IronRuby needs a real
Assembly.Location(forLib/and RbConfig) and the natives have to be files to be loaded at all, so nothing runs from memory. The payload goes to$IRUBYC_EXTRACT_BASE_DIR/<name>-<hash>/, by default<temp>/irubyc-<user>/(created0700; a directory others can write to is refused).<hash>is the payload's SHA-256, so a rebuilt DLL never reuses a stale copy, and the build is byte-for-byte reproducible. Each process extracts into a private temporary directory and renames it into place, so concurrent first loads are safe — the losers use the winner's copy — and a copy with files missing is moved aside and extracted again. For a small demo plugin: about 250 ms to extract, 33 MB on disk. - Type-load order. A plugin host calls
GetTypes()on the DLL before any of its code runs, and IronRuby is not resolvable until the bootstrap has installed its resolvers. The resolvers are installed by a[ModuleInitializer], which runs before the first method of the DLL (the host's constructor call), and everything that derives from IronRuby types lives in a second, generated assembly inside the payload. So the--cscode must not derive from or implement IronRuby/DLR types, nor hold them in struct fields; fields, locals, parameters and return types of IronRuby types are fine — the CLR resolves those only when a method using them is compiled. - Load contexts. IronRuby is loaded into the plugin's own
AssemblyLoadContext(Assembly.LoadFilegives every plugin one); that context'sResolvingevent is the hook that fires,AppDomain.AssemblyResolveis only a fallback. Two IronRuby plugins in one process each get their own IronRuby, and neither sees the other's. - Platform. Like
--single-file, the DLL carries the build machine's native libraries. The DLL itself would load anywhere, so it checks at run time and fails with a message naming the platform it was built for. Build on Windows for Windows.
For a small demo plugin:
9.3 MB with the standard library, 5.4 MB with --no-stdlib. The first run of the activity in a
process, extraction and booting the engine included, takes about 2.4 s (1.2 s with --no-stdlib,
which skips RubyGems); later runs of the activity take a few milliseconds.
Linux is the primary target and the one the numbers above are measured on. Windows works:
ir.cmd and iirb.cmd run, and spec/language (2920 examples) and spec/security pass with
0 failures. spec/core and spec/library are rougher there — symlinks/chmod/umask in
core/file, fork and signals in core/process, and IO.select/IO#wait on pipes (which is
poll(2), so core/io and the socket suites hang rather than fail). Win32OLE and
readline are not implemented on Windows; IO.console is nil, so irb falls back to its ANSI
input path. Windows Forms and WPF (require "System.Windows.Forms", "PresentationFramework")
come from the Windows Desktop runtime, which ir references whenever it is built for Windows
(-p:IronRubyWindowsDesktop=false leaves it out); on Linux and macOS they do not exist.
Samples/Notepad is Windows Notepad written that way, in Ruby
(Samples\Notepad\notepad.cmd); its test drives it with nobody at the keyboard and runs in CI.
macOS (arm64) is built by the release workflow but not tested in CI; pipes to child
processes, socket addresses and Puma were fixed there in 4.0.1 (#12, #13).
Releases carry a ready-to-run IronRuby for Linux x64, Windows x64 and macOS arm64 (not tested in CI); the table at the top links the ones for 4.0.3. They are self-contained, so no .NET has to be installed. Unpack one and run the launchers in it:
$ tar xzf ironruby-4.0.3-linux-x64.tar.gz && cd ironruby-4.0.3-linux-x64
$ ./ironruby -e 'puts RUBY_DESCRIPTION'
$ ./iirb.sh
$ ./igem.sh install rackFrom 4.0.2 on, the archives have ironruby and ironruby.cmd (see above), and ir.sh /
ir.cmd keep working; 4.0.1 and earlier have only those.
On Windows, unzip it and use ironruby.cmd, iirb.cmd and igem.cmd, or double-click
Samples\Notepad\notepad.cmd. From 4.0.3 on there is also an installer,
ironruby-<version>-win-x64-setup.exe, in the style of RubyInstaller: it installs the same
tree into C:\IronRuby40-x64 (or a directory you choose), for you or for all users, puts
that directory first on your PATH so ironruby, ir, iirb and igem work from a new
terminal, and its uninstaller removes it all again, installed gems included. It needs no
admin rights for you alone, and keeps the default path free of spaces, which Bundler, the
Rails templates and mspec do not handle. Nothing in that directory is named like a CRuby
launcher, so CRuby's ruby, irb and gem are not affected. Util/installer/build-installer.sh makes
it from the zip with Inno Setup, from
Util/installer/ironruby.iss. The archive has the same layout as a checkout, with the
Release build in Src/Console/bin/Release/net8.0, so everything above applies to it
unchanged. irubyc works too, but it still needs the .NET SDK to compile the application.
The Windows archive also carries the Windows Desktop runtime, for Windows Forms and WPF, and
Samples/. License/ holds IronRuby's licenses and, for each .NET framework an archive
bundles, Microsoft's license and third-party notices.
Util/package-release.sh <rid> builds such an archive from a checkout, and
.github/workflows/release.yml builds all three, and the Windows installer from the zip,
installs and uninstalls it once for the current user and once for all users, and
publishes them when a v* tag is pushed; the launch video is rendered and
added by hand.
Releases from 4.0.2 on carry a signed build provenance attestation
for each archive (from 4.0.3 on, the installer too) and for SHA256SUMS, made by GitHub's actions/attest in the job that
built the file. It says
which workflow built the file, from which commit, in which run, and that the file is
the one it built. Check one with the GitHub CLI:
$ gh attestation verify ironruby-<version>-linux-x64.tar.gz --repo Largo/ironrubyWhat is in an archive comes from these places, none of them a binary in this repository:
IronRuby, the DLR and prism (libprism too) are compiled in the job from source, at the
revisions release.yml pins; the NuGet packages the projects name, at the versions they
pin - AngleSharp, AngleSharp.Xml, Microsoft.Data.Sqlite, System.Text.Encoding.CodePages
and SQLitePCLRaw.bundle_e_sqlite3, whose native e_sqlite3 is SQLite as that package
builds it on Windows and macOS; on Linux, SQLite's own amalgamation source, downloaded
from sqlite.org and checked against the SHA-256 Util/build-e_sqlite3.sh pins, which
that script compiles in the job instead (the package's library crashes beside another
SQLite); and the .NET runtime. The repository itself holds no programs - no .exe,
.dll, .so or any other binary that runs - and CI keeps it so with
Util/check-no-executables.rb, which looks at what each tracked file is, not at its name.
Requires the .NET SDK, plus Ruby and a C compiler to build prism.
$ git clone https://github.com/IronLanguages/dlr ../dlr # modern DLR, referenced as source
$ git -C ../dlr checkout <DLR_REF from .github/workflows/ci.yml>
$ for p in Build/dlr-patches/*.patch; do git -C ../dlr apply "$PWD/$p"; done
$ git clone https://github.com/ruby/prism ../prism
$ (cd ../prism && ruby templates/template.rb && make shared) # libprism.so
$ (cd Src/Prism && ruby generate.rb) # C# nodes + loader
$ dotnet build Src/Console/Ruby.Console.csprojThe prism revision is pinned: Generated/ is produced from a particular config.yml, and
a libprism built from a different one deserializes into the wrong fields. See
Src/Prism/README.md for the revision and how to check a checkout.
Running the conformance suite:
$ git clone https://github.com/ruby/spec && git clone https://github.com/ruby/mspec
$ RUBY_EXE=./ir.sh ./ir.sh -Imspec/lib mspec/bin/mspec-run spec/language
$ Util/parallel-sweep.sh out # all five suites, 8 at a time, ~8 minutes
$ Util/run-tests.sh # IronRuby's own C# tests
$ Util/bench/run.sh # benchmarks against CRuby
$ Util/gems/run.sh # default/bundled gems, each with its own suiteWindows needs prism.dll instead of libprism.so, and nothing else: ir.exe is an
ordinary framework-dependent .NET 8 app, so it can be published from Linux and run on a box
that has only the runtime installed. Build prism with the RubyInstaller DevKit's MinGW
(make shared names its output after RbConfig's SOEXT, so there it is libprism.dll):
C:\> git clone https://github.com/ruby/prism C:\prism
C:\> cd C:\prism && git checkout 30a25553
C:\prism> ruby templates\template.rb
C:\prism> ridk exec make shared -j4 :: -> C:\prism\build\libprism.dll
Copy libprism.dll next to libprism.so in ../prism/build and build or publish for
Windows; IronRuby.Prism.csproj ships whichever shared libraries are there and
PrismNative loads the one for the platform it wakes up on:
$ dotnet build Src/Console/Ruby.Console.csproj -c Release -r win-x64 --self-contained falseCopy Src/Console/bin/Release/net8.0/win-x64/, Src/StdLib/, ironruby.cmd, ir.cmd and iirb.cmd to the
Windows machine, keeping the same relative layout (ir.cmd finds the binaries under
Src\Console\bin\%IR_CONFIG%\net8.0\, with or without a win-x64 level). Then:
C:\ir> set IR_CONFIG=Release
C:\ir> ir.cmd -e "puts RUBY_DESCRIPTION"
IronRuby 4.0.3 (4.0.0) on .NET 8.0.31 [x64-mswin64]
C:\ir> iirb.cmd
C:\ir> set RUBY_EXE=C:\ir\ir.cmd
C:\ir> ir.cmd -Imspec/lib mspec/bin/mspec-run spec/language
ir.cmd is the twin of ir.sh; note that -X:StdLib is split on the platform's path
separator, so its argument is ;-separated on Windows (a :-separated list would be cut
apart at every drive letter).
Conformance on Windows 11 x64 (.NET 8.0.31), same ruby/spec revision as the Linux table above. Some suites are run a directory at a time there because three of them hang (see below), which the whole-suite numbers on Linux do not need:
| suite | examples | Windows | Linux |
|---|---|---|---|
spec/language |
2920 | 0 failures, 0 errors | 0 |
spec/security |
32 | 0 | 0 |
spec/command_line |
167 | 13 failures, all of them the spec's Unix shell syntax — -e 'code' (cmd.exe does not treat ' as a quote) and 2> /dev/null |
0 |
spec/library |
3902 | 1 failure, 193 errors; io-wait, net-http and socket hang |
0 |
spec/core |
20999 | 74 failures, 172 errors | 21 errors, all ObjectSpace |
Where the Windows errors are: core/file (113) and core/filetest (8) are symlinks,
chmod/umask and the other Unix file metadata; core/process (36 failures, 15 errors) is
fork, process groups, uid/gid and signals; library/win32ole (114) and library/readline
(25) are libraries IronRuby does not implement and whose specs only run on Windows;
library/zlib (3 errors, and 2 failures on .NET 10) is the zlib below. core/objectspace needs
-X:ObjectSpace, as on Linux.
zlib on Windows runs on the zlib inside .NET itself (System.IO.Compression.Native:
zlib-ng from .NET 9 on, Intel's zlib in .NET 8), since there is no libz to bind to. It
inflates and deflates, gzip included, but its compressed bytes are that compressor's: zlib-ng's
differ from classic zlib's (the 2 library/zlib failures on .NET 10 compare exact bytes), and what .NET does not
export raises NotImplementedError: set_dictionary, Deflate#params once data has gone in,
and Inflate#sync_point? (the 3 errors).
What Windows does not have yet: io/console (IO.console is
nil, the termios binding being Unix-only by construction; irb and reline fall back to
their ANSI path and start fine — Src/Libraries/Termios would need a Win32 console-API
twin), IO#wait/IO.select on a pipe (they are poll(2), which is why io-wait,
socket and core/io hang rather than fail), redirection of descriptors above 2 in
Process.spawn/exec options (Windows hands a child three handles, not a descriptor
table, so such a redirection is refused with EINVAL), and the Unix-only half of
Src/Libraries/Builtins/PosixProcess.cs (setsid, getpriority, setrlimit, …).
Apache License 2.0, as the original. See Src/Public.
The original content description is preserved in README.txt.


