IronRuby 4.0: Ruby 4.0 su .NET 8
IronRuby è Ruby sul runtime .NET. Microsoft lo ha avviato nel 2007 sulla Dynamic Language Runtime (DLR), la stessa base di IronPython, e lo ha affidato alla community nel 2010. L’ultima release è uscita nel 2011. Parlava Ruby 1.9.2 e richiedeva il .NET Framework 4; da allora il progetto è rimasto perlopiù fermo.
L’ho riportato in vita nel mio fork, largo/ironruby. Si compila e gira su .NET 8 sotto Linux e Windows, e analizza Ruby con prism, lo stesso parser usato da CRuby, invece della grammatica 1.9 portata a mano nel 2011. RUBY_VERSION ora è 4.0.0.
Per questo lo chiamo IronRuby 4.0. D’ora in poi il numero di versione di IronRuby segue la versione di Ruby che implementa: IronRuby 4.0 è Ruby 4.0, e diventerà 4.1 quando passerà a Ruby 4.1. L’ultima cifra conta le release proprie di IronRuby. La prima release binaria, 4.0.0-preview1, è disponibile ora.
Come si presenta
$ ./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
È irb 1.16.0 con reline, le stesse versioni incluse in CRuby 4.0, con evidenziazione della sintassi, indentazione automatica, completamento con Tab e cronologia. In CRuby io/console è un’estensione C, quindi IronRuby lo implementa in C# su termios.
La sintassi moderna di Ruby funziona: pattern matching, argomenti keyword, metodi endless, safe navigation, it e _1, e l’inoltro degli argomenti con ....
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? }
Cosa è cambiato
| IronRuby 1.1 (2011) | IronRuby 4.0 (2026) | |
|---|---|---|
| Runtime | .NET Framework 4 | .NET 8 (Linux, Windows) |
| Build | msbuild legacy | dotnet build |
| Parser | grammatica 1.9 portata a mano | prism |
RUBY_VERSION |
1.9.2 | 4.0.0 |
| Libreria standard | Ruby 1.9 | Ruby 4.0, inclusa nel repository |
| Gem | RubyGems 1.3.7 | RubyGems 4.0 e Bundler |
| Esecuzione | interprete DLR e IL | in più un JIT a livello di metodo e OSR |
prism come front end
prism è una libreria C. IronRuby carica libprism tramite P/Invoke, legge la serializzazione binaria dell’albero sintattico di prism con un loader C# generato e la mappa sull’AST esistente di IronRuby. Il resto della pipeline del compilatore resta invariato. JRuby usa la stessa architettura per il suo loader Java. Uno script Ruby genera le 152 classi di nodi C# dal config.yml di prism stesso, quindi aggiornare prism significa ricompilare la libreria e rieseguire il generatore.
La sintassi per cui l’AST dell’epoca 1.9 non aveva nodi viene ricondotta a costrutti che già conosce. Il pattern matching, per esempio, diventa una serie di controlli ===, deconstruct e deconstruct_keys con binding di variabili locali.
Quanto è compatibile?
Misuro la compatibilità con ruby/spec, la suite di test usata da JRuby, TruffleRuby e dalle altre implementazioni. Il riferimento è CRuby 4.0.6: quando IronRuby e una spec non concordano, verifico cosa fa ruby e modifico IronRuby, non la spec.
| suite | esempi | falliti su Linux |
|---|---|---|
spec/language |
2934 | 0 |
spec/command_line |
175 | 0 |
spec/security |
34 | 0 |
spec/library |
6398 | 0 |
spec/core |
23136 | 21, di cui 20 ObjectSpace.each_object |
ObjectSpace.each_object deve attraversare l’heap, cosa che .NET non offre di default. Come in JRuby, è disponibile tramite un’opzione (-X:ObjectSpace).
Su Windows anche spec/language e spec/security passano senza errori. Il resto è meno rifinito, soprattutto a causa di funzionalità Unix come symlink, fork e segnali. Esiste una build per macOS, ma è stata testata pochissimo.
Gem e Rails
igem install rack e ir -S bundle install installano gem da rubygems.org con vero TLS. Le gem incluse in CRuby 4.0, come rake, minitest, rexml e csv, sono fornite con IronRuby. Le librerie che CRuby implementa in C, come json, psych, openssl, bigdecimal, zlib e date, sono scritte in C#. Ho portato in C# anche le parti in C di msgpack, oj e bcrypt, e nio4r e puma funzionano.
Un’applicazione Rails 8.1 si avvia e risponde alle richieste HTTP. ActiveSupport, ActionPack, ActionView con template ERB, ActiveJob e ActiveRecord si caricano tutti. Per un’app Rails completa mancano due cose, entrambe dovute a estensioni C: nokogiri e un adapter per database, dato che sqlite3, pg, mysql2 e trilogy sono tutti nativi.
In generale IronRuby non può caricare estensioni C, perché non esiste un ruby.h con cui compilarle. Molte gem ripiegano su puro Ruby quando manca la loro estensione, e IronRuby sfrutta questo fallback.
Prestazioni
Due compilatori specializzano il codice sui tipi effettivamente usati da un programma, ed entrambi sono attivi di default:
- JIT a livello di metodo: un metodo molto usato viene sostituito da una copia con interi e float non boxed, aritmetica CLR e chiamate dirette per la ricorsione. Se un metodo viene ridefinito, o un intero va in overflow, si torna alla versione generica.
- OSR (on-stack replacement): un ciclo che gira abbastanza a lungo viene sostituito da una copia specializzata mentre è in esecuzione. Così si raggiungono anche i cicli critici dentro un blocco chiamato una sola volta.
Su 50 benchmark, IronRuby è in media geometrica circa 3 volte più lento di CRuby 4.0.6. Per l’aritmetica pura è più veloce: fib, mandelbrot, int_arith e float_arith girano in 0,27–0,74 volte il tempo di CRuby. Eccezioni e fiber sono i punti deboli, perché costruire un backtrace su .NET è costoso e ogni fiber è un thread CLR.
.NET da Ruby, Ruby da .NET
È lo scopo per cui IronRuby è nato. Il codice Ruby può usare direttamente i tipi .NET:
date = System::DateTime.new(2000, 1, 1)
System::Environment.OSVersion.Platform
Nella direzione opposta, un’applicazione .NET può integrare IronRuby ed eseguire script Ruby, per esempio come plugin o linguaggio di scripting.
irubyc: distribuire applicazioni
Come maintainer di OCRAN, ci tengo a poter distribuire programmi Ruby a chi non ha Ruby installato. irubyc, chiamato così sul modello di jrubyc di JRuby, trasforma un programma Ruby in un’applicazione .NET:
$ ./irubyc.sh --single-file --self-contained -t out app.rb lib/
I file Ruby vengono incorporati nell’assembly come risorse, insieme a IronRuby e alla libreria standard. --single-file produce un unico eseguibile e --self-contained include anche il runtime .NET, così il programma gira su una macchina senza .NET. Con --library, irubyc produce una singola DLL che un’applicazione host .NET può caricare come plugin, con IronRuby al suo interno.
irubyc non compila ancora Ruby in IL: i sorgenti incorporati vengono analizzati all’avvio. Un prototipo di vera compilazione ahead-of-time, da Ruby a IL a un eseguibile NativeAOT, si trova in Util/aot.
Provarlo
La prima release di anteprima, IronRuby 4.0.0-preview1, offre download per Linux x64, Windows x64 e macOS su Apple silicon. Includono il proprio runtime .NET, quindi non serve installare nient’altro:
$ 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
Su Windows estraete l’archivio e usate irb.cmd, ir.cmd e igem.cmd. La build per macOS si avvia, ma finora l’ho testata pochissimo. Anche irubyc funziona dal download; per compilare un’applicazione serve il .NET SDK.
Se preferite compilarlo da voi, il README spiega come fare. Servono il .NET SDK, Ruby e un compilatore C per prism.
IronRuby 4.0 è ancora un’anteprima. Ma esegue il Ruby di oggi sul .NET di oggi, e il divario con CRuby si riduce a ogni giro di ruby/spec. Se lo provate e qualcosa non funziona, aprite una issue o scrivetemi.