Ruby拡張ライブラリ作成チュートリアル


目次

  1. 準備
  2. まずはC言語プログラムを呼び出す
  3. 簡単な数値のやりとり
  4. NArray配列を渡す
  5. Swigを使ってお手軽変換
  6. 参考文献

準備

このチュートリアルではRuby 1.8系列の使用を前提にしています。 確認はすべてRuby 1.8.4 [i386-Cygwin]で行いました。

NArrayはVersion 0.5.8を用いています。

またそれ以外に以外に以下のものが必要です。

Cコンパイラ

Rubyのコンパイルに用いたものと同じものが必要です。 バイナリパッケージを導入した場合はどのコンパイラでビルドされたものか 知る必要がありますが、UNIX系ではたいていgccです。

ruby.h, mkmf.rb

Rubyをソースコードから自分でビルドした場合はインストールされていると 思いますが、バイナリパッケージを導入した場合はruby.hやmkmf.rbがあるか 確認する必要があります。開発用パッケージとして別パッケージで提供 されていることがあります。

narray.h, narray_config.h

NArrayをインストールすれば通常は一緒にインストールされると思いますが、 もし無ければNArrayのソースコードをダウンロードして展開し、

% ruby extconf

としてください。narray.hとnarray_config.hを適当な場所に置きます。 通常はnarray.soと同じディレクトリに置きます。

Swig

Swig はC/C++の関数をスクリプト言語から呼び出せるようにするための ラッパー関数自動生成ツールです。 各プラットフォームでバイナリパッケージが提供されています。 無ければSwigのホームページからソースコードをダウンロードして コンパイルしてください。

このチュートリアルではSwig Version 1.3系列の使用を前提にしています。 確認はすべてSwig Version 1.3.25にて行いました。


まずはC言語プログラムを呼び出す

まずは一番簡単なものでRubyからの呼び出し方を練習してみます。 よくある練習「Hello, world!」です。

hello.c

#include <stdio.h>
void hello()
{
  printf("Hello, world!\n");
}

これをRubyから呼び出すには、Rubyのメソッドとして登録するコードが 必要になります。 ここではライブラリ名を"test"としています。

test1.c

#include "ruby.h"

void hello();

VALUE wrap_hello(self)
     VALUE self;
{
  hello();
  return Qnil;
}

void Init_test()
{
  VALUE module;

  module = rb_define_module("Test");
  rb_define_module_function(module, "hello", wrap_hello, 0);
}

Rubyでrequire 'test'とすればこのライブラリが読み込まれます。 Rubyはライブラリを読み込んだらまず 「Init_ライブラリ名」 という関数を実行します。 ここにC言語で書いた関数をメソッドとして登録するコードを書きます。 ここではTestというモジュールを作成して、 そこにhelloという名前でモジュール関数を登録しています。

モジュールを作成するには以下の関数を用います。

VALUE rb_define_module(const char *name);

ここでVALUE型構造体とはRubyのオブジェクトを格納している構造体です。

C言語の関数をRubyのモジュール関数として登録するにはいくつかの方法がありますが、 ここでは最も簡単な方法を示します。

void rb_define_module_function
           (VALUE module, const char *name, VALUE (*func)(), int argc);

Rubyから直接呼び出す関数は戻り値としてRubyのオブジェクトを返す 必要があります。 今は特に何も戻り値が必要でないので、Rubyの「nil」に 対応する「Qnil」という定数を与えています。

このプログラムをコンパイルしてみましょう。 Rubyには拡張ライブラリをコンパイルするためのMakefileを作る仕組みが あります。 必要なソースコードを一つのディレクトリにまとめ、同じディレクトリに 「extconf.rb」という名前のファイルを作り、以下の内容を書きます。

extconf.rb

require 'mkmf'
create_makefile('test')

ここではtestという名前で拡張ライブラリを作るための Makefileを作成するように指示しています。

Makefileを作り、Makeします。

% ruby extconf.rb
% make

これにより「test.so」ができます(環境によっては拡張子が異なるかもしれません)。

これをRubyから実行してみましょう。

% ruby -r'test' -e'Test.hello'
Hello world!


簡単な数値のやりとり

まずはRubyから呼び出したい関数を作ってみましょう。 例として二つの数値を足す関数を考えます。

add.c

int add(a, b)
     int a, b;
{
  return( a + b );
}

この関数が正しく動くことを確認するために、 これを呼び出すCのプログラムを書いてみましょう。

main.c

#include <stdio.h>

int add(int a, int b);

int main()
{
  int a, b;

  a = 1;
  b = 2;
  printf( "%d + %d = %d\n", a, b, add(a, b) );
}

% cc -o add.exe add.c main.c
% ./add.exe
1 + 2 = 3

正しく動くことが確認できました。

これをRubyから呼び出すには、データの変換が必要です。 Rubyでは数値を含めすべてのものがオブジェクトとして扱われます。 個々のオブジェクトはCの構造体として実装されています。 ですからC言語で書いたライブラリと数値のやりとりをするには 構造体から数値を取り出したり、数値を構造体に入れる必要があります。 整数型や浮動小数点型は簡単に相互変換するマクロ、関数があります。

これらを用いてデータを変換しC言語で書いた関数を呼び出すラッパー関数を 作ってみます。

test2.c

#include "ruby.h"

int add(int a, int b);

VALUE wrap_add(self, aa, bb)
     VALUE self, aa, bb;
{
  int a, b, result;
  
  a = FIX2INT(aa);
  b = FIX2INT(bb);
  result = add(a,b);
  return INT2FIX(result);
}

void Init_test()
{
  VALUE module;

  module = rb_define_module("Test");
  rb_define_module_function(module, "add", wrap_add, 2);
}

Makefileを作り、Makeします。extconf.rbは先程と同じものを用います。

% ruby extconf.rb
% make

Rubyから実行してみましょう。

% ruby -r'test' -e'Test.add(1,2)'
3


NArray配列を渡す

まずはRubyから呼び出したい関数を作ってみましょう。 例として一次元配列の全要素をかけ算する関数を考えます。

mul_all.c

float mul_all(array,nx)
     float array[];
     int nx;
{
  float result = 1.0;
  int i;

  for(i=0; i<nx; i++){
    result = result * array[i];
  }
  return(result);
}

この関数が正しく動くことを確認するために、 これを呼び出すCのプログラムを書いてみましょう。

main.c

#include <stdio.h>

float mul_all(float array[], int nx);

int main()
{
  int n = 5;
  float a[n];
  int i;
  
  for(i=0; i<n; i++){
    a[i] = i+1;
    printf("%f\n",a[i]);
  }
  printf("mul_all()=%f\n", mul_all(a,n));
}
% cc -o mul_all.exe mul_all.c main.c
% ./mul_all.exe
1.000000
2.000000
3.000000
4.000000
5.000000
mul_all()=120.000000

正しく動くことが確認できました。

これをRubyから呼び出す際に前回と異なるのは オブジェクトの変換です。 NArrayはnarray.hの中で以下のような構造体として定義されています。 Rubyのオブジェクトを格納しているVALUE構造体にはこのNARRAY構造体への ポインタが格納されています。

/* struct for Numerical Array */
struct NARRAY {
  int    rank;    /* # of dimension */
  int    total;   /* # of total element */
  int    type;    /* data type */
  int   *shape;
  char  *ptr;     /* pointer to data */
  VALUE  ref;     /* NArray object wrapping this structure */
};

必要なラッパー関数は以下のようになります。

test3.c

#include "ruby.h"
#include "narray.h"

float mul_all(float array[], int nx);

VALUE wrap_mul_all(self, na)
     VALUE self, na;
{
  VALUE na2;
  struct NARRAY *n_na;
  float result;

  na2 = na_cast_object(na, NA_SFLOAT);
  GetNArray(na2,n_na);
  result = mul_all((float*)n_na->ptr, n_na->total);
  return( rb_float_new(result) );
}

void Init_test()
{
  VALUE module;

  rb_require("narray");
  module = rb_define_module("Test");
  rb_define_module_function(module, "mul_all", wrap_mul_all, 1);
}

ここではGetNArrayでオブジェクトからNArrayの構造体を取り出し、 その中から配列へのポインタを取り出しています。 その際、na_cast_objectで配列をSFloat型に強制的に型変換しています。 もし型が違うとメモリ破壊エラーを起こすからです。 注意すべきこととしては、配列の中身を操作する際には ポインタ変数に代入を行ってはいけないということです。 ポインタの指す実メモリを読み書きしてください。 そうしないとオブジェクトが破壊され、メモリ破壊エラーを起こします。

このプログラムをコンパイルするのにはやはりextconf.rbを作成して Makefileを作る必要があります。ここではnarray.hを読み込んでいますので、 以下のように書きます。

extconf.rb

require 'mkmf'

dir_config('narray',$sitearchdir,$sitearchdir)

if ( ! ( have_header('narray.h') && have_header('narray_config.h') ) ) then
   print <<-EOS
   ** configure error **  
   Header narray.h or narray_config.h is not found. If you have these files in 
   /narraydir/include, try the following:

   % ruby extconf.rb --with-narray-include=/narraydir/include

   EOS
   exit(-1)
end

if /cygwin|mingw/ =~ RUBY_PLATFORM
  unless have_library('narray')
   print <<-EOS
   ** configure error **  
   libnarray.a is not found.
   % ruby extconf.rb --with-narray-lib=/narraydir/lib

   EOS
   exit(-1)
  end    

end
 
create_makefile('test')

作成したライブラリをRubyから呼んでみましょう。 Cygwin以外の環境では一行目の require 'narray'は要らないかもしれません。

% ruby extconf.rb
% make
% irb
irb(main):001:0> require 'narray'
=> true
irb(main):002:0> require 'test'
=> true
irb(main):003:0> a = NArray.sfloat(5).indgen!(1,1)
=> NArray.sfloat(5): 
[ 1.0, 2.0, 3.0, 4.0, 5.0 ]
irb(main):004:0> Test.mul_all(a)
=> 120.0

上の例では関数をモジュール関数としてRubyに登録しましたが、 もうちょっとRubyらしい使い方をしてみましょう。 mul_allをNArrayのクラスメソッドとして登録してみます。 ラッパー関数を以下のようにします。

test4.c

#include "ruby.h"
#include "narray.h"

float mul_all(float array[], int nx);

VALUE wrap_mul_all(self)
     VALUE self;
{
  VALUE na2;
  struct NARRAY *n_na;
  float result;

  na2 = na_cast_object(self, NA_SFLOAT);
  GetNArray(na2,n_na);
  result = multiple((float*)n_na->ptr, n_na->total);
  return( rb_float_new(result) );
}

void Init_test()
{
  VALUE class_na;

  rb_require("narray");
  class_na = rb_const_get(rb_cObject, rb_intern("NArray"));
  rb_define_method(class_na, "mul_all", wrap_mul_all, 0);
}

ここではselfという引数の中に呼び出し元であるNArrayオブジェクトが 格納されています。

コンパイルして使ってみましょう。

% ruby extconf.rb
% make
% irb
irb(main):001:0> require 'narray'
=> true
irb(main):002:0> require 'test'
=> true
irb(main):003:0> NArray.sfloat(5).indgen!(1,1).mul_all
=> 120.0

ちょっとスマートになった気がしませんか。


Swigを使ってお手軽変換

ラッパー関数を作成する作業は、数が少ないときには 簡単ですが、数が増えると大変です。 特に大規模なライブラリの移植を考えているときには 同じような作業を繰り返さなくてはいけません。 このようなときに便利なものがあります。 ラッパー関数を自動生成してくれる Swig というツールがあります。

ここではSwig Version 1.3.25 (Compiled with g++ [i686-pc-cygwin]) を使って解説していきます。

簡単な数値のやりとり

まずは簡単な数値のやりとりから練習しましょう。 Swigを使ってラッパー関数を作るときには拡張子が「.i」のファイルに Cの関数のプロトタイプ宣言を記述します。 ソースコード本体からプロトタイプ宣言を切り離してヘッダーファイルに まとめている場合はヘッダーファイルを読み込めば作業が楽になります。

先程練習に使った関数です。

add.c

int add(a, b)
     int a, b;
{
  return( a + b );
}

これに対するラッパー関数を作成するには、 Swigに対する入力ファイルとして次のようなものを作成します。

test5.i

%module test
%{
%}

extern int add(int a, int b);

ここでは 「%module test」で「Test」というモジュールに 関数を作成することを指示しています。 それに引き続く「%{ ... %}」の中に必要なインクルードファイルなどを記述します。

これをSwigで処理してラッパー関数を作ります。

% swig -ruby test5.i

うまくいけば「test5_wrap.c」というファイルが生成されます。

コンパイルします。Makefileの作成にはやはり以下の内容のextconf.rbを利用します。

extconf.rb

require 'mkmf'
create_makefile('test')
% ruby extconf.rb
% make
% ruby -r'test' -e'p Test.add(1,2)'
3

自分でラッパー関数を書いたときと同じように動くことが分かります。

NArray配列を渡す

次に一次元NArray配列のかけ算を行う関数をラップしてみましょう。 mul_all.c に対するSwig入力ファイルは次のようになります。

test6.i

%module test
%{
#include "narray.h"
%}

%typemap(ruby,in) (float *NARRAY, int LENGTH ){
  VALUE na;
  struct NARRAY *n_na;

  na = na_cast_object($input, NA_SFLOAT);
  GetNArray(na, n_na);
  $1 = (float*)n_na->ptr;
  $2 = n_na->total;
}

extern float mul_all(float *NARRAY, int LENGTH);

NArrayは標準のデータ型ではないので、Swigに変換の仕方を教えてあげる必要が あります。それを行っているのが「%typemap」の部分です。

まず一行目の

%typemap(ruby,in) (float *NARRAY, int LENGTH ){

ですが、ここではどの引数に対する処理であるかを示しています。

(ruby,in)

はRubyに変換するときの処理で、関数への入力として用いられる引数に 対する処理であることを示しています。次に

(float *NARRAY, int LENGTH )

は、関数のプロトタイプ宣言において この名前が使われている引数の組に対する処理であることを示します。

二行目以降には以前ラッパー関数に書いたのと同じようにNARRAY構造体への ポインタを取り出すコードを書いています。 ここで用いられている特殊な変数名として 「$input」「$1」「$2」があります。 「$input」はRubyから与えられるVALUE型引数オブジェクトになります。 「$1」「$2」はC言語の関数へ渡す引数になります。 引数の型は一行目で指示したとおりになります。

ここでは二つの引数を一度に処理していますが、 このようにまとめて処理すると、Ruby側からは引数が一つしか見えません。 つまりNArrayオブジェクトを一つ引数として渡すと、 float型配列へのポインタとint型数値がmul_allに渡されます。 戻り値のfloat型数値はFloat型オブジェクトとしてRuby側に渡されます。

Swigで処理してコンパイルしてみましょう。 extconf.rb は「NArray配列を渡す」で用いたものを再利用してください。

% swig -ruby test6.i
% ruby extconf.rb
% make
% irb
irb(main):001:0> require 'narray'
=> true
irb(main):002:0> require 'test'
=> true
irb(main):003:0> Test.mul_all(NArray.sfloat(5).indgen!(1,1))
=> 120.0


参考文献