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 を呼び出したいとする。

callee.rb
module Test
  class Callee
    def foo(a)
      puts a
    end
  end
end

Cアプリケーションは以下のように書けば良い。

sample.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 においてあります。