RubyKaigi 2026 キーノートレポート

田籠聡さん「Ruby Boxを探す旅」 〜RubyKaigi 2026 1日目キーノート

2026年4月22日(水)から24日(金)まで、北海道函館アリーナ及び函館市民会館でRubyKaigi 2026が開催されました。

今年のRubyKaigiは、Ruby 4.0で試験的に導入されたRuby::Boxを扱うキーノート「The Journey of Box Building」とともに幕を開けました。登壇したのは田籠聡さんです。

キーノートの前半では、Ruby Boxとはなにか、Ruby VMのなかでどのようにしてRuby Boxを見つけるのか、といったことが説明され、後半では、Ruby Boxがどのように生まれたのかという物語が語られました。

本稿では機能名としてはRuby::Boxと表記し、講演全体や概念を示す場合はRuby Boxと表記します。

Ruby::Boxとはなにか?

まず、Ruby::Boxとはなにかについて、これはRuby 4.0に入った新機能のひとつです。Ruby::BoxはかつてNamespaceと呼ばれていたものをRubyKaigi 2025以降に替えたものです。

使いかたとしては、Ruby::Box.newと書いて、RUBY_BOX=1でオンにして使います。Ruby::Boxを使うことでなにが嬉しいのかというと、monkey-patchなどを分離できることです。というのも、Rubyではオープンクラスを採用しているため、いつもどこでもクラスを書きかえることが可能です。この機能を隔離するためにRuby::Boxというもので実現します。

Ruby::Boxがない場合、アプリケーション内applibなどで変更した機能を実行環境上すべてに影響を与えます。その変更を覆い隠す手段としてRuby::Boxがあります。どういうことかというとRubyプロセス内に別々の空間を作り、各空間のみに影響するようにします。

Boxの分離イメージ(出典: Satoshi Tagomori, “The Journey of Box Building”, slide 15)
Box の分離イメージ

ユースケースとしては以下のようなものが考えられます。テスト、同じライブラリの別バージョン、そしてパッケージ機能などがあります。それぞれ見ていきます。

Use Case 1: テスト
想定ユースケースの一つはテストです。この例の場合、アプリケーションコードにモックを入れたいことがあります。手順が大変なのでモンキーパッチで済ませたいことがあると思います。モンキーパッチで対処してしまうと必要な箇所以外にも影響を与えてしまう可能性があります。ここでRuby::Boxを利用することでモンキーパッチの影響をBox内に閉じ込めてしまうことができます。
Use Case 2: ライブラリのバージョンが異なるもの
次に想定しているユースケースはアプリケーションサーバーでライブラリのバージョンが異なるものを試したいときです。v1とv2で非互換のライブラリを同時に動かし、v2で動かない場合はv1に戻すなどが考えられます。
Use Case 3: パッケージング
最後に考えられるユースケースとしてMatzが話しているパッケージングの機能です。この機能はパッケージをBoxの中に読み込み、そのBoxから公開APIだけを取り出すような使いかたです。このユースケースではBoxを使うことでアプリケーションを壊すことが少なくなります。ただしフックポイント、メモリ使用量など考えるべき点はたくさんあります。

Boxを探す旅

Ruby::Boxの構成要素として大きく3つあります。Box毎にクラス、メソッドの定義を持つこと、スクリプトや拡張ライブラリをBoxの中に読み込むこと、最後にいまどのBoxでプログラムが実行されているかを管理、検出することです。

これまでのRubyKaigiでも、各Ruby::Box向けのクラスやメソッド定義はRubyKaigi 2024で、スクリプトやsoファイルなど拡張を読み込むことについてはRubyKaigi 2025でそれぞれ話しています。

今回は、RubyのVMでのRuby::Box実行中のBoxの管理方法について説明しました。

Ruby::Boxはクラスやメソッドの定義を実行プログラム上で分離します。しかしながら、Rubyのプログラムはクラス、メソッドいつでも定義され、どこでも参照されます。Ruby VMは、実行中のコードがどのRuby::Boxの文脈にあるのか、つまりcurrent boxを決定する必要があります。

main.rbの中からapp.rbを読み込む例です。

Boxのサンプルコード app.rbとmain.rb(出典: Satoshi Tagomori, “The Journey of Box Building”, slide 26)
# 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.newでBoxを初期化します。Boxをレシーバーにしてrequire_relativeを呼びます。この例ではb1.require_relative("app")を呼び出し、app.rbb1のなかに読み込みます。このとき、実行されているメインの空間からは直接app.rbに定義されたモジュールAppは見えません。このときApp.fooは、b1の中に読み込まれたAppモジュールを参照して呼び出される必要があります。

Boxの切り替えは、Ruby::Boxの特定のメソッドを呼んだときに発生します。対象になるのはrequirerequire_relativeloadevalです。これらで読み込まれたコードはBoxの中で評価されます。

ファイルとBoxが一対一に決まるわけではないです。同じファイルを別々のBoxに読み込むこともできます。境界はファイルのように見えると思いますが、ファイル自体がBoxを決めるわけではないです。

では、ある場所でcurrent boxが何かをどう調べるのか。普通に例外を出すと、Rubyレベルのbacktraceが得られます。main.rbからrequire_relativeされ、app.rbのトップレベルに入り、モジュール定義の中で例外が起きた、といった情報はわかります。

しかしRuby VMの実装を調べるには、それだけでは情報が足りません。もっと低レベルの実行状態が必要になります。

そこで、開発用にはrb_bugのような仕組みで Rubyをクラッシュさせることにします。クラッシュさせることでcrash reportを見ることができるようにします。ここで通常のRubyには入っていないデバッグ用にrb_bugは自分で定義したものです。このcrash reportにはRubyレベルの backtrace だけでなく、control frameの情報が出ます。

Ruby VMの実行状態を見るうえで重要なのがcontrol frameです。CRubyにはrb_control_frame_tという構造体があります。control frameはスタックになっており、Rubyの実行状態を表します。

Rubyレベルのbacktraceは、このcontrol frameから作られたものです。Cレベルの内部情報や、DUMMY、IFUNCのような内部フレームを隠し、Rubyプログラマが読むための形に変換・フィルタしたものがbacktraceです。

Ruby Boxの実装では、Rubyレベルのbacktraceだけではなく、control frameの中身を見ながら「この時点のcurrent boxは何か」を判断していきます。

サンプルコードでcurrent boxの遷移を見ると、最初にmain.rbが実行されるときはメインのboxの中で動きます。Ruby::Box.newb1を作るところもmain boxの中です。

そのあと、b1.require_relative("app")によってapp.rbを読み込みます。するとapp.rbのトップレベル評価はb1の中で行われます。Appモジュールの定義も、モジュール内のローカル変数の評価も、App.fooのメソッド定義もb1の中で行われます。

メソッドの中身は読み込み時には評価されません。実行するときに、b1::App.fooを呼び出すと、そのメソッドはb1のBoxで定義されたものなので、current boxはb1になり、実行されます。

ここまでが基本的なRuby::Boxの説明でした。次からはそのcurrent boxを探す話をしていきます。

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_CREFSPECVALFLAGSなどが並びます。Rubyのオブジェクトを表すVALUEの並びとして管理されます。

ローカル変数はenvのtail側に並びます。たとえばメソッド引数xとローカル変数cdがあれば、それらがenvの中に格納されます。

control frameはスタックとして積まれます。各control frameはそれぞれenvを参照しています。

CRubyのスタックメモリ空間では、一方の端からcontrol frameが積まれ、反対側の端からenvが積まれます。呼び出しが深くなるにつれて、両者が内側へ伸びていきます。どこかで衝突すると、それがRubyのstack overflowになります。

envのメンバーはep[-1]のような負のインデックスでアクセスされます。これは、control frameとenvがスタック空間の反対側から伸びる構造に由来します。

control frameとenvの関係
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_REQUIRED、ISOLATEDなどがあります。また、GCにこの値をRubyの整数オブジェクトのように扱わせるためのタグも使われます。Ruby VMの内部ではこうしたビット表現が多く使われており、実装を読むうえで重要になります。

frame typeごとに、envの中に入っている値は異なります。METHOD、BLOCK、CLASS、TOP、CFUNC、IFUNC、EVAL、RESCUE、DUMMYといったframe typeがあり、それぞれLOCALかどうか、SPECVALに何が入るか、ME_CREFに何が入るかが違います。

MEはmethod entryです。どの名前で、どのクラスに定義されたメソッドなのか、method definitionへの参照などを持ちます。method definition、つまり rb_method_definition_structには、このメソッドがどのBoxで定義されたかを示す情報を追加できます。

BHはblock handlerです。メソッドに渡されたブロックを実行するために使われます。CREFはClass REFerenceで、定数探索やrefinement、可視性などに関わります。Prev EPは外側のenvへのポインターです。

method definitionにrb_box_t *boxのような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ではありますが、従来のSPECVALME_CREFだけではBoxを直接得られません。

CLASS/TOP frameは、メソッドのようにmethod definitionを持つわけではありません。また、ブロックを受け取ることもありません。そのため、method definitionやblock handlerからBoxをたどる方法が使えません。

Ruby 4.0では、CLASS/TOP frameのSPECVALにBox情報を入れるようにします。これにより、CLASSやTOPのLOCAL envからもBoxを取り出せるようになります。

その結果、current boxを見つける基本ルールはこうなります。まず一番近いRuby frameを探します。必要ならC functionからRuby frameへ戻ります。そこから一番近いLOCALなenvを探します。そして、そのLOCAL envのSPECVALまたはME_CREFからBoxを取ります。

メソッド定義には、そのメソッドが定義されたBoxをmarkできます。クラス定義やトップレベルでメソッド定義が行われる場合には、そのときのCLASS/TOP frameのBoxを使ってmethod definitionをmarkします。

ここで必要になるのがloading boxです。Ruby::Box#require#require_relativeでファイルを読み込むとき、そのファイルのトップレベルframeがpushされます。このTOP frameを「どのBoxに読み込まれたか」でmarkする必要があります。

loading box

つまり、ファイルを読み込むときには、まだTOP frameが積まれる前の段階で、⁠これから読み込む先のBox」を覚えておく必要があります。それがloading boxです。

current boxは、いま実行しているframeから求められるBoxです。一方、loading boxは、これからファイルを読み込むときに使うBoxです。

この違いは、$LOAD_PATHのような読み込み前に必要な情報で重要になります。requireがどのファイルパスを解決するかは、ファイルの評価が始まる前に決めなければなりません。したがって、まだTOP frameがpushされてcurrent boxが切り替わる前に、読み込みに関係するグローバルな状態をloading boxに基づいて切り替える必要があります。

つまり、loading boxはcurrent boxよりも少し早いタイミングで効いてくるBoxです。

loading boxを見つけるために、Ruby::Box#requireなどを呼んだframeにVM_FRAME_FLAG_BOX_REQUIRE、つまりBOX_REQUIREというframe flagを立てます。

loading boxを探すときは、呼び出しstackを逆にたどって、BOX_REQUIREが立っているframeを探します。そのframeがRuby::Box#requireの呼び出しであれば、cfp->selfにレシーバーのRuby::Boxインスタンスが入っています。そこから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::Loaderを使います。Boxを作ったとき、そのBoxの中のObjectBox::Loaderをincludeします。

Boxの中でレシーバーなしのrequireが呼ばれると、まずBox::Loader#requireが呼ばれます。このrequireは自分自身のframeにBOX_REQUIREflagを立て、その後superでRubyGemsのrequireへ処理を渡します。

RubyGemsのrequireを経由してRuby VMのrequireに入ったあとでも、stackをたどってBOX_REQUIREflagを探せば、RubyGemsではなくBox::Loader#requireのframeに到達できます。そこで、このrequireがどのBoxの中から呼ばれたのかを正しく判断できます。

つまりBox内の通常のrequireを補足するために、Box::Loaderが必要になります。

ここまで、current boxをどう見つけるか、loading boxをどうmarkするかを見てきました。

Ruby Boxの実装では、クラスやメソッドをBoxの中で定義する話、Rubyスクリプトや拡張ライブラリをBoxの中に読み込む話、実行中のcurrent boxを見つける話など、Ruby VMのさまざまな場所を触ることになります。

Ruby::Boxへ至る道

ここからは、Ruby::Box、というよりNamespaceが生まれた秘話が語られました。田籠さんは、次のように話し始めました。

「Rubyは好きだけど、これからRubyの開発者になることはないだろうなあ。今のRubyに付け加えたいことないしな、と思っていたんですよ。RubyKaigi 2023のChris Salzberg(@shioyama)さんの発表、Multiverse Rubyを聞くまではね」

"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となります。そこから登壇までの時系列は、以下のとおりです。

  1. D0 (2023/5/12) Salzbergさんのプレゼンに衝撃を受けた日
  2. D11 (2023/5/23) PoCを最初のコミット
  3. D46 (2023/6/27) bugs.ruby-lang.orgに"Namespace on read"として機能提案
  4. D78 (2023/7/29) Namespaceのアイデアを松江RubyKaigiで発表
  5. D166 (2023/10/25) Ruby Grantに採択
  6. Y1D1 (2024/5/13) RubyKaigi 2024 "Namespace, What and Why" を発表
  7. Y1D3 (2024/5/15) MatzがRuby 4.0するためにはNamespaceが必要と宣言
  8. Y2-D26 (2025/4/15) "State of Namespace"をRubyKaigi 2025で発表
  9. Y2-D24 (2025/4/17) Matzが今年(2025年⁠⁠、ZJITと一緒に入れてRuby 4.0をリリース宣言
  10. Y2-D11 (2025/5/1) GitHubにPRを作成
  11. Y2-D1 (2025/5/11) merge
  12. Y2D179 (2025/11/7) Ruby::Boxへ名前変更
  13. Y2D227 (2025/12/25) Ruby 4.0リリース
  14. Y3-D20 (2026/4/17) オープニングキーノート!!!

床屋談義

田籠さんは、RubyKaigiの楽しみ方についても話しました。

「RubyKaigi 2026にも、皆さんにとってのMultiverse Rubyになるようなトークがあるかもしれません。これから3日間、面白いトークがぎっしり詰まっています。ただ聞いているだけでも楽しいですが、それだけでは届かないものもあります。パーティーやバー、会場のいろいろな場所で、何が面白かったのかを他の人と話してほしいです。その会話の中から、新しいモチベーションが生まれるかもしれません。そのためにRubyKaigiという場があります。参加者が同じ場所に集まり、発表を聞き、話し合うことに意味があります。オーガナイザーとスポンサーには、この場を作ってくれたことに感謝しています」

その後、個人的な話の続きを紹介しました。

3年前のその夜、Namespaceという機能名ではなく、⁠箱」という名前にしようという話で盛り上がりました。boxは日本語で箱です。さらに、いつか函館でRubyKaigiをやれば「箱会議」になる、という冗談までありました。最終的には「箱」そのものではなくRuby Boxという名前になりました。それでも、この名前や今日の話は、SalzbergさんのMultiverse Rubyがなければこの形にはなっていなかったと振り返り、Salzbergさんに感謝の言葉を述べました。

参加していたSalzbergさんも登壇し、当時のことを振り返りました。⁠Multiverse RubyとRuby Boxはやり方こそ違いますが、Rubyを直接変えるのは大変だから別の形で何かを作ろうとしていたところから、田籠さんが本当にRuby本体でやるべきだと進めていった」と話し、そのことへの感謝を述べました。

Salzbergさんに感謝している田籠さん
田籠さんとSalzbergさん

田籠さんは最後に、⁠ことはある日突然やってくる! パーティー、バー、どこでも発生する。だから楽しもう。Ruby::Boxは塩山(Chris Salzberg)さんがいなければできていなかった。ありがとう!!!そして今年のRubyKaigiを楽しんでください」という言葉でキーノートを締めくくりました。

The Journey of Box Building

キーノートの前半では、Ruby Boxの基本的な機能と、Ruby VMの中でBoxが実際にどのように識別され動いているのかが技術的に解説されました。後半はどのようにしてRuby Boxが生まれたのかという物語が語られました。

Ruby Boxの仕組みをたどる技術的な旅と、その機能が生まれるまでの旅の両方を描いた、RubyKaigiの開幕にふさわしいキーノートでした。

おすすめ記事

記事・ニュース一覧