読者です 読者をやめる 読者になる 読者になる

FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

Ruby1.8/1.9両対応のC拡張モジュールを作る方法

プログラミング

Python用に作成した拡張モジュールをRubyに移植する必要があり,Ruby拡張モジュールの作成方法を調べました。

以下の仕様を満たすgemパッケージを作成することが目的になります。

  • Ruby1.8とRuby1.9に両対応したC拡張モジュールを作成する。
  • Foo::Barのように階層を持たせた名前空間とする。
  • コンパイル済みのバイナリモジュールを含む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段深くしただけなので,モジュールの名前空間は別途定義してやる必要があります。

具体的には,拡張モジュールの初期化ルーチンの中で,以下のように記述します。

VALUE module_foo = rb_define_module("Foo");
VALUE module_bar = rb_define_module_under(module_foo, "Bar");

なお,初期化ルーチンの関数名は Init_xxx のように命名しますが,‘xxx’はライブラリのファイル名に対応するので,この場合 Init_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つが思いつきました。

  • CからRubyのencodeメソッドを呼び出す。
  • Rubyの文字列変換様内部ルーチンを使う。

前者のほうが正統派かもしれませんが,オーバーヘッドが気になったので,後者でやってみることにしました。

ただ,まったく資料が見つけられなかったので,ヘッダファイルで定義されている関数を組み合わせて作ってみました。あまり自信がありません。ツッコミ大歓迎です。

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