Rubyとnamespaceと拡張ライブラリについて

2020年代はモノリスの時代

ここ10年ほど流行っていたマイクロサービスについての理解が深まり、その限界が広く知られるようになってきた。ShopifyのModular Monolithについての記事などは代表例だろう。並行して必要性が叫ばれるようになってきたのがnamespaceだ。RubyKaigi 2023ではshioyamaさんがMultiverse Rubyと題して発表をしていたし、その後tagomorisさんも記事を書いている

Rubyでは以前からnamespaceへの議論が行われてはいたのだが、Ruby 2.0の頃の議論では主にMonkey Patchingによる副作用を局所化するためのものだった。当時の議論とはライブラリ読み込みの局所化という課題が挙がっている点が異なっており、それに伴い技術的な困難も異なる。上述の記事でもそれらの多くは整理されているのだが、この記事ではまだ議論の深まっていない点について考えてみる。

拡張ライブラリ

Rubyにnamespaceを導入するに当たって課題となるのが、拡張ライブラリだ。何が問題なのかを理解するには、まず「拡張ライブラリ」とは一体何なのかを改めて考える必要がある。

拡張ライブラリはつまるところ、DLL(Dynamically Link Library、動的リンクライブラリ、共有ライブラリとも言う)である。Rubyの拡張ライブラリは Init_{ライブラリ名} (foo.so ならば Init_foo という名前)という関数を持っており、この関数でクラスやモジュール、メソッドなどを定義する。Rubyで require "foo.so" などとした場合、Rubyは dlopen(3) を用いてこのDLLを開き、 Init_foo を呼ぶことでクラスやメソッドなどが実際に定義される。

個々で発生する問題は主に以下の三つとなる * Rubyでそのライブラリを使う際のクラス名・モジュール名 * そのライブラリや依存先ライブラリのシンボル名の衝突 * そのライブラリ内の静的変数

読み込み先のクラス・モジュール名

拡張ライブラリ内でクラスやモジュールを定義する場合、通常は rb_define_module("Foo") などと定義する。この場合、 Foo は Object の下に定義される。

これは、namespace内では rb_define_module_under(ns, "Foo") などと読み替えるような仕組みを入れればよいだろう。

シンボル解決

拡張ライブラリが他のライブラリのシンボル、具体的には関数や変数を用いている場合、名前が衝突してしまうことがある。あるライブラリの異なるバージョンが持つシンボルは同じことが多いだろう。つまりシンボル名が衝突してしまう。 Linuxにはこのような状況を避けるための dlmopen(3) という関数があるがポータブルではない。 多少の副作用はあるが、おそらく解決策は dlopen への引数に RTLD_GLOBAL ではなく RTLD_LOCAL を指定することと思われる。この場合、他の拡張ライブラリからこの拡張ライブラリのシンボルが見えなくなる。具体例だと libv8.gem のようなgemがこれにあたる。このような治安の悪いgemはそこまで数は多くないとは思うが…。

静的変数

DLL内の静的変数の値が変更できる場合、あるnamespace内での変更が別のnamespaceに波及してしまう。例えば、morisさんの挙げているojの設定の例では、設定情報はライブラリ内の静的変数に保存されている。 これを対策しようと思う場合、同一のgemを複数回メモリ上の別の場所にロードする必要があるように思われる。素直にdlopenを使うとそれはできないので、別の場所にDLLをコピーするなどの対策をとることになるが、倍のメモリを消費することになるので副作用が大きい。 クラス変数なりRuby側で管理している領域に保存するように、gem自体を変更してもらうようにするのが無難かもしれない。

まとめ

拡張ライブラリのnamespace対応における問題は、Ruby側の問題と、DLL側の問題に分類でき、後者の制約は突破することが難しい。その制約の中でもそれなりに一定の機能を持ったものは出来そうなことをまとめました。