← Articles

IronRuby 4.0 : Ruby 4.0 sur .NET 8

IronRuby, c’est Ruby sur le runtime .NET. Microsoft l’a lancé en 2007 sur le Dynamic Language Runtime (DLR), la même base qu’IronPython, puis l’a confié à la communauté en 2010. La dernière version est sortie en 2011. Elle parlait Ruby 1.9.2 et nécessitait le .NET Framework 4 ; depuis, le projet est resté plutôt silencieux.

Je lui ai redonné vie dans mon fork, largo/ironruby. Il se compile et tourne sur .NET 8 sous Linux et Windows, et il analyse Ruby avec prism, le même parser que CRuby, au lieu de la grammaire 1.9 portée à la main en 2011. RUBY_VERSION vaut désormais 4.0.0.

C’est pourquoi je l’appelle IronRuby 4.0. Désormais, le numéro de version d’IronRuby suit la version de Ruby qu’il implémente : IronRuby 4.0 correspond à Ruby 4.0, et il passera en 4.1 lorsqu’il adoptera Ruby 4.1. Le dernier chiffre compte les versions propres à IronRuby. La première version binaire, 4.0.0-preview1, est disponible dès maintenant.

À quoi ça ressemble

$ ./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

C’est irb 1.16.0 avec reline, dans les mêmes versions que celles livrées avec CRuby 4.0, avec coloration syntaxique, indentation automatique, complétion par Tab et historique. Dans CRuby, io/console est une extension C ; IronRuby l’implémente donc en C# au-dessus de termios.

La syntaxe Ruby moderne fonctionne : pattern matching, arguments nommés, méthodes sans end, navigation sûre, it et _1, et transfert d’arguments avec ....

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? }

Ce qui a changé

  IronRuby 1.1 (2011) IronRuby 4.0 (2026)
Runtime .NET Framework 4 .NET 8 (Linux, Windows)
Build ancien msbuild dotnet build
Parser grammaire 1.9 portée à la main prism
RUBY_VERSION 1.9.2 4.0.0
Bibliothèque standard Ruby 1.9 Ruby 4.0, intégrée
Gems RubyGems 1.3.7 RubyGems 4.0 et Bundler
Exécution interpréteur DLR et IL plus un JIT de méthodes et OSR

prism comme frontend

prism est une bibliothèque C. IronRuby charge libprism via P/Invoke, lit la sérialisation binaire de l’arbre syntaxique produite par prism avec un loader C# généré, et la projette sur l’AST existant d’IronRuby. Le reste de la chaîne de compilation reste inchangé. JRuby utilise la même architecture pour son loader Java. Un script Ruby génère les 152 classes de nœuds C# à partir du config.yml de prism lui-même ; mettre à jour prism revient donc à recompiler la bibliothèque et à relancer le générateur.

La syntaxe pour laquelle l’AST de l’époque 1.9 n’avait pas de nœud est ramenée à des constructions qu’il connaît déjà. Le pattern matching, par exemple, devient une suite de vérifications ===, deconstruct et deconstruct_keys avec des liaisons de variables locales.

Quel degré de compatibilité ?

Je mesure avec ruby/spec, la suite de tests qu’utilisent JRuby, TruffleRuby et les autres implémentations. CRuby 4.0.6 sert de référence : quand IronRuby et une spec divergent, je vérifie ce que fait ruby et je modifie IronRuby, pas la spec.

suite exemples échecs sous Linux
spec/language 2934 0
spec/command_line 175 0
spec/security 34 0
spec/library 6398 0
spec/core 23136 21, dont 20 pour ObjectSpace.each_object

ObjectSpace.each_object doit parcourir le tas, ce que .NET ne permet pas par défaut. Comme dans JRuby, c’est disponible derrière une option (-X:ObjectSpace).

Sous Windows, spec/language et spec/security passent aussi sans échec. Le reste y est plus approximatif, surtout à cause de fonctionnalités Unix comme les liens symboliques, fork et les signaux. Il existe une version macOS, mais elle n’a presque pas été testée.

Gems et Rails

igem install rack et ir -S bundle install installent des gems depuis rubygems.org via un vrai TLS. Les bundled gems de CRuby 4.0, comme rake, minitest, rexml et csv, sont livrées avec IronRuby. Les bibliothèques que CRuby implémente en C, comme json, psych, openssl, bigdecimal, zlib et date, sont écrites en C#. J’ai aussi porté en C# les parties C de msgpack, oj et bcrypt, et nio4r et puma tournent.

Une application Rails 8.1 démarre et répond aux requêtes HTTP. ActiveSupport, ActionPack, ActionView avec des templates ERB, ActiveJob et ActiveRecord se chargent tous. Il manque deux choses pour une application Rails complète, et toutes deux viennent d’extensions C : nokogiri, et un adaptateur de base de données, puisque sqlite3, pg, mysql2 et trilogy sont tous natifs.

En général, IronRuby ne peut pas charger d’extensions C, car il n’y a pas de ruby.h contre lequel compiler. Beaucoup de gems se rabattent sur du pur Ruby quand leur extension manque, et IronRuby utilise ce repli.

Performances

Deux compilateurs spécialisent le code selon les types qu’un programme utilise réellement, et tous deux sont activés par défaut :

Sur 50 benchmarks, IronRuby est environ 3 fois plus lent que CRuby 4.0.6 en moyenne géométrique. En arithmétique pure, il est plus rapide : fib, mandelbrot, int_arith et float_arith tournent en 0,27 à 0,74 fois le temps de CRuby. Les exceptions et les fibers sont les points faibles, car construire une backtrace coûte cher sur .NET et chaque fiber est un thread CLR.

.NET depuis Ruby, Ruby depuis .NET

C’est pour cela qu’IronRuby a été créé. Le code Ruby peut utiliser directement les types .NET :

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

Dans l’autre sens, une application .NET peut embarquer IronRuby et exécuter des scripts Ruby, par exemple comme plugin ou langage de script.

irubyc : empaqueter des applications

En tant que mainteneur d’OCRAN, il me tient à cœur de livrer des programmes Ruby à des personnes qui n’ont pas Ruby installé. irubyc, nommé d’après jrubyc de JRuby, transforme un programme Ruby en application .NET :

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

Les fichiers Ruby sont embarqués dans l’assembly comme ressources, avec IronRuby et la bibliothèque standard. --single-file produit un seul exécutable, et --self-contained embarque aussi le runtime .NET, de sorte que le programme tourne sur une machine sans .NET. Avec --library, irubyc produit une seule DLL qu’une application hôte .NET peut charger comme plugin, avec IronRuby à l’intérieur.

irubyc ne compile pas encore Ruby en IL : les sources embarquées sont analysées au démarrage. Un prototype de vraie compilation anticipée, de Ruby vers IL puis vers un exécutable NativeAOT, se trouve dans Util/aot.

L’essayer

La première préversion, IronRuby 4.0.0-preview1, propose des téléchargements pour Linux x64, Windows x64 et macOS sur Apple silicon. Ils apportent leur propre runtime .NET ; rien d’autre n’est à installer :

$ 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

Sous Windows, décompressez l’archive et utilisez irb.cmd, ir.cmd et igem.cmd. La version macOS démarre, mais je ne l’ai presque pas encore testée. irubyc fonctionne aussi depuis le téléchargement ; pour compiler une application, il faut le SDK .NET.

Si vous préférez le compiler vous-même, le README explique comment. Il vous faut le SDK .NET, Ruby et un compilateur C pour prism.

IronRuby 4.0 est encore une préversion. Mais il fait tourner le Ruby d’aujourd’hui sur le .NET d’aujourd’hui, et l’écart avec CRuby se réduit à chaque passe de ruby/spec. Si vous l’essayez et que quelque chose casse, merci d’ouvrir une issue ou de m’écrire.

Liens