エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

RubyInline gem で C 拡張を手軽に作ってみた

f:id:Saiya:20181204042827p:plain

こんにちは、サーバーサイドエンジニアといいつつも最近は 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 言語のコードのビルドやデプロイの手間が大幅に軽減されます。

ソースコード をざっと見たところ、内部的には以下のような動きをしているようです:

  1. inline 関数の呼び出し元のファイル(class を定義しているファイル)の更新日時・ハッシュ値を元に、キャッシュ用のハッシュ値を算出 *5
  2. 上記で算出したハッシュ値に該当するコンパイル済みのファイルがあれば、それを読み込む
  3. コンパイル済みキャッシュがないならば、以下の処理を行う
    1. 与えられた C ソースの中で最初に宣言されている関数を正規表現で見つける
    2. 上記の関数に対して、 VALUE 型と C 言語の型を変換するためのラッパー(後述)を生成
    3. Ruby 本体のコンパイル時に使われた C コンパイラ・オプションなどを使って C ソースをコンパイルする
      • RbConfig から LDSHARED などを取得しています

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
  • 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 の気をつけどころ・ハマりどころ

何はともあれ -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.prefixbuilder.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 も利用しておりますし、それ以外にもいろいろな言語・技術などを積極的に活用しております。ご興味あればぜひ下記フォームよりお気軽にコンタクトしてくださいませ:

jobs.m3.com

*1:ImageMagick を使っても良かったのですが、環境構築や保守の手間を考慮し試しに使ってみました

*2:去年にやったことの背景を思い出すのに時間がかかったので、また将来に迷わないよう...

*3:例えば世代別 GC である RGenGC の導入時にすらも、C 拡張に対するほぼ完全な互換性が確保されていました

*4:スクリプト言語処理系で一般的なタグ付き共用体になっているそうです

*5:ここで C のソースではなく Ruby のファイルを見てしまっている点はハマりどころです、後述

*6:libpng のような setjmp/longjmp するライブラリなどを使わない限り

*7:画像処理などだとメモリ確保の実装は変えられないので後者になる

*8:ライトバリアを「適切に」入れる

*9:これは関数の宣言ではなく関数の中に追記するコード、なので注意

*10:正確には .so, .dylib といった Dynamic Link ライブラリ