2013年12月10日火曜日

RubyによるC拡張ライブラリの作成方法


◆ 目的
Cで書いたコードを動的(あるいは静的)にリンクさせ、Rubyを高速に動作させる



◆ Cで置き換えるRubyのコード
下記のRubyコードをCで置き換える
コード自体に深い意味はない、ただのサンプル用である

class Counter

  def initialize
    @count = 0
  end

  attr_reader :count

  def increment
    @count += 1
  end

end



◆ 利用例
test.rb
# Cで書き直し、ビルドした共有ライブラリを取り込む
require 'counter'

# あとは普通のRubyコードど同じ要領で利用できる
cnt = Counter.new

puts cnt.count
cnt.increment
puts cnt.count


$ ruby test.rb
0
1




◆ Cによるクラス定義
Ruby拡張ライブラリ(共有ライブラリ.so)を作成するためのCコードを記述していく

counter.c
#include <ruby.h>

// 関数の宣言(定義は後述)
static VALUE counter_alloc(VALUE klass);
static VALUE counter_initialize(VALUE self);
static VALUE counter_increment(VALUE self);
static VALUE counter_count(VALUE self);



/* Ruby拡張ライブラリ(共有ライブラリ.so)がrequireされた時、
   Init_XXX()関数がまず呼び出される */
void
Init_counter(void)
{
  // VALUEは各種オブジェクト構造体へのポインタにキャストして使われる
  // 今回はCレベルで定義されたクラスを表すRData構造体になる
  VALUE Counter;


  // クラスの定義
  Counter = rb_define_class("Counter", rb_cObject);
  /* rb_cObjectはクラスオブジェクト(Object)のVALUEを格納しているグローバル変数である
  今回作成するクラスオブジェクトのスーパークラスがObjectクラスであり、
  つまりここではrb_cObjectになる
  Rubyコードで書けば、"class Counter < rb_cObject" である */


  // フィールドのクラスへの関連付け
  // Counterクラスへcountフィールドを追加する
  // 実際に割り当てるフィールドは、counter_alloc関数内で定義する
  rb_define_alloc_func(Counter, counter_alloc);


  // メソッドのクラスへの関連付け
  // Counterクラスへinitializeメソッド(実体はcounter_initialize関数)を追加する
  // 引数の数は0
  // プライベートなメソッドなので、本APIを使っている
  rb_define_private_method(Counter, "initialize", counter_initialize, 0);

  // Counterクラスへcountメソッド(実体はcounter_count関数)を追加する
  // 引数の数は0
  // Rubyコード上のattr_readerを使ったアクセサメソッドである
  rb_define_method(Counter, "count", counter_count, 0);

  // Counterクラスへincrementメソッド(実体はcounter_increment関数)を追加する
  // 引数の数は0
  rb_define_method(Counter, "increment", counter_increment, 0);
}



// フィールドの定義
struct counter {
    int count;
};

// フィールドをクラスへセット
static VALUE
counter_alloc(VALUE klass)
{
  // フィールド用にメモリを割り当てる
  struct counter *ptr = ALLOC(struct counter);
  /* ALLOC()はmalloc()に失敗するとGCを行ってからやりなおし、
     それでも失敗したら例外を投げる
     従って返り値をチェックする必要がないので便利である */

  // klassへフィールドをセットする
  return Data_Wrap_Struct(klass, 0, -1, ptr);
  // Data_Wrap_Struct() のプロトタイプについては下記"◆ GCについて補足"欄を参照のこと
}



// initializeメソッドをクラスへセット
static VALUE
counter_initialize(VALUE self)
{
    struct counter *ptr;

    // self("struct RData*"であるVALUE)から"struct counter*"を取り出す
    Data_Get_Struct(self, struct counter, ptr);
    ptr->count = 0;

    // initialize の返り値には意味がないので、適当にnilを返しておく
    return Qnil;
}


// counterメソッドをクラスへセット
static VALUE
counter_count(VALUE self)
{
  struct counter *ptr;

  Data_Get_Struct(self, struct counter, ptr);
  return INT2FIX(ptr->count);
  /* Rubyではオブジェクトの実体を構造体で表現し、扱うときは常にポインタ経由で扱う
     構造体はクラスごとに違う型を使うが、ポインタはどのクラスの構造体でも常にVALUE型である*/
}


// counter_incrementメソッドをクラスへセット
static VALUE
counter_increment(VALUE self)
{
    struct counter *ptr;

    Data_Get_Struct(self, struct counter, ptr);
    ptr->count++;
    return self;
}




◆ GCについて補足
Data_Wrap_Struct() のプロトタイプは下記である
VALUE Data_Wrap_Struct(VALUE klass,
                       void (*dmark)(void*),
                       void (*dfree)(void*),
                       void *data);


klass: このオブジェクトのクラス
dmark: struct RData の dmark メンバに格納する関数ポインタ
dfree: struct RData の dfree メンバに格納する関数ポインタ
data : ユーザの用意した構造体へのポインタ

struct counter{} 内でcounterの型をint型にしていた
Rubyの整数を使うためにVALUE型に変えれば、intの範囲はRuby任せにできる

ここで注意点
Rubyでは小さな整数はFixnumクラスであり、
大きくなるとBignumクラスになるのは知っているかと思う
Bignum には構造体があるので、GCマークして保護しなくてはいけない


(変更前)
static VALUE
counter_alloc(VALUE klass)
{
  struct counter *ptr = ALLOC(struct counter);

  return Data_Wrap_Struct(klass, 0, -1, ptr);
}


(変更後)
static void
counter_mark(struct counter *ptr)
{
  rb_gc_mark(ptr->count);
}

static VALUE
counter_alloc(VALUE klass)
{
  struct counter *ptr = ALLOC(struct counter);

  return Data_Wrap_Struct(klass, counter_mark, -1, ptr);
}




◆ 共有ライブラリの作成
まずはMakefileを作成する

extconf.rb
#  Makefile を作成するライブラリが必要である
require 'mkmf'

# 'counter'の部分はInit_xxxx のxxxxを使う
create_makefile 'counter'


実行するとMakeファイルができている
$ ruby extconf.rb


makeして共有ライブラリを作る
$ make

これで、counter.soができあがる



◆ 実行
先の利用例で記した方法で実行可能である

0 件のコメント:

コメントを投稿