こんにちは、2023年に新卒で入社し、クラウドハウス採用事業部でバックエンドエンジニアをしている上野(izumitomo)です。最近ではスクラムマスターとして働いています。
本記事では、RubyKaigi 2024の1日目のSatoshi Tagomori(@tagomoris)さんによるセッション『Namespace, What and Why』について紹介させていただきます。
Namespaceとは
本セッションはNamespaceという、Rubyのコードを隔離された別々の空間に分ける開発中の機能がテーマです。 ここにおけるNamespaceとは、アプリケーションやライブラリを隔離された空間上で読み込むものです。
Namespaceの実現のためには以下の3つの処理が必要となります。
- ある空間でアプリケーション・ライブラリを読み込む
- 読み込んだことによる影響を他の空間から隔離する
- その空間で定義されたメソッドを外の空間から呼び出す
Namespaceは以下の3つの衝突を防ぐことをモチベーションとして作られています。
- 名前の衝突
- UserやConfigurationなど、すでに使われている名前を使うことはできない。
- 定義の衝突
- モンキーパッチがプロセス全体に影響してしまう。
- バージョンの衝突
- 1つのアプリケーションで異なるバージョンのライブラリを読み込むことはできない
また、Namespaceはアプリケーションやライブラリを読み込むときにNamespaceにおける振る舞いを記述します。これにより今まで公開されているgemをそのままNamespace内で使うことが可能となります。
セッション中に行われたデモでは、以下のような手順でNamespaceを使ったサンプルコードを動かしていました。
- 依存しているgemのバージョンや定義されたメソッドが異なるAppAとAppBを用意する。
- AppBのプログラム内でNamespaceを作り、その中でAppAを読み込む。
- Namespaceの中と外でそれぞれ、依存しているgemのバージョンやメソッドのアクセス可否を出力させる処理を実行する。
- Namespaceの中ではAppAが依存しているgemのバージョンとメソッドのアクセス可否が出力され、Namespaceの外ではAppBが依存しているgemのバージョンとメソッドのアクセス可否が出力されていることを確認する。
なお、プログラム実行時にはたま〜にSEGVしてしまうらしく、実演時には幸運にもその貴重な例をお目にかかることができましたが、2回目で成功した時には会場は歓声と拍手に包まれていました!
会場の熱気が伝わらないのが惜しい……!!
Namespaceの実装
Namespace下のクラス/モジュールの定義
Namespaceはモジュールのサブクラスとして実装されています。 そのため従来のモジュールで名前空間を切るような直感的なイメージで使うことができそうですね。
Namespace下のクラス/モジュールの定義は、内部的にはKernel#load
が使われています。Ruby3.1で新たにloadメソッドの第2引数に任意のモジュールを渡せるようになっており、これを利用することでNamespaceごとにクラスやモジュールを定義しています。
以下のサンプルコードを読むと、ざっくりとKernel#load
の挙動が掴めるのではないでしょうか。script.rbをMyModuleの名前空間の下に定義しています。
# script.rb # メソッドを定義 def hello puts "Hello from script.rb!" end
# main.rb # モジュールを定義 module MyModule; end # MyModuleをincludeしても。helloメソッドはMyModule内で未定義 include MyModule hello # => => NameError: undefined local variable or method `hello' for main:Object # script.rbをMyModuleの名前空間下でロードされ、MyMmodule#helloが定義される load 'script.rb', MyModule hello # => => NameError: undefined local variable or method `hello' for main:Object include MyModule hello # => Hello from script.rb!
また、Namespaceの実装ではCのネイティブライブラリに手を加えており、クラス定義時などで呼ばれる内部APIのclass_alloc
の振る舞いを変えています。
具体的にはclass_allocがNamespaceの中で呼ばれたときには、Namespaceの中でクラスを作るように書き換えることで、Namespaceの機能を実現しているようです。
ネイティブライブラリのロード
アプリケーションに使われるgemでは、共通して使用しているCのネイティブライブラリがありますが、Namespaceを導入すると、Namespace間で同じネイティブライブラリを複数回読み込むことになります。
通常、ネイティブライブラリの読み込み時にはdlopenという関数が呼ばれますが、すでに読み込んだことのあるライブラリに対して再度dlopenが実行されると、読み込みの処理をスキップします。
Namespaceを導入する場合、Ruby上では隔離された空間上でそれぞれ共通のネイティブライブラリを読み込みます。この時、2回目以降のdlopenについてもライブラリの読み込みをスキップせずに行う必要があります。
そこで、ネイティブライブラリを読み込む際に、プロセスIDとNamespaceのオブジェクトIDをプレフィックスとして新たにファイルを作り、ライブラリの中身をそのファイルにコピーするように工夫しています。
そうすることで、異なる空間上で同一のネイティブライブラリを参照する際に別々のネイティブライブラリとして扱われるため、読み込みのスキップを防ぐことができます。
他にも、ネイティブライブラリを複数回読み込む際に、シンボルの衝突を回避するためのオプションも指定できるようになっています。
Namespace内の組み込みクラス/モジュールの扱い
モンキーパッチでは新しいクラス/モジュールを定義する場合だけではなく、組み込みクラス/モジュールのオーバーライドも可能です。しかし、Namespaceの中で組み込みクラスに対する修正はそのNamespaceの外では隠蔽される必要があります。
そこで、Namespaceではrb_classext_tと呼ばれるRubyの内部実装におけるクラスやモジュールの追加情報を格納するためのコンテクスト(Cの構造体)をNamespaceごとに管理することで、これを実現しています。
このコンテクストにはメソッドや定数、インスタンス変数、サブクラスなどの情報が含まれており、Namespaceごとにコンテクストを管理し、実行時にはそのNamespaceのコンテクストにスイッチさせていくことで、同名のクラスでありながらそれぞれのクラス固有の情報を参照できます。
コンテクストはコピーオンライトと呼ばれる方式で管理され、複製を要求されても実際には複製を実行せず、複製元や複製先の値が変更される際に初めて複製を行うようにしています。
例えば、Namespace内でRubyの組み込みクラスであるStringクラスを使う際、Stringクラスに変更を加えない限り、Namespace内で参照するStringクラスは組み込みのStringクラスの定義を参照します。 そして、Namespace内でStringクラスに新たにメソッドの追加など、クラスの情報を変更したときに初めて組み込みクラスのStringクラスのコンテクストをNamespace内に複製し、クラスに対する変更を複製先のコンテクストに反映します。
複製の回数を抑えることでメモリのオーバーヘッドを削減しつつ、Namespace内の組み込みクラス/モジュールのオーバーライドも可能となっています。
Namespaceが実現する未来
セッションの最後には、Namespaceによってもたらされる(かもしれない)未来の話をされていました。Namespaceの活用によって、以下のような事を実現できる可能性があるとのことです。
- アプリケーションごとにNamespaceを付与することで、1つのアプリケーションサーバで複数のアプリケーションをコンテナなどを使わずに一つのプロセスで動かす。
- gemの依存性の解決に失敗した時に、Namespaceを活用して依存性の問題を自動的に解決する。
- 全てのライブラリを個別にNamespaceで管理することで、ライブラリ同士の衝突を防ぐ
Namespaceはまだ基本的な機能しかなく、これらはまだアイデアベースでまだ具体的な実装はない状態とのことですが、実現されればRubyにどんな変化が訪れるのか……非常に楽しみですね!
感想
実際にセッションを聞く前までは、言語仕様を固めるような初期段階じゃないと、Namespaceのようなアプローチを取り入れるのはなかなか難しいのかなと思っていました。Javaなどの他の言語にはNamespace相当のものがありますが、Rubyの簡単にオープンクラスを作れる柔軟な言語特性のトレードオフで、どうしてもNamespaceのような概念は相性が悪いように見えました。
ですが、Rubyの言語特性の良さを残しながら導入するというNamespaceのアプローチはとても魅力的で、何よりそのチャレンジングな試みに会場も大盛り上がりでした!
その証拠に、セッションが終わった後にスピーカーのtagomorisさんの元に多くの人が訪れて、意見交換が行われていました。
個人的には、Namespaceの概念を考える上で、glibcなどのネイティブライブラリのバージョン管理はNamespaceの中でどのように扱われるについて気になりました。
1日目のセッション後のOfficial Partyでtagomorisさんとお会いできたので上記の疑問を質問したところ、Namespace間におけるネイティブライブラリの分離可否、その方針についてはまだ未定とおっしゃっていました。余談ですが、自分のような知識に乏しい駆け出しエンジニアでも、フランクに聞きやすい空気感が醸成されているのが、RubyKaigiの良さの一つだと感じました。
最後に、tagomorisさん、素晴らしいセッションをありがとうございました!
Official Partyのツーショット写真も載せて、今回のブログを締めさせていただきます。 酔っ払っていたせいか画質が……。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。 jp.techouse.com