IronRuby 4.0:.NET 8で動くRuby 4.0
IronRubyは、.NETのランタイム上で動くRubyです。2007年にMicrosoftがIronPythonと同じDynamic Language Runtime(DLR)の上で開発を始め、2010年にコミュニティへ引き継がれました。最後のリリースは2011年です。対応していたのはRuby 1.9.2で、.NET Framework 4が必要でした。それ以来、プロジェクトはほとんど止まっていました。
私はフォークlargo/ironrubyで、IronRubyをよみがえらせました。LinuxとWindowsの .NET 8 でビルド・実行でき、2011年に手作業で移植された1.9の文法の代わりに、CRubyと同じパーサーである prism でRubyを解析します。RUBY_VERSION は 4.0.0 になりました。
そこで名前も IronRuby 4.0 にしました。これからは、IronRubyのバージョン番号は実装しているRubyのバージョンに合わせます。IronRuby 4.0はRuby 4.0で、Ruby 4.1に対応したら4.1になります。最後の数字はIronRuby自身のリリースの番号です。最初のバイナリ版、4.0.0-preview1を公開しました。
実際の様子
$ ./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
これはCRuby 4.0に同梱されているものと同じirb 1.16.0とrelineで、シンタックスハイライト、自動インデント、Tab補完、履歴が使えます。CRubyでは io/console はC拡張なので、IronRubyではtermiosを使ってC#で実装しています。
パターンマッチ、キーワード引数、エンドレスメソッド、ぼっち演算子(&.)、it や _1、... による引数の転送など、最近のRubyの構文が使えます。
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? }
変わったこと
| IronRuby 1.1(2011年) | IronRuby 4.0(2026年) | |
|---|---|---|
| ランタイム | .NET Framework 4 | .NET 8(Linux、Windows) |
| ビルド | 古いmsbuild | dotnet build |
| パーサー | 手作業で移植した1.9の文法 | prism |
RUBY_VERSION |
1.9.2 | 4.0.0 |
| 標準ライブラリ | Ruby 1.9 | Ruby 4.0(同梱) |
| gem | RubyGems 1.3.7 | RubyGems 4.0とBundler |
| 実行 | DLRインタプリタとIL | さらに メソッドJITとOSR |
フロントエンドとしてのprism
prismはCのライブラリです。IronRubyはP/Invokeで libprism を読み込み、prismが構文木をバイナリにシリアライズしたものを生成されたC#のローダーで読み取り、IronRubyの既存のASTに変換します。コンパイラのそれ以降の部分はそのままです。JRubyもJavaのローダーで同じ仕組みを使っています。152個のC#のノードクラスは、prism自身の config.yml からRubyのスクリプトで生成します。そのため、prismを更新するにはライブラリをビルドし直してジェネレーターを再実行するだけです。
1.9時代のASTに対応するノードがない構文は、ASTがすでに知っている構造に変換します。たとえばパターンマッチは、ローカル変数への束縛を伴う ===、deconstruct、deconstruct_keys のチェックの連続になります。
互換性はどのくらい?
JRubyやTruffleRubyなど、ほかの実装も使っているテストスイートruby/specで測っています。基準はCRuby 4.0.6です。IronRubyとspecの結果が食い違ったら、ruby の動きを確かめて、specではなくIronRubyのほうを直します。
| スイート | 例の数 | Linuxでの失敗 |
|---|---|---|
spec/language |
2934 | 0 |
spec/command_line |
175 | 0 |
spec/security |
34 | 0 |
spec/library |
6398 | 0 |
spec/core |
23136 | 21(うち20が ObjectSpace.each_object) |
ObjectSpace.each_object はヒープをたどる必要がありますが、.NETは標準ではそれを提供していません。JRubyと同じように、オプション(-X:ObjectSpace)で有効にできます。
Windowsでも spec/language と spec/security は失敗ゼロです。それ以外は、シンボリックリンク、fork、シグナルといったUnixの機能のために、まだ粗い部分があります。macOS版もありますが、ほとんどテストしていません。
gemとRails
igem install rack や ir -S bundle install で、rubygems.orgから本物のTLSでgemをインストールできます。rake、minitest、rexml、csvなど、CRuby 4.0のbundled gemsはIronRubyに同梱されています。json、psych、openssl、bigdecimal、zlib、date など、CRubyがCで実装しているライブラリはC#で書いています。さらにmsgpack、oj、bcryptのC部分をC#に移植し、nio4rとpumaも動きます。
Rails 8.1のアプリケーションが起動し、HTTPリクエストに応答します。ActiveSupport、ActionPack、ERBテンプレートを使うActionView、ActiveJob、ActiveRecordはすべて読み込めます。完全なRailsアプリに足りないものは二つで、どちらもC拡張です。nokogiriと、データベースアダプターです。sqlite3、pg、mysql2、trilogyはすべてネイティブだからです。
一般に、IronRubyはC拡張を読み込めません。コンパイルに使う ruby.h がないからです。拡張がないときのための純粋なRubyの代替実装を持つgemも多く、IronRubyはその代替実装を使います。
パフォーマンス
二つのコンパイラが、プログラムで実際に使われる型に合わせてコードを特殊化します。どちらも標準で有効です。
- メソッドJIT: よく呼ばれるメソッドを、ボックス化しない整数・浮動小数点数、CLRの演算、再帰の直接呼び出しを使うコピーに置き換えます。メソッドが再定義されたり、整数がオーバーフローしたりすると、汎用のバージョンに戻ります。
- OSR(オンスタック置換): 十分長く回ったループを、実行中のまま特殊化したコピーに置き換えます。一度しか呼ばれないブロックの中のホットなループにも効きます。
50個のベンチマークの幾何平均では、IronRubyはCRuby 4.0.6の約3倍遅いです。純粋な計算では速く、fib、mandelbrot、int_arith、float_arith はCRubyの0.27〜0.74倍の時間で終わります。弱点は例外とFiberです。.NETではバックトレースの作成が重く、FiberひとつひとつがCLRのスレッドだからです。
Rubyから.NETを、.NETからRubyを
これこそIronRubyが作られた目的です。Rubyのコードから.NETの型を直接使えます。
date = System::DateTime.new(2000, 1, 1)
System::Environment.OSVersion.Platform
逆に、.NETのアプリケーションにIronRubyを組み込み、プラグインやスクリプト言語としてRubyのスクリプトを実行することもできます。
irubyc:アプリケーションのパッケージ化
OCRANのメンテナーとして、Rubyをインストールしていない人にもRubyのプログラムを届けられることを大切にしています。JRubyの jrubyc にならって名付けた irubyc は、Rubyのプログラムを.NETのアプリケーションにします。
$ ./irubyc.sh --single-file --self-contained -t out app.rb lib/
Rubyのファイルは、IronRubyや標準ライブラリと一緒に、リソースとしてアセンブリに埋め込まれます。--single-file で実行ファイルひとつになり、--self-contained を付けると.NETのランタイムも同梱されるので、.NETのないマシンでも動きます。--library を使うと、IronRubyを内蔵したDLLひとつを作り、.NETのホストアプリケーションからプラグインとして読み込めます。
irubyc はまだRubyをILにコンパイルしません。埋め込まれたソースは起動時に解析されます。RubyからILを経てNativeAOTの実行ファイルまで作る本当の事前コンパイルのプロトタイプは、Util/aot(英語)にあります。
試してみる
最初のプレビュー版IronRuby 4.0.0-preview1は、Linux x64、Windows x64、AppleシリコンのmacOS向けにダウンロードできます。.NETのランタイムが同梱されているので、ほかに何もインストールする必要はありません。
$ 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
Windowsではzipを展開して、irb.cmd、ir.cmd、igem.cmd を使います。macOS版は起動しますが、まだほとんどテストしていません。ダウンロードした版でも irubyc は使えますが、アプリケーションをコンパイルするには.NET SDKが必要です。
自分でビルドしたい場合は、README(英語)に手順があります。.NET SDK、Ruby、prism用のCコンパイラが必要です。
IronRuby 4.0はまだプレビュー版です。それでも、今のRubyを今の.NETで動かせますし、ruby/specを回すたびにCRubyとの差は縮まっています。試してみて動かないところがあれば、issueを立てるか、メールでお知らせください。