Ruby Programing for FORTRAN 〜SWIGを使う(6)〜

内容

文字列等、長さを一緒に渡す必要のあるFORTRANの関数を拡張ライブラリでラップする方法を説明します。

.

これまでFORTRANの型のうち、INTEGER、配列、複素数を利用した関数についてSWIGでのラッピングの方法を説明してきました。今回、基本的な型の説明の締めくくりとして文字列を説明た後変化形として基本編2で意図的に説明を省いた可変長配列にについて説明することにします。
ではおなじみの例題FORTRAN関数です。

%cat test5.f
        CHARACTER *(*) FUNCTION FILL(C, N)
        CHARACTER C
        INTEGER N,I

        FILL = ''
        DO I = 1,N
        FILL(I:I) = C
        END DO
        FILL(N+1:N+1) = CHAR(0)
        RETURN
        END
   

遂にaddから脱出(^_^) fillはcで指定された文字n個で埋めた文字列を返します。まあ、FORTRANの関数自体はそうたいしたことはないですね。ちょっと気をつける必要があるのはCの文字列は'\0'で終わるのでCに文字列を渡そうとする場合最後に'\0'をつけなければいけない点でしょうか。
が、Cから呼ぶと....非常に厄介です....

%cat test5-c.c
#include 

int main(void)
{
  char ret[10], ch;
  int n, ret_len, ch_len;

  ch_len = sizeof(ch);
  ch = '*';
  ret_len = sizeof(ret);
  n = 9;

  fill_(ret, ret_len, &ch, &n, ch_len);

  printf("%s\n", ret);
  return(0);
}
   

なんでしょう。この引数の多さは....2つの引数が5つにまで増殖してしまっています。 最初の2つが受け取る文字列の格納場所と(格納場所の)長さを指定する引数でその次2つがが本来の引数。 そして最後に文字列の長さがきます。 あ、あと、文字列の長さはポインタでなく値で渡すことにも注意してください。 ところでfillはFORTRANでは第1引数はCHARACTERになっているのになぜCHARACTER*と同じ扱いにな手しまっているのか?と思われるかもしれません。 確かにFORTRANのコードだけ見れば受け取るのは間違いなく1つの文字です しかし結局のところCから渡すポインタで渡すことになるため、これは1の長さの文字列と解釈しても何ら問題はありません。 こういった場合f77は1の長さの文字列として解釈するようになっているのです。 紛らわしいことは確かですが.....
さてコンパイルして試してみましょうといいたいところですが、今回はちょっと工夫が要ります。 いつも通りにやると.....

%gcc -o test5 test5-c.c test5.o
test5.o: In function `fill_':
test5.o(.text+0x36): undefined reference to `s_copy'
test5.o(.text+0x7a): undefined reference to `s_copy'
test5.o(.text+0xb6): undefined reference to `s_copy'
   

s_copyへの参照が見つからないと文句をいわれてしまいました。 s_copyってなんでしょうか.....結論から言ってしまうとこれはf77が内部で利用するライブラリ内に存在する関数です。 man f77してもらうと分かると思いますがそれはlibg2cがそのランタイムです。 FreeBSD4.3-RELEASEでは/usr/lib/libg2c.soとなっていました。ではこれを指定してやればよさそうです。

%gcc -o test5 test5-c.c test5.o -lg2c
/usr/lib/libg2c.so: warning: tempnam() possibly used unsafely; consider using mkstemp()
/usr/lib/libg2c.so: undefined reference to `log'
/usr/lib/libg2c.so: undefined reference to `sqrt'
/usr/lib/libg2c.so: undefined reference to `cosh'
/usr/lib/libg2c.so: undefined reference to `floor'
/usr/lib/libg2c.so: undefined reference to `j1'
/usr/lib/libg2c.so: undefined reference to `y0'
/usr/lib/libg2c.so: undefined reference to `yn'
/usr/lib/libg2c.so: undefined reference to `erf'
/usr/lib/libg2c.so: undefined reference to `drem'
/usr/lib/libg2c.so: undefined reference to `cos'
/usr/lib/libg2c.so: undefined reference to `tanh'
/usr/lib/libg2c.so: undefined reference to `sin'
/usr/lib/libg2c.so: undefined reference to `atan2'
/usr/lib/libg2c.so: undefined reference to `pow'
/usr/lib/libg2c.so: undefined reference to `sinh'
/usr/lib/libg2c.so: undefined reference to `jn'
/usr/lib/libg2c.so: undefined reference to `exp'
/usr/lib/libg2c.so: undefined reference to `tan'
/usr/lib/libg2c.so: undefined reference to `atan'
/usr/lib/libg2c.so: undefined reference to `asin'
/usr/lib/libg2c.so: undefined reference to `j0'
/usr/lib/libg2c.so: undefined reference to `acos'
/usr/lib/libg2c.so: undefined reference to `y1'
/usr/lib/libg2c.so: undefined reference to `erfc'
   

うーん、今度はlibg2c.soで参照できない関数があるっていわれてしまいました。 今度は何が足りないのでしょうか.....でもよく見るとtanとかcosとかあって数値計算っぽい? ということはlibmでしょうか?

%gcc -o test5 test5-c.c test5.o -lm -lg2c
/usr/lib/libg2c.so: warning: tempnam() possibly used unsafely; consider using mkstemp()
   

さて、実行。

%./test5
*********
   

意図どおり動いていますね.....(最初これを見たときは感動しましたよ(^_^))
さて、前準備が長くなりすぎましたがそろそろ本題に入りましょう。

%cat test5.i
%module test5
%include typemaps.i

%typemap(ruby,in) char *INPUT {
  int len;
  len = RSTRING($source)->len;
  $target = (char*)malloc(len + 1);
  strncpy($target, RSTRING($source)->ptr, len);
  $target[len] = '\0';
}

%typemap(ruby,ignore) int STRLEN {
  $target = 0;
}

%typemap(ruby,ignore) char *OUTPUT {
  char buf[256];
  $target = buf;
}

%typemap(ruby,argout) char *OUTPUT {
  $target = rb_str_new($source, strlen($source));
}

%typemap(ruby,freearg) char *INPUT {
  free($source);
}

void fill_(char *OUTPUT, int STRLEN, char *INPUT, int *INPUT, int STRLEN);
   

文字列の長さ以外はあまり問題ないでしょう。 少し特殊なのは第1引数ですが、前回紹介したignoreを利用してRuby側からは見えないようにしているだけです。 また、どれだけ配列を確保しておいたらいいのかも決めかねるところですが、とりあえず256としました。
さて、一番のメインは文字列の長さをどう扱うかです。 Rubyの場合、文字列クラスに長さも含まれているわけですから当然

test(str, len)
   

のようにはしたくないですよね。しかし、SWIGには引数同士の関係を定義する手段はありません。 今回のfill_でいえば第1引数と第2引数は文字列とその長さという関係があるわけですがそれを伝える手段はありません。 第2引数に第1引数にRubyが渡す値を利用することは非常に困難です。 が、ちょっと考えてください。SWIGを使う目的はあくまでコーディングの手間を省くということです。 別にすべてSWIGだけでできなくても問題はないのではないでしょうか? もっともSWIGを使ったがために余計コストがかかってしまっては意味がありませんが。 まあ、今の段階ではどのくらいか分かりませんのでまずは試してみる事にしましょう。 とりあえずignoreで見えなくしておき、0で初期化しておきます。
さて、生成します。生成したコードtest5_wrap.cのうち、fill_のラッパ部分を以下に示します。

static VALUE
_wrap_fill_(int argc, VALUE *argv, VALUE self) {
    VALUE varg0 ;
    VALUE varg1 ;
    VALUE varg2 ;
    VALUE varg3 ;
    VALUE varg4 ;
    char *arg0 ;
    int arg1 ;
    char *arg2 ;
    int *arg3 ;
    int arg4 ;
    char buf[256] ;
    int temp ;
    VALUE vresult = Qnil;

    {
        arg0 = buf;
    }
    {
        arg1 = 0;
    }
    {
        arg4 = 0;
    }
    rb_scan_args(argc, argv, "20", &varg2, &varg3);
    {
        int len;
        len = RSTRING(varg2)->len;
        arg2 = (char*)malloc(len + 1);
        strncpy(arg2, RSTRING(varg2)->ptr, len);
        arg2[len] = '\0';
    }
    {
        temp = NUM2INT(varg3);
        arg3 = &temp;
    }
    fill_(arg0,arg1,arg2,arg3,arg4);
    {
        vresult = rb_str_new(arg0, strlen(arg0));
    }
    {
        free(arg2);
    }
    return vresult;
}
   

arg1 = 0; arg4 = 0;が先ほどignoreで指定した部分です。ここを修正することになります。 あと、rb_scan_argsを前に持ってきます。

    rb_scan_args(argc, argv, "20", &varg2, &varg3);
    {
        arg0 = buf;
    }
    {
        //arg1 = 0;
        arg1 = sizeof(buf);
    }
    {
        //arg4 = 0;
         arg4 = RSTRING(varg2)->len;
    }
    //rb_scan_args(argc, argv, "20", &varg2, &varg3);
   

思ったよりもコストは大きくないのではないでしょうか? これくらいなら許容範囲と私は思います。
さて、あとは拡張ライブラリをこれらに基づいてつくるわけですが、 Makefileにいつもに加え、Cでテストしたときのライブラリをリンクするよう修正をする必要があります。

#LIBS = -L. -l$(RUBY_INSTALL_NAME) -l
LIBS = -L. -l$(RUBY_INSTALL_NAME) -lc -lm -lg2c
   

あとはテストするだけですね。

%make clean
%make
f77 -fPIC -c -o test5.o test5.f
cc -fPIC -D_THREAD_SAFE -O -pipe  -fPIC -I/usr/local/lib/ruby/1.6/i386-freebsd4.3
 -I/usr/local/include -c -o test5_wrap.o test5_wrap.c
cc -shared -Wl,-soname,test5.so -L/usr/local/lib -o test5.so test5.o test5_wrap.o
 -L. -lruby -lc -lm -lg2c
%ruby -e "require 'test5.so'; p Test5.fill_('*', 9)"
"*********"
   

ところで、fill_('**', 9)としてしまっても同じ結果が起きてしまいます。 test5_wrap.cのarg4の部分を

    rb_scan_args(argc, argv, "20", &varg2, &varg3);
    {
        arg0 = buf;
    }
    {
        //arg1 = 0;
        arg1 = sizeof(buf);
    }
    {
        //arg4 = 0;
        if (RSTRING(varg2)->len != 0) {
          rb_raise(rb_eTypeError, "length must be 1");
          return(vresult);
        }
        arg4 = RSTRING(varg2)->len;
    }
    //rb_scan_args(argc, argv, "20", &varg2, &varg3);
   

のようにした方がいいのかもしれません。

さて、文字列が終わったので最後に可変長の配列のFORTRANの関数をラップする方法を簡単に説明します。

%cat test6.f
        INTEGER FUNCTION ADD(V,NV)

        INTEGER I, TOTAL, V(NV)
        TOTAL=0

        DO I=1,NV
        TOTAL=TOTAL+V(I)
        END DO

        ADD=TOTAL
        RETURN
        END
%
%cat test6-c.c
#include 

int main(void)
{
  int n = 256;
  int i, sum[n];

  for(i = 0; i < n; i++)
    sum[i] = i;

  printf("%d\n", add_(sum, &n));
  return(0);
}
%
%gcc -o test6 test6.o test6-c.c
%./test6
32640
   

test3.fの焼き直しをtest6.fとしてこれをベースにします。 与える配列の長さを引数に取ることで柔軟性が大幅に上がっていますね。 (というよりも、固定長だとまったく使えないといってもいいかも;p) あとは文字列の時と同じような考え方でtest6.iをつくり、コードを生成し、修正を加えます。

%cat test6.i
%module test6
%include typemaps.i

%typemap(ruby,in) int *INTARRAY {
  int i, len;

  len = RARRAY($source)->len;
  $target = (int*)malloc(sizeof(int)*len);
  for (i = 0; i < len; i++)
    $target[i] = NUM2INT(rb_Integer(RARRAY(varg0)->ptr[i]));
}

%typemap(ruby,ignore) int *LENGTH(int tmp) {
  tmp = 0;
  $target = &tmp;
}

%typemap (ruby, freearg) int *INTARRAY {
  free($source);
}

%apply int *INTARRAY { int *argv }
%apply int *LENGTH {int *len }
int add_(int *argv, int *len);
   

以下が生成されるコード。

static VALUE
_wrap_add_(int argc, VALUE *argv, VALUE self) {
    VALUE varg0 ;
    VALUE varg1 ;
    int *arg0 ;
    int *arg1 ;
    int tmp ;
    int result ;
    VALUE vresult = Qnil;

    {
      tmp = 0;
      arg0 = &tmp;
    }
    rb_scan_args(argc, argv, "10", &varg0);
    {
        int i, len;

        len = RARRAY(varg0)->len;
        arg0 = (int*)malloc(sizeof(int)*len);
        for (i = 0; i < len; i++)
        arg0[i] = NUM2INT(rb_Integer(RARRAY(varg0)->ptr[i]));

    }
    result = (int )add_(arg0,arg1);
    vresult = INT2NUM(result);
    {
        free(arg0);
    }
    return vresult;
}
   

これに次のような修正を加えます。

    rb_scan_args(argc, argv, "10", &varg0);
    {
        //tmp = 0;
        tmp = RARRAY(varg0)->len;
        arg1 = &tmp;
    }
    //rb_scan_args(argc, argv, "10", &varg0);
   

拡張ライブラリを作る方法はtest3の場合と変わりません。 違うのはその使い方。

%ruby -e "require 'test6.so'; p Test6.add_([1*100])"
100
   

無事、固定長から脱することができました。わーい。

.

前に戻る [目次] 次に進む