CライブラリとRubyの不整合について

昨日、SDL2のRubyバインダを作ろうとして挫折したので、何が問題になったのかをメモ。
結論から言うと、SDL2のオブジェクトの寿命管理がC用の考え方で作られていて、Rubyで扱うためにRubyオブジェクト化した場合にGCによる解放処理との整合性が取れない。SDL2のオブジェクトの解放メソッドをRubyのメソッドとして実装してプログラマに確実にやらせるのはRubyっぽくないし、解放せずにGCに回収された場合にどうすんの?って話にもなるし、無理矢理Rubyっぽく実装するとSDLの内部実装に依存した形になってしまうのでよろしくない。
まあ、たぶん作ること自体はできるけど、SDL2そのもののストレートなバインダを作るには、必ずどこか強引な部分が出てくる。それが納得いかない、というわけだ。頭のいい人ならうまい解決策を見つけられるかもしれんので、誰かが作ってくれることを期待しよう。
以下詳細。

SDL2のオブジェクトとRubyオブジェクト

SDL2はC用のライブラリなので構成は関数単位である。SDLオブジェクトは構造体で表現されて、SDL_Createなんちゃらとか、SDL_Getなんちゃらという関数を呼び出すと、SDLオブジェクトのポインタが返ってくる。使わなくなったSDLオブジェクトはSDL_Destroyなんちゃらとか、SDL_Freeなんちゃらという関数で解放してやることになるが、モノによっては自動的に解放してくれることもあり、そういうのは自分で解放しようとするとエラーになる。ていうか一度間違えて解放したときにはセグフォでコケた。
Cの場合は保持すべきものはプログラマが保持して、解放すべきときにプログラマが解放する、というのが原則であり、それを守らなければメモリリークしたりクラッシュしてもよい。そういうもんである。WindowsXP使ってたときはDirectXのTextureをロックして書き込むときにポインタの計算間違えたらブルースクリーンでOSごとコケたりしていた。そういうもんである。
RubyではGCがメモリ管理をしてくれるので、使うぶんにメモリ解放などを意識することは無い。拡張ライブラリ作者はGCを強く意識しないといろいろとまずいことが起こる。外部リソースに関しては、GC任せにするといつ解放されるかわからないので、ユーザが解放できるようなメソッドを用意しておくのが望ましいが、解放しなかったからといってGC回収時にも放置されてリークするとか、ユーザが解放してからそのオブジェクトを使うとクラッシュするとか、そういうことが起こらないようにしておきたい。RubyはCと違って安全で安心な言語なので、拡張ライブラリを作る場合もそれを崩さないようにするのが大事である。

SDL2バインダをどう作るか

もっとも簡単に作るなら、SDLオブジェクトのポインタをそのまま数字で返して、たとえば「SDL_Windowを表すハンドルです」とか言っておけば、関数そのままメソッドとして実装するだけでいける。しかしこれだとRubyで書くプログラムなのにCと同レベルの厳密な管理が必要になってしまってあんまり嬉しくない。ぜんぜん安全でも安心でもない。Rubyぽくもない。
RubyからSDL2を使えるようにするんだから、SDLオブジェクトをRubyオブジェクトとしてラッピングしてしまえば、機能単位でまとまるし、扱いやすくなる。SDLオブジェクトのポインタを持つRubyオブジェクトを作って、クラスを設計すればいけるんじゃないか。うまいこと作ればRubyぽい安全、安心なSDL2ライブラリが作れる。
とまあ、そんなことを考えて作り始めたのだな。

SDL2のWindowとRenderer

SDL2は複数のウィンドウに対応するので、SDL_Windowというオブジェクトが頭に存在する。これに対し、従来の描画方法であるWindowSurfaceを使うやり方と、3Dデバイスで描画するRendererを使うやり方が提供されている。SDL2を使うならRendererを使わなければ価値半減。
SDL_RendererはSDL_Windowに対して1個生成できる。生成するにはSDL_CreateRenderer()を使う。生成した後はSDL_GetRenderer()で再取得できる。どっちもSDL_Rendererのポインタが返ってくる。画像データであるSDL_TextureはSDL_CreateTexture()やSDL_CreateTextureFromSurface()などを使って生成する。Rendererを作るにはWindowが必要だし、Textureを作るにはRendererが必要である。Window:Renderer:Textureは1:1:nの関係となる。
では、これらをRuby用にしようとするとどうなるだろう。まあ、普通に考えたらSDL::Windowクラスと、SDL::Rendererクラスと、SDL::Textureクラスを作ることになる。ところが、だ。このWindowとRendererの関係をどうするかで悩むことになるのだ。

SDLオブジェクトとRubyオブジェクトの不整合

SDL2バインダなので、SDL2で実装されている関数はほぼそのまますべて実装したい。ただ、使い勝手はRubyぽくしたい。このワガママがすべての原因だった。
順番に考えてみよう。SDL_WindowからRendererを生成するにはSDL_CreateRenderer()を呼ぶ。引数はSDL_Windowのポインタで、返ってくるのはSDL_Rendererのポインタである。RubyレベルではSDL::Window#create_rendererでSDL::Rendererオブジェクトを返すようになる。このSDL::Rendererはその場で作られるので新しいRubyオブジェクトであり、保持するSDL_Rendererも新しいRendererである。SDL_CreateRendererを2回呼ぶとエラーになるので、ここまでは問題ない。
生成したSDL_RendererをSDL_Windowから再取得する場合はSDL_GetRenderer()を呼ぶことになっている。SDL_GetRendererのRuby版を実装しよう。SDL::Window#get_rendererである。こいつは何を返すのか。そりゃまあ、SDL::Rendererオブジェクトなんだろうけど、新しいRubyオブジェクトを作るのか?SDL_Rendererは生成したときと同じポインタが返ってくるはずだから、同じSDL_Rendererを参照するRubyオブジェクトが毎回生成されるのか?同じオブジェクトが返ってきたほうがいいんじゃないか?でももしSDL_GetRenderer()で返ってきたポインタが違った場合、SDL::Rendererオブジェクトが保持するポインタを書き換えるのか?それとも新しいオブジェクトを作るのか?
毎回違うRubyオブジェクトを生成する場合、SDL_DestroyRenderer()をどれかで呼んだら全部が死ななければならない。SDL_Renderer構造体は解放されるわけだから、それを保持したRubyオブジェクトはアクセスしたらコケる。該当するSDL_Rendererを保持するRubyオブジェクトをどうやって見つけるのか。SDL_Rendererを直接持たず、もう一段データ構造を挟んで対応するか?どれか1つがGCに回収される場合は?SDL_Rendererが解放されたらまずい。でも全部回収されたらSDL_Rendererを解放してやらないといけない。全部ってどう認識するのか。挟んだデータ構造をリファレンスカウントで管理するのか。ややこしくなってきた。
じゃあ毎回同じRubyオブジェクトを返すようにするとどうなるかと言うと、SDL::WindowオブジェクトがSDL_RendererポインタとSDL::Rendererオブジェクトを保持することになる。この場合、SDL::Windowの参照が切られてSDL::Rendererだけ生き残るというパターンが発生するとまずいので、SDL::RendererにはSDL::Windowへの参照を持たせる必要がある。両方の参照が無くなった場合にのみGCで回収されるわけだ。解放しないで回収されたとき、Sweep処理の中で解放処理が行われるべきである。ところが、SDL::WindowとSDL::Rendererの回収処理がどっちが先に行われるかがわからない。SDL::Windowが先に回収されてしまうと、SDL::Renderer回収時にSDL_Windowが無いことになるので動きが怪しい。とすると、SDL_RendererはSDL::Rendererが保持するのではなく、SDL::Windowが保持しておいて、解放時にSDL_Windowの前にSDL_Renderererを処理することになって、RubySDL::RendererってSDL_Rendererを持ってないけど実際何なの?って話になる。
泥沼である。

結局のところ

SDL2をそのままの使用感で、Rubyオブジェクトにシンプルにラッピングしようという考えが根本的に間違っているのではないか。SDL2はオブジェクトの関連と寿命管理、解放タイミングがちょっと複雑で、それをそのままCから見たのと同じようにRubyから見えるようにしつつ全自動で安全に処理する、という発想は、言語の特性の差のぶんだけ実装が難しくなるのではないか。などということを考えている。
ぶっちゃけ、SDL_GetRenderer()を実装しなければこの問題はばっさりと消えてなくなるわけで、他に似たような問題があっても、SDL2の全関数を実装するという目標さえ持たなければどうにでもなる。ただ、SDL2の関数を取捨選択して再構成するのは、SDL2のRubyバインダを作っているというよりも、SDL2の機能を使う別の何かを作っていると言ったほうが近いだろう。SDL2なのにSDL2のマニュアルを見てもいまいち役に立たないわけだから。

おしまい

まあ、とりあえず俺には無理だったということで、圧倒的な挫折感である。絶望した。もぅマヂ無理。いつまで待ってもSDL2のRubyバインダが出ないなあーって思ってたけど、そういうことなのかね。
俺がSDL2をいじってる目的は「SDL2を使ったDXRuby互換ライブラリの可能性を探る」であり、SDL2のRubyバインダ開発ではない。それの上に構築できないかなーと思ってチャレンジしてみた。これができないとなると、直接Cで書き上げるか、別の何かを作ってその上に構築するか、という感じになってくるが、わざわざ別のもん作って挟む意味もないかもしれない。
ただ、SDL2はSDL2のいいところがあって、それを殺したDXRuby互換ライブラリというのもちょっとさびしいものがあって、SDL2のいいところを活用するライブラリを作って、その上にDXRuby互換機能を追加する、というほうがいろいろ便利なのかなーとか、なんかこう、うまい何かは無いものか、ということを考えているのである。
なんにせよ、何かしらの成果物が出てくるのはまだまだ先なんじゃなかろうか。SDL2は興味深いのでしばらくこれで遊ぶことにはなると思う。