Ruby拡張ライブラリとは,C言語で書く Ruby のためのライブラリである.最近は Ruby 自体も速くなってきており,速度面でのメリットは低下してきているが,C言語で提供されているライブラリのラッパーを作るなど,拡張ライブラリならではの使いみちはまだまだある.ここでは,私が自作拡張ライブラリを作るにあたって経験したことなどを備忘録としてまとめる.なお,以下の内容は Ruby 2.3 を対象としており,より新しいバージョンでは事情が異なる場合がある.
まずは C,Ruby 双方の言語をそれなりに扱える必要がある.具体的には RubyExtensionProgrammingGuide の「対象とする読者」程度のレベルは必須である.それ以下の場合,各言語,あるいはプログラミングそのものを別に学ぶ必要がある.
拡張ライブラリを初めて書こうという場合には,まず RubyExtensionProgrammingGuide を読むのがよいだろう(ただし,やや古い記事であることには注意を要する).追加の情報源として,ruby のソースに含まれる README.EXT.ja や リファレンスマニュアルの C API 一覧 が役に立つ.ただし C API 一覧の作りこみはかなり甘く,一部間違いが含まれているなど,信用ならないところがある.実際に C レベルの組み込みライブラリの機能を利用する場合,ruby のソースコードそのものを確認したほうが安全である.ソースコードは,当然ながら ruby そのものに関する深く濃密な情報を含んでおり,ruby の動作の理解や,拡張ライブラリ作成のために大いに参考になる.また,拡張ライブラリを書くにあたって避けては通れない GC の理解の一助として RubyとPythonにおけるガベージコレクションの視覚化 なども読んでおくとよいかな.
C は,当然ながら Ruby より低級な言語であるため,簡単に SEGV を引き起こすことができる.また,拡張ライブラリ特有の問題として,意図せず GC に回収されてしまった VALUE
を操作しようとして同様に ruby をクラッシュさせてしまうこともある.このようなとき,ruby は「[BUG]」で始まるエラーメッセージを表示して異常終了する.通常であれば gdb などを使ってデバッグするところであるが,Cygwin 上で開発している場合,gdb が読めるコアを吐いてくれないという問題がある.そんなときに便利な gem が segv-handler-gdb である.ちなみに ruby 自体をデバッグオプション付きでビルドするには configure
時に ./configure optflags="-O0" debugflags="-g3"
とすればよい.バグが拡張ライブラリにあっても,クラッシュ箇所は ruby 本体側だったりするので,[BUG] に対するデバッグにはこの方法でビルドした ruby を使うとよいだろう.
ruby の GC は任意のタイミングで発生し,適切に保護されていない VALUE
を回収したり,適切に初期化されていない VALUE
を参照したりすることがある.
初期化への対策としては,VALUE
を保持する領域の確保には ZALLOC()
や ZALLOC_N()
,あるいは xcalloc()
を使うのがよいと思われる.これらは確保した領域を0クリアするため,未初期化 VALUE
を参照しようとすることがなくなる.ruby では即値である Qfalse
が (VALUE) 0
であるため,ビットレベルの 0 クリアで安全に初期化できる.
VALUE
の適切な保護を簡略化するため,あるいは一時的な VALUE
が大量に発生し,いちいち GC が走っては重くなってしまう場合への対策として,一時的に GC を禁止する方法がある.C では rb_gc_disable()
が Ruby における GC.disable
の実体であり,これによって GC を禁止できる.いつまでも GC を禁止したままだとメモリリークを起こしてしまうので,あとで rb_gc_enable()
で禁止を解除する必要があるが,途中で例外が発生すると適切に禁止を解除できないおそれがある.その対策として,Ruby の ensure
を使う方法があり,C では rb_ensure()
を使うことができる.
GC のマークフェイズにおいてどこからも参照されていないことが判明し,解放されることが決定されたオブジェクトに対して,ObjectSpace.define_finalizer
でセットした Proc の実行および free 用関数の実行は即座には行われない.これはGC のために Ruby の実行を止める時間を短くし,必ずしも同期する必要のない処理を非同期化するためのものだと思われる.free 用関数で行う free 以外の後始末処理や,ファイナライザの処理が他に影響する場合,これらがいつ実行されるのかわからないことに十分留意する必要がある.これらの後始末処理は,GC.disable
中にも発生しうる(GC.disable
する前に走った GC の後始末が,GC.disable
したあと,GC.enable
する前に発生することがある)ことに特に注意が必要だ.
mkmf は,サブディレクトリに配置されたソースコードのコンパイルおよびリンクはしてくれない.その他にも,かゆいところに手が届かない部分が多い.そういう部分は,create_makefile()
したあと,生成された Makefile を直接書き換えてしまえば良い.幸い Ruby は Makefile を編集するようなちょっとした処理に向いた言語である.
GC.stress=true
上記の [BUG] や,[BUG] すら表示されずに落ちるという深刻なバグの発生タイミングは GC に依存することが多い.GC がいつ走るかの予測は難しく,再現困難な状況になる場合がある.そんなとき,Ruby スクリプト上で GC.stress=true
とすると,GC が実行可能なすべてのタイミングで GC が走り,GC を発生タイミングとするバグの再現性が向上する.特定のタイミングで GC が走ることによって発生するバグの検出のためにも,テスト段階では GC.stress=true
としておくのがよいだろう.
まずは多くの警告をオンにする -Wall
と -Wextra
の二種は必須だろう.これに加えて -Winit-self
と -Wshadow
はオンにしたほうがよいだろう.前者は int i=i;
のような初期化に対して警告するもので,後者はシャドウイング,すなわち int i; { int i; }
のようにブロックや関数定義の内側のローカル変数が外側のローカル変数やグローバル変数を隠蔽するときに警告するものである.
ruby は通常 gcc でビルドされるが,拡張ライブラリのビルドにおいては clang の使用も検討すると良いだろう.関数型マクロまわりでコンパイルエラーが発生した際,gcc ではマクロ定義箇所の行番号しか示されず,バグの特定が困難であるのに対して,clang では呼び出し元の行番号も示され,バグの特定に大いに役立つ.