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 :
- JIT de méthodes : une méthode chaude est remplacée par une copie avec des entiers et flottants non boxés, de l’arithmétique CLR et des appels directs pour la récursion. Si une méthode est redéfinie ou qu’un entier déborde, on revient à la version générique.
- OSR (on-stack replacement) : une boucle qui tourne assez longtemps est remplacée par une copie spécialisée pendant son exécution. Cela atteint les boucles chaudes à l’intérieur d’un bloc appelé une seule fois.
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.