こんにちわ。安定して世の中の5年ビハインドを追う hasegaw です。
Ruby というと 10年近く前に tDiary のレイアウトとかいじるために数行書き換えた程度しか経験がなくて Ruby プログラミングなんてできなかったワタクシですが、この度 Ruby で改めて Hello World いたしまして、というか Ruby は書けないけども Ruby の拡張モジュールを C(++) で実装して色々わかったことを。
■最低限の実装
拡張モジュールはC言語(もしくは後述の方法によりC++言語)にて記述できる。以下のファイルを hoge.c としてローカルディレクトリに置く。ファイル名はなんでも良い。
#include "ruby.h"
VALUE hello_func(VALUE self, VALUE s) {
printf("hello %s\n", RSTRING_PTR(s));
return INT2FIX(777);
}
void Init_hoge()
{
VALUE module;
module = rb_define_module("Hoge");
rb_define_module_function(module, "hello",
(hello_func), 1);
}
このファイルをビルドするためには extconf.rb ファイルを作成する。この中には以下の2行を書いておけばよい。
require 'mkmf'
create_makefile('hoge')
このうち create_makefile() の引数として指定した部分ができあがる拡張モジュールファイルの名前になる。例えば Linux
環境の Ruby であれば hoge.so だったり、 OS X 環境の Ruby であれば hoge.bundle
だったりというファイル名が出力される共有ライブラリになる。
なおコンパイル対象のファイルについての指定はしていないが、ローカルディレクトリにあるソースコードは片っ端からコンパイルするみたい。でもお作法的には "ライブラリ名.cpp" がメインのソースコード名になるようだ。
共有ライブラリを作るには extconf.rb を実行して Makefile を生成させ、その Makefile でコンパイルする。
$ ruby extconf.rb
$ make
拡張モジュールを読み込むためには Ruby 起動時に -r"module_name" オプションを指定すればよい。たとえば下記のようなイメージでC言語側の実装を呼び出せる。
ruby -rhoge -e 'Hoge.hello("world")'
-> "hello world"
■ g++ でコンパイルしたい
Linux 上にて、参照するインクルードファイルのパースに g++ が必要だったのでソースを gcc ではなく g++
でどうしてもコンパイルしたかった。このため、当初はコンパイラを g++ に切り替えるために無理やり Makefile の CC=
の行を書き換えていたが、どうやら extconf.rb に下記の変更を加えるのが正しい?みたい。
require 'mkmf'
have_library('stdc++') # 追加した
create_makefile('hoge')
ただ、これだけだと未だソースは gcc でコンパイルされてしまう。もう一つの作業はソースコードの拡張子を .c から .cpp に書き換える。
$ mv hoge.c hoge.cpp
■ g++ でコンパイルを通す
これで使われるコンパイラが g++ に変わったが、そうすると明示的に型キャストしないとコンパイルが通らなかったりする(OS Xのclangではそのまま通るみたい)。rb_define_module_function() の第二引数の型キャストで叱られるが、下記の要領で回避できる。
変更前
rb_define_module_function(module, "hello",
(hello_func), 1);
変更後
rb_define_module_function(module, "hello",
reinterpret_cast<VALUE(*)(...)>(hello_func), 1);
これでコンパイルは通るのだが、実際にはこのライブラリは使えない(RubyがInit_hoge関数にリンクできない)。
$ ruby -rhoge -e 'Hoge.hello("world")'
./hoge.so: ./hoge.so: undefined symbol: Init_hoge - ./hoge.so (LoadError)
悩んだ末判ったことは g++ でコンパイルしているとエクスポートされるシンボル名が gcc で C 言語としてコンパイルした場合と変わってしまうということに気づいた。
gcc でコンパイルした場合
$ objdump -t hoge.so | grep Init_
00000000000006e0 g F .text 000000000000002f Init_hoge
g++ でコンパイルした場合
$ objdump -t hoge.so | grep Init_
0000000000000760 g F .text 000000000000002f _Z9Init_hogev
Google 先生に聞いてみたら、これは関数を宣言するときに extern "C" と書いておけば g++ でも C 言語ルールで関数をエクスポートしてくれるらしい。 g++ でコンパイルできる拡張モジュールは最終的に下記のようなソースコードになった。(もはや Ruby じゃなくて C/C++ の話w)
#include "ruby.h"
extern "C" VALUE hello_func(VALUE self, VALUE s) {
printf("hello %s\n", RSTRING_PTR(s));
return INT2FIX(777);
}
extern "C" void Init_hoge()
{
VALUE module;
module = rb_define_module("Hoge");
rb_define_module_function(module, "hello",
reinterpret_cast<VALUE(*)(...)>(hello_func), 1);
}
ここまでがわかれば、後は Web サイト上に様々な情報が落ちているので、もう悩むことはない。特に Ruby 上のオブジェクトをいじるときに下記が大変よいとっかかりになった。
「はじめてのRuby拡張ライブラリ」 (とみたまさひろさん)
http://www.slideshare.net/tmtm/ruby-ext
でRubyの世界とどう干渉すればいいかはわかるし、その先で何か困ったらとりあえず Ruby についている README.EXT を見ればよい。
README.EXT.ja も読むとよいですよ〜(もう読んでそう)
返信削除oh, README.EXT.ja も大事な情報源ですね。参照していたにもかかわらず挙げるの忘れていました。。。
返信削除