こんにちは、サーバーサイドエンジニアといいつつも最近は React をいじっていることが多い矢崎(id:Saiya)です。
去年あたりに自宅用に作っている Rails のアプリケーションから、libgd *1 の提供する C 言語向けの API を呼び出すコードを導入していたのですが、その際に使った RubyInline gem が便利だったので、(主に自分への備忘録を兼ねて)使う上での留意点や tips などを書いておきます *2。公式のサンプルにあるような簡単な利用例ではあまり迷うことはないのですが、多少凝った用途で使う上でハマった点もありましたため、備忘とご共有までに。
なお、この記事は エムスリーアドベントカレンダー 7 日目の記事です、他の記事もぜひご覧くださいませ。
RubyInline gem とは
C 言語のソースの文字列を inline
メソッドに渡すとコンパイルされ、Ruby のメソッドとして呼び出せるという便利な gem です。
以下に引用する 公式のサンプルコード を見れば雰囲気がわかるかと思います:
require "inline" class MyTest inline do |builder| builder.c " long factorial(int max) { int i=max, result=1; while (i >= 2) { result *= i--; } return result; }" end end factorial_5 = MyTest.new.factorial 5
C 拡張をしたいがために自前の gem
を作ったりする必要もなく、手軽に C のコードを Ruby から呼び出すことが可能な点が大変魅力的です。性能のためだけでなく、Ruby から呼び出せないライブラリ等を使う上でも有用な選択肢であると思います。
また、Ruby アプリケーションの起動時に C のコードをコンパイルすると突飛に聞こえるかもしれませんが、Ruby 2.6 から入った JITもまさにそのような仕組みであり、MRI/CRuby においてはそのようなアプローチは自然なものなのではないかと思います。
MRI/CRuby の C 拡張
MRI/CRuby は C 拡張の書きやすさ・保守しやすさ*3を常に意識しており、Ruby と C 言語の基礎知識があれば MRI/CRuby の C 拡張を書くことは難しくありません。
また 公式のドキュメント の情報量も拡充されてきており、各種 Google 情報や適当な C 拡張の gem の中身などと併せて見ることで必要な知識は十分得られることでしょう。
それほど大規模でない C 拡張の場合、主に以下の 3 つの道具の存在を知っていればそれっぽく実装するには十分そうでした:
VALUE
型と C 言語の型(int
等)を相互変換するマクロ関数たち (INT2NUM
,NUM2INT
とか)VALUE
は Ruby のオブジェクト全般を表現する C の struct *4
- TypedData 型
- C の struct を Ruby のオプジェクトにラップする際にとても便利な仕組み
rb_define_
で始まる関数群- Ruby で定義・宣言できるものは、それが何であれ C 拡張からも定義・宣言できる
このように MRI/CRuby の C 拡張のコードそのものを書くことは難しくないのですが、実際に利用するためには make するための諸々の整備(多くの場合は自作 gem としてラップすることでしょう)が必要になるため、開発やデプロイに一手間かかる面はあります。
RubyInline の動作原理
公式のサンプルコード にあるように class
の中で inline
メソッドを呼び出すと、その際に渡した C 言語のソースがコンパイルされ、当該 class
のメソッドとして呼び出せるようになります。なので C 言語のコードのビルドやデプロイの手間が大幅に軽減されます。
ソースコード をざっと見たところ、内部的には以下のような動きをしているようです:
inline
関数の呼び出し元のファイル(class
を定義しているファイル)の更新日時・ハッシュ値を元に、キャッシュ用のハッシュ値を算出 *5- 上記で算出したハッシュ値に該当するコンパイル済みのファイルがあれば、それを読み込む
- コンパイル済みキャッシュがないならば、以下の処理を行う
- 与えられた C ソースの中で最初に宣言されている関数を正規表現で見つける
- 上記の関数に対して、
VALUE
型と C 言語の型を変換するためのラッパー(後述)を生成 - Ruby 本体のコンパイル時に使われた C コンパイラ・オプションなどを使って C ソースをコンパイルする
- RbConfig から
LDSHARED
などを取得しています
- RbConfig から
C 拡張の関数の引数には Ruby のオブジェクトすべてが VALUE
型として渡されるため、FIX2INT
といった CRuby 固有のマクロを使って C 言語の int
型などに変換して読み取ることになります。RubyInline gem を使う場合、C 言語の関数の引数に int
型などを宣言しておくと、FIX2INT
などのマクロを使って VALUE
型を変換するコードを自動で挿入してくれます。
C 拡張のハマりどころ
RubyInline 固有でなく、そもそもの C 拡張の実装時に気をつけるべき点があります:
raise とクリーンナップ処理
C 言語には例外の概念がないが Ruby には例外の概念があります。しかも、C 拡張で使う色々な関数・マクロは Ruby 例外を投げることがあります。例えば公式ドキュメントにある通り FIX2INT は例外を投げます。
C 拡張の関数の処理の途中で Ruby 例外が発生した場合、C 拡張の関数の処理は途中で打ち切られてしまうため、リソースのクリーンナップが問題になります:
// コードはかなり簡略化したイメージです VALUE resize_image_file(VALUE self, VALUE filename, VALUE width, VALUE height){ gdImagePtr im, om; im = gdImageCreateFromJpeg(StringValuePtr(filename_in)); // ↓の NUM2INT で例外が発生すると im の画像が放置されリークする om = gdImageScale(im, NUM2INT(w), NUM2INT(h)); // ... }
先に述べたように関数の引数の VALUE
からの変換であれば RubyInline が自動で行ってくれるのですが、例えば Ruby のクラスのプロパティを読み取る場合などは自力で VALUE
を取り扱う必要があるため、そのような場合に留意が必要です。またもちろん VALUE
の取扱以外の処理でも Ruby 例外が発生するものはあります。
これに対する対処法としては以下が考えられます:
- Ruby の例外が発生しうる処理を関数の途中などでは極力呼ばないようにする
- といっても
NUM2INT
のような処理ですら例外が起きえるので、この努力目標的な手段は最後の手段...
- といっても
- CRuby の提供するマクロ・関数の呼び出しは C 拡張の処理の先頭に寄せておき、処理の途中では呼び出さない
- 関数の先頭以外では Ruby の
VALUE
や Ruby 処理系の各種関数を一切使わないようにする - C 言語の関数内部であれば大域脱出がそもそも発生しない*6
- 関数の先頭以外では Ruby の
- rb_ensure を使い、C 拡張内部で
ensure
相当の実装をする- 確実ではあるのですが、ensure 節の処理を一つの関数の中のラベルなどとして記述できるのではなく、別の関数にしなければならないので手間
メモリ確保と GC
Ruby は GC が存在する言語ですが C 言語はそうではないため、Ruby の GC とうまく協調する必要があります。
- Ruby はメモリ不足時に GC を実行するが C の
malloc
は当然そうではないため、 参考サイト にある通りALLOC
などのマクロを使うか、あるいはメモリ不足時にrb_gc()
などとすること *7 TypedData
を使って C のstruct
をラップして Ruby のオブジェクトにする場合は TypedData の free 関数をちゃんと実装する- RGenGC などの GC 実装の詳細を意識した実装*8は大変な上に特定の GC 実装と密結合になってしまうので、できるだけ避ける
- C 拡張が確保したメモリに VALUE を入れる(非 Ruby オブジェクトから Ruby のオブジェクトを参照する)ことは避ける
- C 拡張をラップする Ruby のオブジェクトから C 拡張を呼び出す形にし、必要なオブジェクト参照はすべて Ruby のオブジェクト側で保持すれば良い
- RubyInline を使っていれば自然とそうなるはず
- C 拡張は関数の第一引数に
VALUE self
を受け取れる(RubyInline の場合自動で引数が足されている)ので、self の中身を参照することができる
RubyInline の気をつけどころ・ハマりどころ
何はともあれ -Wall
C のコンパイル時に -Wall
(警告をできるだけ出す)は忘れずに有効にしたいです。警告というものの、実際はバグが指摘されていることがほとんどかと思います。
CRuby 自体のコンパイルオプションに入っていないオプションは RubyInline のコンパイルオプションにも入らないため、確実に有効にしたいコンパイルオプションはinline
gem で C 拡張をコンパイルする際の builder
に明示的に設定すると良いです: builder.add_compile_flags '-Wall'
複数の関数を定義したり関数の宣言(≒ ヘッダーファイルの自作)をする際の方法
C のソースが大きくなる場合、サブルーチン(関数)を複数定義したり、ソースをヘッダーファイル(.h
)に分ける必要が生じることがありますが、RubyInline ではその場合のやり方に少し癖があります。
まず、inline
gem で C 拡張をコンパイルする際の builder
には以下の 3 つを渡せます:
builder.prefix
: C のヘッダー(.h
)相当のソースコードbuilder.c
: C のソース(.c
)相当のソースコードbuilder.add_to_init
: init 関数(C拡張の初期化関数)の内部に追記するコード *9
上記を使うことで複数の関数やヘッダーファイル相当の実現は可能なのですが、以下の点に留意が必要です:
builder.prefix
やbuilder.c
の中で#include "自前のソース.h"
は使えない- あくまで builder に渡したソースコード文字列だけが一時ファイルに書き出されて cc に渡る
- 筆者は
#include
を読み込んで展開する処理を Ruby で実装した - なお、
builder.include '<stdio.h>'
やbuilder.add_link_flags "-lgd"
などとすることでシステム上にあるライブラリの読み込みはできる
builder.c
にはinline
gem によって生成するメソッドに対応する関数が一番最初に来なければならない- なのでサブルーチンを先に定義(実装)する書き方はできない。ヘッダーファイルに宣言して、サブルーチンはメインの関数の後で実装するしかない
ビルド済みライブラリのキャッシュの取扱い
inline
gem はコンパイルした結果生成されるバイナリ *10 をキャッシュしています。しかしそのキャッシュを再利用するかどうかの判定に、C のソースコード自体の差分ではなく 呼び出し元の Ruby のファイルの内容と更新日時を使っています。
なので C のソースコードをファイルから読み込むようにしている場合や、Ruby 処理系・依存ライブラリのバージョンアップ時を行った際などにも、キャッシュが効いてしまうことがあります。
INLINEDIR
環境変数で指定のパス(またはホームディレクトリ以下)に .ruby_inline
といった名前のディレクトリができているはずなので、そのディレクトリをアプリケーション起動時などに適宜削除しましょう。
まとめ
このように CRuby の C 拡張の作成や RubyInline Gem の取扱いにはいくつかの注意点はあるものの、C 言語向けのライブラリ等を楽に使える利便性は大きいと思います。もし gem などが用意されていないライブラリなどを使いたくなった際には検討してみるとよろしいかと思います。
We are hiring !
エムスリーでは Ruby も利用しておりますし、それ以外にもいろいろな言語・技術などを積極的に活用しております。ご興味あればぜひ下記フォームよりお気軽にコンタクトしてくださいませ:
*1:ImageMagick を使っても良かったのですが、環境構築や保守の手間を考慮し試しに使ってみました
*2:去年にやったことの背景を思い出すのに時間がかかったので、また将来に迷わないよう...
*3:例えば世代別 GC である RGenGC の導入時にすらも、C 拡張に対するほぼ完全な互換性が確保されていました
*4:スクリプト言語処理系で一般的なタグ付き共用体になっているそうです
*5:ここで C のソースではなく Ruby のファイルを見てしまっている点はハマりどころです、後述
*6:libpng のような setjmp/longjmp するライブラリなどを使わない限り
*7:画像処理などだとメモリ確保の実装は変えられないので後者になる
*8:ライトバリアを「適切に」入れる
*9:これは関数の宣言ではなく関数の中に追記するコード、なので注意
*10:正確には .so, .dylib といった Dynamic Link ライブラリ