Swig de C++ to Ruby 〜C++のクラスから Rubyのクラスへ〜

はじめに

Rubyと同じく、C++もクラスをもっています。 SwigをつかえばC++のクラスをそのままRubyのクラスにすることができます。 SwigのドキュメントにC++の章がありますので、 まずはそちら "5 SWIGとC++" を読みましょう。

C++で書かれたあるソフトをswigを利用してラップする場合、 インストールされるヘッダーファイルがたいへん役に立ちます。 運が良ければそのヘッダーファイルがそのまま使えますが、 多くの場合多少変更が必要となります。 ここでは、 C++で書かれたソフトをラップする際に必要ないくつかの変更の解説を行います。

まずはヘッダーファイルをそのまま使ってみよう

とりあえずヘッダーファイルをそのまま使ってみましょう。 そのままで問題なくコンパイルでき、かつ使えたあなたは幸運です。 だめな場合でもエラーメッセージがとても重要な情報となります。 以下のようにすべてのヘッダーファイルを列挙します。

%cat test.i

%module test
%{
#include "hoge.h"
#include "fuga.h"
%}
%include hoge.h
%include fuga.h
   
%swig -ruby -c++ test.i
   

エラーとなりましたか?
もしくは警告が出ましたか?
エラー等がでた場合は、 ヘッダーファイルをコピーして、修正しましょう。 警告は無視していいことが多いです。

継承

あるクラスが違うファイル中に定義されたクラスを継承している場合、 そのスーパークラスを定義したファイルを先にインクルードする必要があります。

%cat hoge.h

class A {
}
   
%cat fuga.h

#include "hoge.h"
class B : public A {
}
   
%cat test.i

%module test
%{
#include "hoge.h"
#include "fuga.h"
%}
%include hoge.h
%include fuga.h
   

この "%include" する順番が大切です。 以下のようにすると、 親クラスの関数が子クラスの関数として定義されません

%include fuga.h
%include hoge.h
   

入れ子のクラスや構造体

クラスや構造体が入れ子になっているばあいはエラーとなります。 そのばあい以下の様に外に出してやります。

%cat hoge_before.h

class A {
  class B {
  }
}
   
%cat hoge_after.h

class A {
}
class A::B {
}
   

クラスを返す関数

関数の戻値が数値や文字などの場合はswigが自動で Ruby のオブジェクトに変換してくれます。 戻値がクラスやポインタの場合、 swigはrubyのオブジェクトを自動で作ってくれるのはいいのですが、 そのままでは使えません。 自分で typemap を書いてやる必要があります。

%cat hoge.h

class A {
public:
 (A*) clone();
}
   
%typemap(ruby,out) A* {
  $result = SWIG_NewPointerObj((void*)$1, SWIGTYPE_p_A, 1);
}
   

GC

C++クラスのコンストラクタが public として定義されている場合、 swigは自動的に new クラスメソッドを作ってくれます。
また、デストラクタが public として定義されていれば、 GC時に呼び出すようにしてくれます。 したがって普通の場合はメモリ管理に気を使うことはありません。 しかしそうでない場合は自分で書いてやる必要があります。

%cat hoge.h

class A {
protected:
  A();
  ~A();
}
   
%freefunc A "free_A";
%{
void free_A(void*)
{
   //way to delete
}
%}
   

ファイル数が非常に多い場合

swigファイルは基本的には一つにするのが簡単です。 しかしヘッダーファイルが非常に多い場合、 一つのファイルにするとコンパイル時に大量のメモリを消費します。 ここではswigファイルを分割します。
ファイルをまたがった継承がある場合、 "%import" する必要があります。

%cat hoge.h

class A {
}
   
%cat fuga.h

#include "hoge.h"
class B : public A {
}
   
%cat hoge.i

%module test
%{
#include "hoge.h"
%}
%include hoge.h
   
%cat fuga.i

%module test
%{
#include "fuga.h"
%}
%import hoge.h
%include fuga.h
   

以下のように swig を実行しましょう

%swig -c++ -ruby -c -feature hoge hoge.i
%swig -c++ -ruby -c -feature fuga fuga.i
   

これらを読み込む C++ ファイルを作ります

%cat test.cxx

#if defined(_WIN32) || defined(__WIN32__) || defined(__CYGWIN__)
#  if !defined(STATIC_LINKED)
#    define SWIGEXPORT(a) __declspec(dllexport) a
#  else
#    define SWIGEXPORT(a) a
#  endif
#else
#  define SWIGEXPORT(a) a
#endif

extern "C" {

extern SWIGEXPORT(void) Init_hoge(void);
extern SWIGEXPORT(void) Init_fuga(void);

SWIGEXPORT(void) Init_test(void){
  Init_hoge(void);
  Init_fuga(void);
}

   

Init_* を呼ぶ順番も継承に気をつけてください。

定数

swig は "#define" で定義された定数をRubyの定数にしてくれます。

%cat hoge.h

#define VERSION "1.1.1"
#define IJ 1
#define XY 0.5
   
%cat hoge_wrap.cxx

	ship
rb_define_const(mTest,"VERSION", rb_str_new2("1.1.1"));
rb_define_const(mTest,"IJ", INT2NUM(1));
rb_difine_const(mTest,"XY", rb_float_new(0.5));
	ship
   

それぞれ適切な形に変換されているのが分かります。
これらの値はswigを実行した環境のものがc++のソースに埋め込まれます。 したがって、このrubyラッパーを配布する場合、 バージョン番号等コンパイルする環境に依存するものがあるので、 c++のソースではなく、swigファイルとヘッダーファイルを配布して、 それぞれの環境でswigから実行してもらわなくてはなりません。

そこでちょっと工夫をしてみましょう。
/usr/share/swig/1.3.24/ruby/rubyprimtypes.swg
を見てください(path はお手元の環境に合わせて適当に変えてください)。 160行目あたりから

%typemap(constant) int, short, long, signed char
        "rb_define_const($module,\"$symname\", INT2NUM($1));";

%typemap(constant) unsigned int, unsigned short, unsigned long, unsigned char
        "rb_define_const($module,\"$symname\", UINT2NUM($1));";

%typemap(constant) long long
        "rb_define_const($module,\"$symname\", LL2NUM($1));";

%typemap(constant) unsigned long long
        "rb_define_const($module,\"$symname\", ULL2NUM($1));";

%typemap(constant) double, float
        "rb_define_const($module,\"$symname\", rb_float_new($1));";

%typemap(constant) bool
        "rb_define_const($module,\"$symname\", ($1 ? Qtrue : Qfalse));";
    

となっているのが分かります。 同様に rubystrings.swg には文字や文字列の場合が書かれています。

これを参考に自分のswigファイルに以下を加えましょう。

%typemap(constant) int, short, long, signed char
        "rb_define_const($module,\"$symname\", INT2NUM($name));";

%typemap(constant) unsigned int, unsigned short, unsigned long, unsigned char
        "rb_define_const($module,\"$symname\", UINT2NUM($name));";

%typemap(constant) long long
        "rb_define_const($module,\"$symname\", LL2NUM($name));";

%typemap(constant) unsigned long long
        "rb_define_const($module,\"$symname\", ULL2NUM($name));";

%typemap(constant) double, float
        "rb_define_const($module,\"$symname\", rb_float_new($name));";

%typemap(constant) bool
        "rb_define_const($module,\"$symname\", ($name ? Qtrue : Qfalse));";

%typemap(constant) char
        "rb_define_const($module,\"$symname\", rb_str_new($name,1));";

%typemap(constant) char *
        "rb_define_const($module,\"$symname\", rb_str_new2($name));";

%typemap(constant) SWIGTYPE*, SWIGTYPE &, SWIGTYPE []
        "rb_define_const($module,\"$symname\", SWIG_NewPointerObj((void *) $name, $name_descriptor,0));";

%typemap(constant) SWIGTYPE "rb_define_const($module,\"$symname\", SWIG_NewPointerObj((void *) &$name, &$name_descriptor, 0));";

%typemap(constant) SWIGTYPE (CLASS::*) "rb_define_const($module, \"$symname\", SWIG_NewPackedObj((void *) &$name, sizeof($type), $name_descriptor));";
    

こんどは、上記の "hoge.h" が

%cat hoge_wrap.cxx

	ship
rb_define_const(mTest,"VERSION", rb_str_new2(VERSION));
rb_define_const(mTest,"IJ", INT2NUM(IJ));
rb_difine_const(mTest,"XY", rb_float_new(XY));
	ship
   

となります。 これでこのc++ファイルを配布すればコンパイルした環境のヘッダーファイルにあわせて 適切な値が入ることが分かります。