Ruby::Box は、1 つのプロセスの中で、クラス・モジュールの定義を分けるための仕組みです。Ruby 4.0 で入った experimental な機能です [Feature #21311]。
(7) Ractor を触ってみよう と並べると分かりやすいかもしれません。
- Ractor は、状態(オブジェクト) を分けます。
- Box は、定義(クラス・モジュール・定数) を分けます。
どちらも experimental で、どちらも「使ってみた人の報告」が一番足りていないものです。
この資料中の実行例は、ruby 4.1.0dev (2026-09-14T04:56:14Z master d568c61094) で確認しています。
Box は、ruby の起動時に環境変数 RUBY_BOX=1 が設定されていないと使えません(プログラムが始まってから設定しても効きません)。
$ RUBY_BOX=1 ./ruby -W:no-experimental box.rb
設定を忘れると、こうなります。
p Ruby::Box.enabled? #=> false
Ruby::Box.new
#=> Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box. (RuntimeError)RUBY_BOX=1 を付けると、起動時に experimental の警告が出ます。-W:no-experimental で消せます。
ruby: warning: Ruby::Box is experimental, and the behavior may change in the future!
See https://docs.ruby-lang.org/en/master/Ruby/Box.html for known issues, etc.
Ruby では、アプリケーションも、読み込んだライブラリも、モンキーパッチも、すべて同じ場所(1 つのクラス・定数の空間) に定義されます。便利な半面、
- 同じ名前のクラスを持つライブラリを、2 つ同時には使えません。
- あるライブラリが当てたモンキーパッチは、アプリケーション全体に効いてしまいます。
- ライブラリ A が依存する gem のバージョンと、ライブラリ B が依存するバージョンが違うと、片方が壊れます。
Box は、読み込んだファイルが定義したクラス・モジュール・定数を、その box の中だけのものにします。
lib_a.rb:
VERSION = "1.0"
class Greeter
def hello = "hello from #{VERSION}"
endmain.rb:
VERSION = "main"
box = Ruby::Box.new
box.require_relative("lib_a")
p VERSION #=> "main"
p box::VERSION #=> "1.0"
p box::Greeter.new.hello #=> "hello from 1.0"$ RUBY_BOX=1 ./ruby -W:no-experimental main.rb
"main"
"1.0"
"hello from 1.0"
Ruby::Box.newで box を作り、box.require/box.require_relative/box.loadでファイルを読み込みます。読み込んだファイルがさらにrequireしたものも、同じ box に入ります。- box の中の定数やクラスは、
box::VERSION、box::Greeterのように box 越しに参照します。 - box の中で動くメソッドは、box の中の定義を見ます(
Greeter#helloが見ているVERSIONは"1.0"のほうです)。
box.eval を使うと、ファイルを作らずに box の中でコードを実行できます。
box = Ruby::Box.new
box.eval(<<~RUBY)
class String
def shout = upcase + "!"
end
RUBY
p box.eval(%q{"hi".shout}) #=> "HI!"
p "hi".respond_to?(:shout) #=> false
"hi".shout #=> undefined method 'shout' for an instance of String (NoMethodError)String への変更が、box の外に漏れていません。「このライブラリのモンキーパッチだけ閉じ込めたい」がそのまま書けます。
Box が一番効くのはこれです。互換性のない 2 つのバージョンを、1 つのプロセスで同時に使ってみましょう。
v1/lib.rb:
class Lib
VERSION = "1.0"
def self.calc(n) = n * 2
endv2/lib.rb:
class Lib
VERSION = "2.0"
def self.calc(n) = n * 3 # 非互換な変更
endtwo.rb:
b1 = Ruby::Box.new
b2 = Ruby::Box.new
b1.require_relative("v1/lib")
b2.require_relative("v2/lib")
p [b1::Lib::VERSION, b1::Lib.calc(10)] #=> ["1.0", 20]
p [b2::Lib::VERSION, b2::Lib.calc(10)] #=> ["2.0", 30]
p b1::Lib.equal?(b2::Lib) #=> false (別のクラスです)
p defined?(Lib) #=> nil (外には定義されていません)同じ名前の Lib クラスが 2 つ、別物として共存しています。box の外には Lib がそもそも存在しません。
書くときに意識するのは 2 つだけです。
- main:
ruby foo.rbのfoo.rbが動く box。起動時に自動で作られます。 - optional:
Ruby::Box.newで作る box。main と技術的には同じものです。
p Ruby::Box.enabled? #=> true
p Ruby::Box.current #=> #<Ruby::Box:3,user,main>
p Ruby::Box.current.main? #=> true
p Ruby::Box.new.main? #=> falsebox.inspect の末尾(#<Ruby::Box:4,user,optional> の optional の部分)が、その box の種類です。
Note: 実装にはもう 2 つ、root(組み込みのクラス・モジュールが定義され、動く場所。プロセスに 1 つ)と master(すべての box の元になる「原本」。ここではコードは動きません)があります。使う側が気にすることは、まずありません。
Ruby::Box のメソッドは、これだけです。
- クラスメソッド:
enabled?、current、main、root、master - インスタンスメソッド:
require、require_relative、load、eval、load_path、main?、root?、master?、inspect
box.load_path は、その box 用の $LOAD_PATH です(外側の $LOAD_PATH とは別のオブジェクトです)。
experimental なので、まだ動かないところがあります。doc/language/box.md の "Known issues" に挙がっているのは、
RUBY_BOX=1が要る(デフォルトでは無効)RUBY_BOX=1のもとでは、extconf.rbが stack level too deep になって native 拡張のインストールが失敗することがあるrequire 'active_support/core_ext'が失敗することがある- box の中で定義したメソッドが、Ruby で書かれた組み込みメソッドから見えないことがある
手元で試して分かったものも 1 つ。非メイン Ractor の中では box を作れません。
Ractor.new { Ruby::Box.new }.value
#=> can not set constants of classes/modules created by another Ractor (Ractor::IsolationError)一方、メインの Ractor で作った box を Ractor に渡して使うことはできます。
box = Ruby::Box.new
box.eval("class Worker; def self.work(n) = n * 2; end")
r = Ractor.new(box) {|box| box::Worker.work(21) }
p r.value #=> 42この種の「どこまで動くか」は、試した人にしか分かりません。動かないものを見つけたら、それは報告する価値があります。
| ファイル | 中身 |
|---|---|
box.c |
Box の本体(1300 行ほど)。Ruby::Box のメソッドはここです |
internal/box.h |
データ構造の定義 |
load.c |
require / load を box ごとに行うための対応 |
variable.c, class.c, vm.c, proc.c |
定数の参照、クラス定義、メソッド探索の側の対応 |
test/ruby/test_box.rb |
テスト(1500 行ほど)。いま何がどこまで動くのか、一番正確に書いてある場所です |
doc/language/box.md |
設計ドキュメント。Known issues と TODO も載っています |
- 自分が普段使っている gem を box に入れて
requireしてみる。動いたら儲けもの、動かなかったら何が起きたかを報告する(いまは、これが一番価値のある貢献です)。 doc/language/box.mdの TODO を 1 つ選んで手を付けてみる。test/ruby/test_box.rbを読んで、書かれていない挙動を見つけ、テストを足す。- Box と Ractor を組み合わせて、どこまでできるか探る。「box ごとに Ractor を立てる」は書けるでしょうか。