Ruby1.8/1.9両対応のC拡張モジュールを作る方法
Python用に作成した拡張モジュールをRubyに移植する必要があり,Ruby拡張モジュールの作成方法を調べました。
以下の仕様を満たすgemパッケージを作成することが目的になります。
私自身は,Rubyが出はじめた頃に一時使っていたものの,その後Pythonに乗り換えてしまったので,最近のRubyの事情はまったく分かっていません(10年以上ブランクがある)。
勘違いしている所もあるかと思いますので,指摘して頂ければ幸いです。
なお,動作確認は Mac OS X 10.6.6 で行っています。
参照資料
Ruby拡張モジュールの作成方法としては「README.ext.ja」が一次資料と思われますが,ヘッダの日付が1995年になっていて,Ruby1.9に対応しているとは思えません。
いろいろ探したのですが「ソースを読め」みたいな記述しか見つからなくて,結局はいくつかのブログ記事とRubyのソースを参考にしました。
主に参考したブログ記事は以下のとおりです。
階層を持たせた名前空間
newgemの設定
C拡張を持つRubyGemsを作るを参考に,newgemで生成したファイルに手を加えました。
まずnewgemを実行して,雛形となるファイルを生成させます。
$ newgem bar
通常拡張モジュール本体のCソースはextディレクトリ直下に置きますが,今回はext/fooディレクトリを作成してその下に置きます。
ext/foo/bar.c
次にetc/foo/extconf.rbを作成します。
コンパイル時に追加で指定したいオプションがあれば,ここで指定します。
require 'mkmf' $CFLAGS += " -I/usr/local/include/foo " $LOCAL_LIBS += " -L/usr/local/lib -lfoo " create_makefile('foo/bar')
Rakefileに以下の行を追加します。
$hoe = Hoe.spec 'reports' do (中略) self.spec_extras = { :extensions => ['ext/foo/extconf.rb'], } end
後は,Manifest.txt に以下のファイルを追加して rake package を実行すれば,通常のソースベースのパッケージが作成されます。
ext/foo/extconf.rb ext/foo/bar.c
これで,
require 'foo/bar'
を実行すると拡張モジュールがロードされるようになります。
1.8と1.9で処理を分岐させる
C拡張モジュールを作成する側からみると,1.8と1.9で大きく異なるのは以下の2点です。
- includeファイルのディレクトリ構造が異なる。
- 文字列の構造が異なる。
Pythonではバージョン番号を持ったマクロが定義されていたので,それを使ってバージョンごとに異なる処理を分岐させていたのですが,どうやらRubyではバージョンに依存したソースは書かせない方針のようです。
結局Ruby1.9以降には,HAVE_RUBY_ST_H, HAVE_RUBY_ENCODING_H 等のマクロが定義されているのを利用して,以下のように書きました。
#ifdef HAVE_RUBY_ST_H #include "ruby/st.h" #else #include "st.h" #endif #ifdef HAVE_RUBY_ENCODING_H #include "ruby/encoding.h" #endif
文字コードの変換
Ruby1.9以降では文字列オブジェクト自身がencodingの情報を持つようになったので,文字列データの文字コードを仮定することはできません。
そこで,拡張モジュール内部で特定の文字コード(ここではUTF-8)に自動変換することにしました。
方法としては以下の2つが思いつきました。
前者のほうが正統派かもしれませんが,オーバーヘッドが気になったので,後者でやってみることにしました。
ただ,まったく資料が見つけられなかったので,ヘッダファイルで定義されている関数を組み合わせて作ってみました。あまり自信がありません。ツッコミ大歓迎です。
static char* str2cstr(VALUE str) { #ifdef HAVE_RUBY_ENCODING_H VALUE utf8 = rb_str_encode(str, rb_enc_from_encoding(rb_utf8_encoding()), 0, Qnil); return StringValuePtr(utf8); #else return StringValuePtr(str); #endif }
ハッシュ内容の列挙
ハッシュの内容を列挙する方法としては,以下の2通りがあるようです。
- keysメソッドを呼ぶ。
- 内部ルーチン rb_hash_foreach() を使う。
rb_hash_foreach は最近追加された関数のようですが(1.8.2以降?),これでやってみることにしました。
以下のように使います。
// callbackルーチン static int f(VALUE key, VALUE val, VALUE arg) { // 1要素毎の処理 if (<正常終了>) { return ST_CONTINUE; } else { // 列挙を中断 return ST_STOP; } } // 呼び出し側処理 { rb_hash_foreach(obj, f, arg); }
【2011-02-09追記】
rb_hash_foreach() を使う方法は色々と不都合が出てきたので,別の方法で実装しました。
- callbackルーチンに渡すことのできるデータargがVALUE型なので,Rubyオブジェクトを別の形式に変換する用途では使いにくい。
- 処理上の都合によりエラー発生時に例外をあげたくなかったが,エラー発生を呼び出し側に伝える手段がない。
方針としては,to_aメソッドによりハッシュを配列に変換してから,配列と同様にループで展開します。
次のようなコードになります。
process_hash(VALUE obj) { VALUE ary = rb_funcall(obj, rb_intern("to_a"), 0); if (TYPE(ary) != T_ARRAY) { // エラー処理 } long i; for (i = 0; i < RARRAY_LEN(ary); ++i) { VALUE entry = rb_ary_entry(ary, i); if (TYPE(entry) != T_ARRAY || RARRAY_LEN(entry) != 2) { // エラー処理 } VALUE key = rb_ary_entry(entry, 0); VALUE val = rb_ary_entry(entry, 1); // 何らかの処理 } }
コンパイル済みgemパッケージの作成
コンパイル済みのgemパッケージを作るRubyGemsプラグイン - 古橋貞之の日記の通りの手順で,コンパイル済みのgemパッケージが作成できます。
$ sudo gem install gem-compile $ gem compile bar-0.0.1.gem
1.8/1.9両対応パッケージを作成する場合は,-f オプションを指定します。
$ gem compile -f 1.8:<ruby1.8のパス>,1.9:<ruby1.9のパス> bar-0.0.1.gem