mrubyで定義したクラスとメソッドをCから呼び出す
mrubyで書いた方がいいところはmrubyで書いてそうじゃないところはCで書く、という開発をするには、Cで定義した関数をRubyから実行させたり、逆にRubyで定義したクラスやメソッドをCから呼び出せるようにする必要があると思った。前者のような実装はmrbgemsを読めばたくさんある一方で、後者の実装は調べたけどあんまりなかった。そこで、先日「Head First C」でCの初歩を学んだことだし、mrubyのソースコードを読みながら後者の「mrubyで定義したクラスとメソッドをCから呼び出す」実装を試行錯誤してみた。
試行錯誤してみてとりあえず動いたというだけで、正しいやり方じゃないかもしれないので、コメントか@naoty_k宛にメッセージをいただけるとありがたいです。また、参照しているmrubyのコミット番号は「9663a7」です。
Rubyのクラスとメソッドを用意
適当にPersonクラスとメソッド2つを用意する。あとでこれらをCから呼び出す。
// person.rb class Person attr_accessor :name, :age def initialize(name, age) @name = name @age = age end def greeting "Hello, my name is #{name}, #{age} years old." end end
mrbcでコンパイル
RubyのファイルをCからロードするにはいくつか方法があるようだけど、今回はmrbcで*.mrb形式にコンパイルしてCからロードするようにする。
$ ls person.rb $ mrbc person.rb $ ls person.mrb person.rb
Cから定義したクラスとメソッドを呼び出す
CからRubyで定義したPerson
インスタンスを生成してgreeting
メソッドの結果を標準出力に表示してみる。
// greeting.c #include <stdio.h> #include <mruby.h> #include <mruby/string.h> int main() { mrb_state* mrb = mrb_open(); // mrubyファイルをロードする FILE *fd = fopen("person.mrb", "r"); mrb_load_irep_file(mrb, fd); // クラスオブジェクトを取得する struct RClass *person = mrb_class_obj_get(mrb, "Person"); // 引数をmrb_valueに変換する mrb_value person_value = mrb_obj_value(person); mrb_value name_value = mrb_str_new(mrb, "naoty", 5); mrb_value age_value = mrb_fixnum_value(25); // Person#newを呼び出す mrb_value naoty = mrb_funcall(mrb, person_value, "new", 2, name_value, age_value); // Person#greetingを呼び出す mrb_value greeting_value = mrb_funcall(mrb, naoty, "greeting", 0); // 返り値をchar*に変換して出力する char *greeting = mrb_string_value_ptr(mrb, greeting_value); printf("%s\n", greeting); mrb_close(mrb); return 0; }
- *.mrb形式のファイルをロードするには
mrb_load_irep_file()
を実行する。 - 次にクラスを取得するには
mrb_class_obj_get()
を実行し、メソッドを呼び出すにはmrb_funcall()
を実行する。 mrb_funcall()
には、第2引数にメソッドのレシーバ、第3引数にメソッド名、第4引数にメソッドの引数の数、第5引数以降にはメソッドの引数を渡す。第2引数と第5引数以降はint
やchar*
などをそのまま渡すことはできなくて、mrb_value
という構造体に変換する必要がある。変換するための関数については長くなりそうなので、別の記事にしようと思う。mrb_funcall()
の返り値もmrb_value
構造体なので、標準出力をするためにchar*
に変換する。
Cをコンパイルして実行
Cのソースコードをmrubyのヘッダーファイルやスタティックライブラリと一緒にコンパイルする。僕の環境だと以下のコマンドでコンパイルできた。
$ gcc -I ~/mruby/include greeting.c ~/mruby/build/host/lib/libmruby.a -lm -o greeting $ ./greeting Hello, my name is naoty, 25 years old.
greeting.cはperson.mrbに依存し、person.mrbはperson.rbに依存しているので、一連のビルドはMakefileかRakefileで自動化したほうがいいと思う。
// Rakefile require "rake/clean" CC = "gcc" MRBC = "mrbc" CLEAN.include("person.mrb") CLOBBER.include("greeting") task default: "greeting" file "greeting" => ["greeting.c", "person.mrb"] do |t| sh "#{CC} -I ~/mruby/include #{t.prerequisites[0]} ~/mruby/build/host/lib/libmruby.a -lm -o #{t.name}" end file "person.mrb" => ["person.rb"] do |t| sh "#{MRBC} #{t.prerequisites[0]}" end
$ rake $ ./greeting
参考
@naoty_k クラスの取り出しはmrb_class_obj_get()、メソッドの呼び出しはmrb_funcall()を使ってください。funcallには派生形あり。
— Yukihiro Matsumotoさん (@yukihiro_matz) 2013年4月30日