Cアプリケーションから libruby を使ってRubyコードに定義されている関数を呼び出す方法について記載する。
対象の Ruby バージョンは 2.4.1。
libruby の生成
libruby は rbenv で ruby をインストールすると、実はすでにできているのでご利用いただける。
~/.rbenv/versions/2.4.1/lib
~/.rbenv/versions/2.4.1/include/ruby-2.4.0/
ここに libruby-static.a とヘッダファイルがある。
利用中の ruby から lib と include パスを動的に取り出す
以下のコマンドでビルドに必要なパスを動的に取り出せる
$ ruby -e 'puts RbConfig::CONFIG["libdir"]'
/Users/seo.naotoshi/.rbenv/versions/2.4.1/lib
$ ruby -e 'puts RbConfig::CONFIG["LIBS"] + " " + RbConfig::CONFIG["LIBRUBYARG_STATIC"]'
-lpthread -ldl -lobjc -lruby-static -framework CoreFoundation
$ ruby -e 'puts RbConfig::CONFIG["rubyhdrdir"] + File::SEPARATOR + RbConfig::CONFIG["arch"]'
/Users/seo.naotoshi/.rbenv/versions/2.4.1/include/ruby-2.4.0/x86_64-darwin16
$ ruby -e 'puts RbConfig::CONFIG["rubyhdrdir"]'
/Users/seo.naotoshi/.rbenv/versions/2.4.1/include/ruby-2.4.0
これを利用して Makefile を書くとこんなかんじになる。
TARGET = sample
LIBS = -L $(shell ruby -e 'puts RbConfig::CONFIG["libdir"]') $(shell ruby -e 'puts RbConfig::CONFIG["LIBS"] + " " + RbConfig::CONFIG["LIBRUBYARG_STATIC"]')
INCLUDE = -I $(shell ruby -e 'puts RbConfig::CONFIG["rubyhdrdir"] + File::SEPARATOR + RbConfig::CONFIG["arch"]') -I $(shell ruby -e 'puts RbConfig::CONFIG["rubyhdrdir"]')
all : $(TARGET)
$(TARGET) : sample.c
gcc $(INCLUDE) $(LIBS) -o $(TARGET) sample.c
clean :
rm -f $(TARGET)
Cアプリケーションからrubyコードを呼び出す例
呼び出し対象のRubyコードが以下のようなものだとして、Test::Callee.new#foo
を呼び出したいとする。
module Test
class Callee
def foo(a)
puts a
end
end
end
Cアプリケーションは以下のように書けば良い。
#include "ruby.h"
#include "ruby/encoding.h"
VALUE $kernel;
void init()
{
// Ruby初期化のおまじない
ruby_init();
ruby_init_loadpath();
rb_enc_find_index("encdb"); // encodingライブラリの初期化
rb_require("rubygems");
rb_require("./callee");
}
void run()
{
VALUE module = rb_const_get(rb_cObject, rb_intern("Test"));
VALUE klass = rb_const_get(module, rb_intern("Callee"));
VALUE obj = rb_class_new_instance(0, NULL, klass); // Test::Callee.new
VALUE str = rb_str_new2("こんにちは");
rb_funcall(obj, rb_intern("foo"), 1, str); // obj.foo(str)
}
int main()
{
init();
run();
}
コメントに書いているのだが、一応解説しておくと、
ruby_init();
ruby_init_loadpath();
rb_enc_find_index("encdb");
が Ruby 初期化のおなじないである。Ruby のスタートアップ処理については usa さんが詳細な記事を書いていたので読むと良い(かもしれない)。
rb_require("rubygems");
rb_require("./callee");
次にここで rubygems
を require しつつ、今回読み込みたい ruby スクリプトを require している。require_relative 相当のものは使えなかったので、./
をつけているが、カレントディレクトリが変わると動かなくなるので実は微妙な気はしている。
rb_require("callee")
として、ビルド後
RUBYLIB=. ./sample
のようにして RUBYLIB を指定しながら実行する方が良いかもしれない。いや、面倒だな。宿題。
VALUE module = rb_const_get(rb_cObject, rb_intern("Test"));
VALUE klass = rb_const_get(module, rb_intern("Callee"));
VALUE obj = rb_class_new_instance(0, NULL, klass);
これは ruby で書くと obj = Test::Callee.new
に相当する。
rb_funcall(obj, rb_intern("foo"), 1, str);
最後にこれで、obj.foo(str)
を呼び出している。完。
今回のコード
https://github.com/sonots/libruby-sample においてあります。