2026年4月22日(水)から24日(金)まで、北海道函館アリーナ及び函館市民会館でRubyKaigi 2026が開催されました。
今年のRubyKaigiは、Ruby 4.Ruby::を扱うキーノート
キーノートの前半では、Ruby Boxとはなにか、Ruby VMのなかでどのようにしてRuby Boxを見つけるのか、といったことが説明され、後半では、Ruby Boxがどのように生まれたのかという物語が語られました。
本稿では機能名としてはRuby::と表記し、講演全体や概念を示す場合はRuby Boxと表記します。
Ruby::Boxとはなにか?
まず、Ruby::とはなにかについて、これはRuby 4.
使いかたとしては、Ruby::と書いて、RUBY_でオンにして使います。Ruby::を使うことでなにが嬉しいのかというと、monkey-patchなどを分離できることです。というのも、Rubyではオープンクラスを採用しているため、いつもどこでもクラスを書きかえることが可能です。この機能を隔離するためにRuby::というもので実現します。
Ruby::がない場合、アプリケーション内appやlibなどで変更した機能を実行環境上すべてに影響を与えます。その変更を覆い隠す手段としてRuby::があります。どういうことかというとRubyプロセス内に別々の空間を作り、各空間のみに影響するようにします。
ユースケースとしては以下のようなものが考えられます。テスト、同じライブラリの別バージョン、そしてパッケージ機能などがあります。それぞれ見ていきます。
- Use Case 1: テスト
- 想定ユースケースの一つはテストです。この例の場合、アプリケーションコードにモックを入れたいことがあります。手順が大変なのでモンキーパッチで済ませたいことがあると思います。モンキーパッチで対処してしまうと必要な箇所以外にも影響を与えてしまう可能性があります。ここで
Ruby::を利用することでモンキーパッチの影響をBox Box内に閉じ込めてしまうことができます。 - Use Case 2: ライブラリのバージョンが異なるもの
- 次に想定しているユースケースはアプリケーションサーバーでライブラリのバージョンが異なるものを試したいときです。v1とv2で非互換のライブラリを同時に動かし、v2で動かない場合はv1に戻すなどが考えられます。
- Use Case 3: パッケージング
- 最後に考えられるユースケースとしてMatzが話しているパッケージングの機能です。この機能はパッケージを
Boxの中に読み込み、そのBoxから公開APIだけを取り出すような使いかたです。このユースケースではBoxを使うことでアプリケーションを壊すことが少なくなります。ただしフックポイント、メモリ使用量など考えるべき点はたくさんあります。
Boxを探す旅
Ruby::の構成要素として大きく3つあります。Box毎にクラス、メソッドの定義を持つこと、スクリプトや拡張ライブラリをBoxの中に読み込むこと、最後にいまどのBoxでプログラムが実行されているかを管理、検出することです。
これまでのRubyKaigiでも、各Ruby::向けのクラスやメソッド定義はRubyKaigi 2024で、スクリプトやsoファイルなど拡張を読み込むことについてはRubyKaigi 2025でそれぞれ話しています。
今回は、RubyのVMでのRuby::実行中のBoxの管理方法について説明しました。
Ruby::はクラスやメソッドの定義を実行プログラム上で分離します。しかしながら、Rubyのプログラムはクラス、メソッドいつでも定義され、どこでも参照されます。Ruby VMは、実行中のコードがどのRuby::の文脈にあるのか、つまりcurrent boxを決定する必要があります。
main.の中からapp.を読み込む例です。
# app.rb
p 1
module App
def self.foo(x=:bar) = [:foo, x].map { it.to_s }
end
# main.rb (RUBY_BOX=1)
...
b1 = Ruby::Box.new
p b1 #=> #<Ruby::Box:3,user,optional>
...
b1.require_relative("app")
...
p b1::App.foo #=> ["foo", "bar"]
Boxを利用する場合はRuby::でBoxを初期化します。Boxをレシーバーにしてrequire_を呼びます。この例ではb1.を呼び出し、app.をb1のなかに読み込みます。このとき、実行されているメインの空間からは直接app.に定義されたモジュールAppは見えません。このときApp.は、b1の中に読み込まれたAppモジュールを参照して呼び出される必要があります。
Boxの切り替えは、Ruby::の特定のメソッドを呼んだときに発生します。対象になるのはrequire、require_、load、evalです。これらで読み込まれたコードはBoxの中で評価されます。
ファイルとBoxが一対一に決まるわけではないです。同じファイルを別々のBoxに読み込むこともできます。境界はファイルのように見えると思いますが、ファイル自体がBoxを決めるわけではないです。
では、ある場所でcurrent boxが何かをどう調べるのか。普通に例外を出すと、Rubyレベルのbacktraceが得られます。main.からrequire_され、app.のトップレベルに入り、モジュール定義の中で例外が起きた、といった情報はわかります。
しかしRuby VMの実装を調べるには、それだけでは情報が足りません。もっと低レベルの実行状態が必要になります。
そこで、開発用にはrb_のような仕組みで Rubyをクラッシュさせることにします。クラッシュさせることでcrash reportを見ることができるようにします。ここで通常のRubyには入っていないデバッグ用にrb_は自分で定義したものです。このcrash reportにはRubyレベルの backtrace だけでなく、control frameの情報が出ます。
Ruby VMの実行状態を見るうえで重要なのがcontrol frameです。CRubyにはrb_という構造体があります。control frameはスタックになっており、Rubyの実行状態を表します。
Rubyレベルのbacktraceは、このcontrol frameから作られたものです。Cレベルの内部情報や、DUMMY、IFUNCのような内部フレームを隠し、Rubyプログラマが読むための形に変換・
Ruby Boxの実装では、Rubyレベルのbacktraceだけではなく、control frameの中身を見ながら
サンプルコードでcurrent boxの遷移を見ると、最初にmain.が実行されるときはメインのboxの中で動きます。Ruby::でb1を作るところもmain boxの中です。
そのあと、b1.によってapp.を読み込みます。するとapp.のトップレベル評価はb1の中で行われます。Appモジュールの定義も、モジュール内のローカル変数の評価も、App.のメソッド定義もb1の中で行われます。
メソッドの中身は読み込み時には評価されません。実行するときに、b1::を呼び出すと、そのメソッドはb1のBoxで定義されたものなので、current boxはb1になり、実行されます。
ここまでが基本的なRuby::
current box 探索の基本
current boxを探す方法としては、まず現在実行中のframeから一番近いRubyのframeを見つけることです。Cで書かれた処理の中にいる場合は、control frameをたどって、最も近いRuby frameまで戻ります。
これからenvという言葉が出てきますが、これは環境変数のENVではありません。Ruby VMのframe environmentを指します。
次に、そのRuby frameが持つenvをたどり、一番近いLOCALなenvを探します。そのLOCALなenvに付いているBoxの情報を調べることで、current boxを見つけます。
ここでいうLOCALなスコープは、ファイル、クラス/モジュール定義、メソッドなどです。一方、ブロック、eval、rescueなどはLOCALではないスコープとして扱われます。
control frameにはep、つまりenv pointerがあります。envには、ローカル変数、ME_、SPECVAL、FLAGSなどが並びます。Rubyのオブジェクトを表すVALUEの並びとして管理されます。
ローカル変数はenvのtail側に並びます。たとえばメソッド引数xとローカル変数c、dがあれば、それらがenvの中に格納されます。
control frameはスタックとして積まれます。各control frameはそれぞれenvを参照しています。
CRubyのスタックメモリ空間では、一方の端からcontrol frameが積まれ、反対側の端からenvが積まれます。呼び出しが深くなるにつれて、両者が内側へ伸びていきます。どこかで衝突すると、それがRubyのstack overflowになります。
envのメンバーはep[-1]のような負のインデックスでアクセスされます。これは、control frameとenvがスタック空間の反対側から伸びる構造に由来します。
envの中にはflagsがあります。ここにはframe magic、frame flags、env flagsなどが入ります。frame magicはframeの種類を表します。たとえばMETHOD、BLOCK、CLASS、TOP、CFUNC、IFUNC、EVAL、RESCUE、DUMMYなどです。frame flagsにはFINISH、CFRAME、LAMBDAなどがあり、env flagsにはLOCAL、ESCAPED、WB_
frame typeごとに、envの中に入っている値は異なります。METHOD、BLOCK、CLASS、TOP、CFUNC、IFUNC、EVAL、RESCUE、DUMMYといったframe typeがあり、それぞれLOCALかどうか、SPECVALに何が入るか、ME_に何が入るかが違います。
MEはmethod entryです。どの名前で、どのクラスに定義されたメソッドなのか、method definitionへの参照などを持ちます。method definition、つまり rb_には、このメソッドがどのBoxで定義されたかを示す情報を追加できます。
BHはblock handlerです。メソッドに渡されたブロックを実行するために使われます。CREFはClass REFerenceで、定数探索やrefinement、可視性などに関わります。Prev EPは外側のenvへのポインターです。
method definitionにrb_のようなBox情報を持たせると、そのメソッドがどのBoxで定義されたかがわかります。つまり、メソッドを実行するときに、どのBoxの中で動くべきかを判断できます。
ブロックの場合は、外側のスコープを参照できるため、Prev EPをたどる必要があります。ネストしたブロックでは、さらに外側のenvへ順にリンクしていきます。
このように、メソッドならmethod definition、ブロックならprevious env、クラスやトップレベルならspecial variableやCREFといった情報を使って、current boxを探します。
frame typeごとの探索方法
frame typeごとにcurrent boxの探し方は異なります。METHODとCFUNCはmethod entryを持つので、method definitionからBoxをたどれます。BLOCK、EVAL、RESCUEなどはLOCALではないため、Prev EPをたどって外側のLOCAL envを探します。IFUNCはCで書かれたブロックのようなもので、こちらも周辺のenvやCREFを見る必要があります。
DUMMYは最初に積まれるframeなので、基本的にはmain boxとして扱えます。
問題になるのはCLASSとTOPです。LOCALではありますが、従来のSPECVALやME_だけではBoxを直接得られません。
CLASS/
Ruby 4.SPECVALにBox情報を入れるようにします。これにより、CLASSやTOPのLOCAL envからもBoxを取り出せるようになります。
その結果、current boxを見つける基本ルールはこうなります。まず一番近いRuby frameを探します。必要ならC functionからRuby frameへ戻ります。そこから一番近いLOCALなenvを探します。そして、そのLOCAL envのSPECVALまたはME_からBoxを取ります。
メソッド定義には、そのメソッドが定義されたBoxをmarkできます。クラス定義やトップレベルでメソッド定義が行われる場合には、そのときのCLASS/
ここで必要になるのがloading boxです。Ruby::や#require_でファイルを読み込むとき、そのファイルのトップレベルframeがpushされます。このTOP frameを
loading box
つまり、ファイルを読み込むときには、まだTOP frameが積まれる前の段階で、
current boxは、いま実行しているframeから求められるBoxです。一方、loading boxは、これからファイルを読み込むときに使うBoxです。
この違いは、$LOAD_のような読み込み前に必要な情報で重要になります。requireがどのファイルパスを解決するかは、ファイルの評価が始まる前に決めなければなりません。したがって、まだTOP frameがpushされてcurrent boxが切り替わる前に、読み込みに関係するグローバルな状態をloading boxに基づいて切り替える必要があります。
つまり、loading boxはcurrent boxよりも少し早いタイミングで効いてくるBoxです。
loading boxを見つけるために、Ruby::などを呼んだframeにVM_、つまりBOX_というframe flagを立てます。
loading boxを探すときは、呼び出しstackを逆にたどって、BOX_が立っているframeを探します。そのframeがRuby::の呼び出しであれば、cfp->selfにレシーバーのRuby::インスタンスが入っています。そこからloading boxを取れます。
ただし、常にselfからBoxが取れるわけではありません。Boxの中で普通のrequire、つまりKernel#requireが呼ばれるケースがあります。この場合はレシーバーが明示されず、selfからBoxを取れないことがあります。
RubyGemsとBox::Loader
さらに厄介なのがRubyGemsです。通常、Kernel#requireはRubyGemsのrequireによって上書きされています。RubyGems自体はRubyのスクリプトで書かれており、別のBox、たとえばroot boxで読み込まれている可能性があります。
Boxの中でrequireを呼び、RubyGemsのrequireを経由してRuby VMのrequireに入ったとします。このとき、単純に一番近いRuby frameを見てBoxを判断すると、RubyGemsのrequireが属するroot boxを見てしまう可能性があります。
しかし本来読み込みたいのは、requireを呼んだアプリケーションコードが属するBoxです。ここでBoxを誤認すると、Box 3に読み込むべきファイルをBox 1に読み込んでしまう、といった問題が起きます。
この問題を解くためにRuby::を使います。Boxを作ったとき、そのBoxの中のObjectにBox::をincludeします。
Boxの中でレシーバーなしのrequireが呼ばれると、まずBox::が呼ばれます。このrequireは自分自身のframeにBOX_flagを立て、その後superでRubyGemsのrequireへ処理を渡します。
RubyGemsのrequireを経由してRuby VMのrequireに入ったあとでも、stackをたどってBOX_flagを探せば、RubyGemsではなくBox::のframeに到達できます。そこで、このrequireがどのBoxの中から呼ばれたのかを正しく判断できます。
つまりBox内の通常のrequireを補足するために、Box::が必要になります。
ここまで、current boxをどう見つけるか、loading boxをどうmarkするかを見てきました。
Ruby Boxの実装では、クラスやメソッドをBoxの中で定義する話、Rubyスクリプトや拡張ライブラリをBoxの中に読み込む話、実行中のcurrent boxを見つける話など、Ruby VMのさまざまな場所を触ることになります。
Ruby::Boxへ至る道
ここからは、Ruby::
「Rubyは好きだけど、これからRubyの開発者になることはないだろうなあ。今のRubyに付け加えたいことないしな、と思っていたんですよ。RubyKaigi 2023のChris Salzberg
"Multiverse Ruby" Shock
Multiverse Rubyは、Rubyプロセスの中に複数のworld、つまり複数の定義空間を持てるようにしたい、というアイデアです。実装方法や細部は現在のRuby Boxとは違いますが、根本にある問題意識は非常に近いものでした。
実はそれ以前、2022年の冬にLFAというapplication serverを作っていました。これはAWS Lambda関数をひとつのRubyプロセス内で複数動かすためのアプリケーションサーバーです。ただし、プロセスグローバルな状態の分離は不完全でした。
その機構とモチベーションについてブログ記事を書き、その後は忘れていたそうです。しかしMultiverse Rubyのトークを聞いて、
前日譚
RubyKaigi 2023 Day 2会場近所のビアバーにて角谷さん、藤村さん、Salzbergさんと夜10時頃から談笑していたときのことです。その夜中に"Namespace 欲しい"となりました。これがRuby Boxのday 0となります。そこから登壇までの時系列は、以下のとおりです。
- D0 (2023/
5/12) Salzbergさんのプレゼンに衝撃を受けた日 - D11 (2023/
5/23) PoCを最初のコミット - D46 (2023/
6/27) bugs. ruby-lang. orgに"Namespace on read"として機能提案 - D78 (2023/
7/29) Namespaceのアイデアを松江RubyKaigiで発表 - D166 (2023/
10/ 25) Ruby Grantに採択 - Y1D1 (2024/
5/13) RubyKaigi 2024 "Namespace, What and Why" を発表 - Y1D3 (2024/
5/15) MatzがRuby 4. 0するためにはNamespaceが必要と宣言 - Y2-D26 (2025/
4/15) "State of Namespace"をRubyKaigi 2025で発表 - Y2-D24 (2025/
4/17) Matzが今年 (2025年)、ZJITと一緒に入れてRuby 4. 0をリリース宣言 - Y2-D11 (2025/
5/1) GitHubにPRを作成 - Y2-D1 (2025/
5/11) merge - Y2D179 (2025/
11/ 7) Ruby:: Boxへ名前変更 - Y2D227 (2025/
12/ 25) Ruby 4. 0リリース - Y3-D20 (2026/
4/17) オープニングキーノート!!!
床屋談義
田籠さんは、RubyKaigiの楽しみ方についても話しました。
「RubyKaigi 2026にも、皆さんにとってのMultiverse Rubyになるようなトークがあるかもしれません。これから3日間、面白いトークがぎっしり詰まっています。ただ聞いているだけでも楽しいですが、それだけでは届かないものもあります。パーティーやバー、会場のいろいろな場所で、何が面白かったのかを他の人と話してほしいです。その会話の中から、新しいモチベーションが生まれるかもしれません。そのためにRubyKaigiという場があります。参加者が同じ場所に集まり、発表を聞き、話し合うことに意味があります。オーガナイザーとスポンサーには、この場を作ってくれたことに感謝しています」
その後、個人的な話の続きを紹介しました。
3年前のその夜、Namespaceという機能名ではなく、
参加していたSalzbergさんも登壇し、当時のことを振り返りました。
田籠さんは最後に、Ruby::は塩山
The Journey of Box Building
キーノートの前半では、Ruby Boxの基本的な機能と、Ruby VMの中でBoxが実際にどのように識別され動いているのかが技術的に解説されました。後半はどのようにしてRuby Boxが生まれたのかという物語が語られました。
Ruby Boxの仕組みをたどる技術的な旅と、その機能が生まれるまでの旅の両方を描いた、RubyKaigiの開幕にふさわしいキーノートでした。
