こんにちは、柴田です。やっとデス・ストランディング2の国道復旧もひと段落してトロフィーコンプリートまであと1つというところまで辿り着きました。
さて、今回は秋以降の活動のお知らせと、Rubyの内部的な仕組みに少し踏み込んで、多くのRubyistにおなじみの Pathname が、どのようにしてRubyのコア機能の一部になったのか、その背景と技術的な詳細を解説します。
RailsWorld 2025 に登壇します
2025年9月4-5日の2日間にわたってオランダのアムステルダムで開催される RailsWorld 2025 に登壇します。
RailsWorld は日本時間の深夜にチケット販売が始まって、毎回すぐに売り切れという大変人気のあるカンファレンスなので、スピーカーとしていつか登壇できたらいいなあと思いつつ、proposal がなかなか通らない...というのを繰り返していました。そんなおり、RailsWorld の organizer から、Ruby 開発チームと Rails 開発チームとの対談セッションに来ないか?と誘いを受けたので快諾して、行くことにしました。
対談相手は tenderlove こと Aaron と byroot とのことで、何を話すといいかなあというのを考えているところですが、 RailsWorld というカンファレンスに参加した人が聞いてよかった、となるようなトピックを用意したいと思います。
Pathnameを組み込みクラスにした話
さて、ここからは最近やっていることの紹介シリーズとして、pathname ライブラリを Ruby 本体に組み込んだ話をしたいと思います。
Rubyでファイルパスをオブジェクト指向的に扱うための定番ライブラリ、Pathname。私も irb や rails console でちょっとした Ruby のコードを書いて動作を検証するときや、使い捨ての Ruby スクリプトの冒頭で何回書いたかわからないくらい require "pathname" と書いてきました。
Pathname("foo.txt") を実行して未ロードのエラーが出るたびに「あ、pathname を require しないと」と require しなおしてから再実行、というのを繰り返してきたため、Pathname くらいは組み込みクラスとして使えたらいいのになあと Feature #17473: Make Pathname to embedded class of Ruby として提案していました。
実際に提案したのは 4 年前ではありますが、2025年7月の開発者会議で Matz に「改めて組み込みクラスにするのはどうですか」と聞いてみたら、別にいいよ、という返事をもらえたので組み込みクラスにしてしまいました。
Ruby 3.5 または 4.0 以降の Pathname
現時点の Pathnameのコアへの組み込みはいくつかの申し送り事項が残っています。
Pathnameの機能は Ruby で記述されている pathname.rb と C 言語による拡張である pathname.so の2つのファイルから提供されます。これまでは require "pathname" を実行するとまず pathname.rb が読み込まれ、pathname.rb の中から pathname.so を呼び出すことで Pathname のすべての機能が利用可能となっていました。
しかし、Ruby 3.5 または 4.0 以降では、Pathname は組み込みクラスとして提供するために、pathname.rb のうち find や fileutils といった他の標準ライブラリに依存していないメソッドを pathname_builtin.rb として分離し、このファイルと pathname.c に定義されるメソッドなどのみを本体に組み込むことにしました。
そのため、Pathname#find や Pathname#rmtree など他のライブラリに依存しているメソッドを使いたい場合は、引き続き require "pathname" を実行する必要があります。これらは遅延ロードされるため、別に組み込みメソッドにしてしまって、メソッドを呼び出したら fileutils などを require するという挙動で良いではないか、という考えもあります。しかし、組み込みクラスに存在するメソッドを実行したら他のライブラリをロードする、というのは違和感がある、という意見も参考にして pathname.rb というファイルを残すことにしました。
これらのメソッドが他のライブラリに依存しない形になればすべての Pathname のメソッドが組み込みクラスとして提供されることになります。興味がある方は、依存ライブラリを使わない実装への書き換えをチャレンジしていただけると助かります。
Pathname を組み込みにする技術的な仕組みの裏側
C 言語での組み込み対象の指定
Pathnameを組み込みクラスにするにあたって、pathname.c のように C で書かれているものはファイルをトップディレクトリに移動した上で、以下のように Makefile ( common.mk ) と inits.c にオブジェクトファイルとエンドポイント関数の名称を追加するだけです。
# common.mk
COMMONOBJS = \
array.$(OBJEXT) \
# ...
pack.$(OBJEXT) \
pathname.$(OBJEXT) \
# ...
# inits.c rb_call_inits(void) { CALL(default_shapes); # ... CALL(MemoryView); CALL(pathname); # ...
pathname.c には Init_pathname という関数が定義されており、rb_call_inits の中に追加した CALL(pathname); はこの関数を呼び出すことを意味します。余談となりますが pathname.c の中には InitVM_pathname という関数もあります。これは VM の初期化時に呼び出される関数なのですが、Multi VM の構想時の名残ということで Init_* と混在して使われているというのが現状です。
また、実際には上記に示したエンドポイントの定義だけではなく、pathname.$(OBJEXT) 自体の依存関係定義もパッケージングのためには依存関係を追記する必要があります。この依存関係の解析と更新は手元でも実行できますが Linux が必要なため macOS などを普段使いしている場合は GitHub Actions に存在する check_dependencies.yml の結果を参照するのが一番楽な方法です。
具体的な中身については今回は省略しますが、まずは上記の変更を加えた上で pull-request を作成すると、依存関係の更新が必要とエラーで GitHub Actions が fail し、diff が出力されます。この diff を確認して、common.mk に内容を反映させると、C 言語による組み込みは完了です。
Ruby スクリプトの組み込み方法
冒頭にも説明したように Pathname は C による拡張だけではなく Ruby スクリプトも主要な機能として持っています。この Ruby スクリプトをインタプリタに組み込むには Ruby のビルドプロセスに用意されている仕組みを使います。
この仕組みは知る人だけが知っている、という予感がするので、今回はドキュメントを新たに書くつもりで本エントリで解説しようと思います。この仕組みを使うことで誰でも自分の手元の Ruby スクリプトを Ruby インタプリタに組み込むことができるようになります。
この組み込みの仕組みはRubyのビルド時に行われ、大きく分けて以下のステップで構成されています。
まず、どの Ruby スクリプトを組み込むかをC言語同様に common.mk で指定します。Rubyのソースコード内にある common.mk に BUILTIN_RB_SRCS という変数があり、ここに対象ファイルがリストされています。
# common.mk
BUILTIN_RB_SRCS = \
# ...
$(srcdir)/kernel.rb \
$(srcdir)/pathname_builtin.rb \
$(srcdir)/ractor.rb \
# ...
$(srcdir) は Ruby のソースコードのルートディレクトリを指し、ここにある pathname_builtin.rb が組み込み対象の Ruby スクリプトになります。もし foo.rb というファイルを組み込みたい場合は、ルートディレクトリに foo.rb というファイルを配置した上で、$(srcdir)/foo.rb を追加します。
Ruby起動時のロード
続いて、 pathname_builtin.rb を Ruby インタプリタの起動時にロードするための設定を行います。これには inits.c というファイルを編集します。inits.c に存在する以下の箇所に BUILTIN(pathname_builtin); のような行を追加します。
# inits.c #define BUILTIN(n) CALL(builtin_##n) BUILTIN(kernel); # ... BUILTIN(pack); BUILTIN(pathname_builtin); # ...
この後に make を実行すると、Ruby のビルドプロセスが Ruby スクリプトを組み込むために必要な rbinc という拡張子のファイルを生成し、pathname_builtin.rb を組み込みクラスとして扱うように設定した上で ruby インタプリタのバイナリに組み込みます。
この rbinc というファイルを生成し、実際に Ruby スクリプトを組み込むための C コードを生成するのが tool/mk_builtin_loader.rb という Ruby スクリプトです。このスクリプトは、指定された Ruby スクリプトをパースして、Ruby VM が実行可能なバイトコードに変換し、それを C の配列として表現します。が、これらの詳細は今回は割愛するので興味がある方は実際に Ruby のソースコードを見てみてください。
後方互換性のための仕組み
さて、Pathname が組み込みクラスとして提供されるようになった後に require "pathname" を実行すると、Ruby は愚直に $LOAD_PATH の中から pathname.rb を探してロードしようとします。pathname の場合は先に説明したように、一部のメソッドが残っているため、pathname.rb をロードする必要がありますが、従来から存在した pathname.so によって提供される Pathname の機能はすでに Ruby インタプリタに組み込まれているため、何度もロードする必要はありません。
Ruby には指定したライブラリをロード済みとしてマークする rb_provide というCの関数が存在します。この関数は、Cのコード内で rb_provide("pathname.so") のように呼び出します。この呼び出しによって、Ruby のグローバル変数 $LOADED_FEATURES に指定された文字列が追加され、require "pathname.so" が実行されても空振りするようになります。
最終的には pathname.rb として残っているメソッドを全て組み込みにした後に rb_provide("pathname.rb") とすることで、既存の require "pathname" というコードを実行しても問題なく動作するようになる、というのをこの Pathname 組み込みの最終ゴールとしています。頑張ろう。
まとめ
今回は、PathnameがRubyのコア機能へと統合された背景と、新たな C や Ruby のコードをインタプリタに組み込む仕組みについて解説しました。
そもそもこの Ruby にして組み込む機能は C で書いたコードよりも Ruby で書いて組み込んだ方が JIT が効いて速いから、C のコメントで書いたドキュメントよりも Ruby に書いた方がやりやすいから、など複数の理由が最近の主な目的になっていますが、そのうち以前に開発されていた Rubinius のように Ruby の大半が Ruby で書かれるようになるのかもしれません。個人的には C よりも Ruby の方が得意なので、Ruby のコードが増えれば増えるほど、何かをやる余地が増えるので嬉しいです。
なお、この Pathname を組み込んだ直後に RubyのPathnameライブラリが本体組み込みになったらGC周りのテスト失敗がおきた - STORES Product Blog という問題も発生するなど、私自身は毎日ワイワイと賑やかに Ruby を開発している毎日ですが、アンドパッドではRubyの内部構造に興味がありつつもプロダクトを開発したい探求心旺盛なエンジニアも募集しています。ご興味のある方は、ぜひ採用情報をご覧ください。