Rubyの拡張ライブラリを作ってみよう!

はじめに

Ruby Advent Calendar jp: 2011 : ATNDの17日目の記事です。昨日は@yoppiblogさんのSeleniumの自動テストをCI環境(Jenkins)で快適に実施するでした。


Rubyを使ってて
遅い…ここだけ超遅い…
とか
あのライブラリが使いたい!でもRuby用のライブラリじゃないし…
みたいなこと、ありますよね?
そんなとき、Rubyの拡張ライブラリで解決できるかもしれません。


Rubyの拡張ライブラリは、普通のライブラリと異なりC(とかC++とかその他の言語とか)で作成します。そのため、Rubyで直接書くよりも高速に処理できたり、Cのインタフェースが用意されているライブラリをRubyから呼びだしたりすることができます。すばらしい!
そんなわけで、拡張ライブラリの作り方をざっくり説明したいと思います。

用意するもの

最小限のRuby拡張ライブラリ

Rubyの拡張ライブラリを作成してから使うまでの流れは、

  1. Cでコードを書く
  2. Makefileを生成するためにextconf.rbを書く
  3. Makefileを生成して make && make install する
  4. 使う

みたいな感じです。


1. Cでコードを書く
foo というライブラリを作ることにしましょう。
まず適当にディレクトリを作って

$ mkdir foo
$ cd foo

こんな感じのコードを書きます。

/* foo.c */
#include <stdio.h>

/* require 'foo' とやると、Init_foo() が呼ばれる */
void Init_foo()
{
  puts("Init_fooですよー");
}

拡張ライブラリでは、

require 'ライブラリ名'

をやると

Init_(ライブラリ名)

が呼び出されます。普通はここでクラスやメソッドの定義などをやります。


2. Makefileを生成するためにextconf.rbを書く
Makefileを生成するためのスクリプトをRubyで書きます。下記のようなファイルをextconf.rbという名前で作成しましょう。

require 'mkmf'
create_makefile('foo')

mkmfをrequireして create_makefile(ライブラリ名) とするだけで、とりあえずMakefileを生成できます。


3. Makefileを生成して make && make install する
extconf.rb を普通に実行すればMakefileが生成されるので、普通に make && make install すればOKです。

$ ruby extconf.rb 
creating Makefile
$ make
compiling foo.c
linking shared-object foo.so
$ make install
/usr/bin/install -c -m 0755 foo.so /path/to/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/i686-linux
installing default foo libraries


4. 使う
普通にrequireするだけ。他のライブラリと全く同じです。

$ irb
ruby-1.9.3-p0 :001 > require 'foo'
Init_fooですよー
 => true 
ruby-1.9.3-p0 :002 > 

クラスとメソッドを定義する

クラス、メソッドを定義するには、それぞれrb_define_class()、rb_define_method()を使います。

/*
  name: クラス名
  super: 親クラス
  戻り値: 定義したクラス
*/
VALUE rb_define_class(const char *name, VALUE super);
/*
  klass: メソッドを定義するクラス
  name: メソッド名
  func: メソッド実行時に呼び出されるCの関数
  argc: メソッドの引数の数
  戻り値: なし
*/
void rb_define_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc);

VALUE型は、Rubyのオブジェクトを指すポインタです。引数や戻り値でRubyのオブジェクトをやりとりする際は、このVALUE型を使います。


また、privateメソッドを定義するrb_define_private_methodもあります。initializeメソッドを定義するときにはこっちを使いましょう。

/*
  klass: メソッドを定義するクラス
  name: メソッド名
  func: メソッド実行時に呼び出されるCの関数
  argc: メソッドの引数の数
  戻り値: なし
*/
void rb_define_private_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc);


では、さっきのfoo.cをこんな感じに変えて、FooクラスとFoo#addメソッドを定義してみます。

/* foo.c */
#include <stdio.h>
#include <ruby.h>

VALUE cFoo; /* Fooクラス */

/* self はメソッドの呼び出し元オブジェクト */
VALUE bar_func(VALUE self)
{
  puts("Foo#barですよー");
  return self;
}

void Init_foo()
{
  /* Fooクラスを定義 */
  cFoo = rb_define_class("Foo", rb_cObject);
  
  /*
    Fooクラスに引数0個のbarメソッドを定義
    barメソッドが呼ばれると、bar_funcが実行される
  */
  rb_define_method(cFoo, "bar", RUBY_METHOD_FUNC(bar_func), 0);
}


こんな感じで実行します。ちゃんと bar_func が呼ばれてますね。

$ ruby extconf.rb && make && make install
$ irb
ruby-1.9.3-p0 :001 > require 'foo'
 => true 
ruby-1.9.3-p0 :002 > foo = Foo.new
 => #<Foo:0x88f0d60> 
ruby-1.9.3-p0 :003 > foo.bar
Foo#barですよー
 => #<Foo:0x88f0d60> 
ruby-1.9.3-p0 :004 > 

Rubyの値とCの値を変換する

実際の拡張ライブラリでは、Rubyの値とCの値との変換が必要になります。
よく使うのはこのへんです。

関数名 用途
int NUM2INT(VALUE x) Rubyの数値をCのint型に変換
double NUM2DBL(VALUE x) Rubyの数値をCのdouble型に変換
char* StringValuePtr(VALUE str) Rubyの文字列をCのchar*に変換
VALUE INT2NUM(int x) Cのint型をRubyの数値に変換
VALUE DBL2NUM(double x) Cのdouble型をRubyの数値に変換
VALUE rb_str_new2(const char* ptr) Cのchar*型をRubyの文字列に変換


こんな感じで使います。

#include <stdio.h>
#include <ruby.h>

VALUE cFoo;

VALUE bar_func(VALUE self, VALUE i_val, VALUE d_val, VALUE s_val)
{
  /* Rubyの値をCの値に変換して */
  int i = NUM2INT(i_val);
  double d = NUM2DBL(d_val);
  char* s = StringValuePtr(s_val);

  /* なんか処理して */
  double x = i + d;
  printf("[%s : %d + %lf = %lf]\n", s, i, d, x);

  /* Cの値をRubyの値に変換して返す */
  return DBL2NUM(x);
}

void Init_foo()
{
  cFoo = rb_define_class("Foo", rb_cObject);

  /* Foo#barは引数を3つ取るように変更 */
  rb_define_method(cFoo, "bar", RUBY_METHOD_FUNC(bar_func), 3);
}


Foo#barに引数を3つ与えて実行してみましょう。

$ ruby extconf.rb && make && make install
$ irb                                    
ruby-1.9.3-p0 :001 > require 'foo'
 => true 
ruby-1.9.3-p0 :002 > foo = Foo.new
 => #<Foo:0x9441ea8> 
ruby-1.9.3-p0 :003 > foo.bar(1, 2.2, 'foobar')
[foobar : 1 + 2.200000 = 3.200000]
 => 3.2 

どうやらちゃんと動いてます。やったね!

さらに調べるには

まとめ

Rubyの拡張ライブラリの作り方をざっくり紹介しました。これでボトルネックもCで書かれたライブラリも怖くないはず!
もうすこし書きたいところではありますが、時間もアレなのでこのへんで。
明日は18日目、@sakairyotaさんです。