(Tips) NArrayの拡張ライブラリを動的に作る(1)
作者:堀之内
ライブラリダウンロード: dynamicdl.rb
はじめに
NArray は C のポインタ にべた並びでデータを保持するので、大量の数値を扱うのに適しています。 NArray を使う場合は、できるだけ NArray のメソッドを駆使して、ループを 回さないのが処理を遅くしないコツです。例えば条件に応じて演算を変える場 合は、ループを使って if 文を使うのでなく、mask を使うというように。
しかし、やっぱり限度がありますよね。ループを使わないとできない(orしんどい) ことも多いでしょう。そんなときは C で拡張ライブラリを書けば高速 に処理できます。しかし、pure Ruby で書くよりはかなり敷居が高い。なによ り、拡張ライブラリを別ファイル (*.c) に書いて、ruby extconf.rb でコン パイルして... ということをするだけで、面倒ですし、プログラムの維持管理 も手間になります。
そこで登場。C による拡張ライブラリを Ruby ソースコードに埋め込み、自動 的にコンパイル&ロードさせる仕組みを紹介します。タネは簡単。拡張ライブ ラリのための Makefile を生成しコンパイルするスクリプトを埋め込んじゃう というわけです。ついでに、拡張ライブラリそのものを作り易くするため、 C と Ruby との間のデータの受け渡しに DL という Ruby の標準添付ライブラリを使います。
ライブラリ兼サンプルプログラム
次のプログラムでは、 DynamicDL というクラスを定義しています。
これが拡張ライブラリの動的な生成を担います。ファイル後半の
テスト部分(if __FILE__ == $0 と end にはさまれた部分)
が利用例になってます。最後の
Test.test_str("Hello world")
na = NArray[9.0,-3.5]
Test.test_double(na.to_s,na.length)
p Test.negative2zero(na)
が、拡張ライブラリを呼んでいるところです。モジュール
Test のメソッド test_str, test_double は、
それぞれ同名の C の関数として定義されていて、String や
Float, Integer のデータを引数にとります。これらの
組み込み型は DL が自動的に Ruby と C の間の
データ変換を行ってくれます。最後の negative2zero 
は、NArray を引数とします。DL は、NArray は知りませんが、
NArray#to_s や NArray.to_na を使えば、文字列と相互変換できますので、
C の関数にちょっとした Ruby ベースのラッパをかぶせることで
簡単に引数にできます。
では、どうぞ:
プログラム dynamicdl.rb
- ダウンロード: dynamicdl.rb
ここで定義してるクラスは DynamicDL は、そのうちもっと強化して Library として登録したいと思っています。
# = Ruby の標準ライブラリ DL を使って NArray の拡張ライブラリを動的に作る
#
#    (C) 堀之内武 2008/04/26
#    LICENCE: Ruby's
# 
# * クラス DynamicDL -- Ruby標準の DL の応用ライブラリ
# * テストプログラム -- NArray 用のサンプル
require "dl/import"
require "mkmf"
# = Ruby の標準添付ライブラリ DL を使って、動的に C コードを生成する
# 
#    (C) 堀之内武 2008/04/26
#    LICENCE: Ruby's
#
# 注意: Cソースや make ファイル、ライブラリファイルはカレントディレクトリ
# に作成する。
#
# == 使用法 
# (本ファイルのテスト部分を参考にせよ.)
# 
# 例えばソース, ライブラリを foo.c, foo.so という名前とし、Foo という
# モジュールで使えるようにするには次のようにする
# 
#   module Foo
#     extend DL::Importable
#     code = <<-'EOS'
#       .... ここに C のコードを書く
#     EOS
#     ext = DynamicDL.new(code, self.to_s.downcase)
#     dlload(ext.make)
#     ext.proto.each{|prt| extern(prt)}
#   end
# 
# なお、NArray 用には下記のテストプログラムのように alias で便利な
# メソッドを作ると良い。
# 
class DynamicDL
  PREFIX = "#include <ruby.h>\n"
  def initialize(code, libname)
    @code = PREFIX + code
    @libname = libname
    @srcname = @libname + '.c'
  end
  # コード生成
  def code
    @code
  end
  # コードダンプ (強制的)
  def dump
    @src = File.open(@srcname,'w'){|f| f.print(code)}
  end
  # コードダンプ (ファイルがあれば聞く)
  def dump_i
    if File.exists?(@srcname)
      print "File #{@srcname} exists. Overwrite it? [Yn]; "
      ans = gets
      raise("Execution stopped") if /^n/ =~ ans
    end
    dump
  end
  # コードダンプ (ファイルがないか一致しない場合)
  def dump_if_dif
    if !File.exists?(@srcname)
      dump
    else
      if code != File.read(@srcname)
        dump
      end
    end
  end
  # コンパイルする
  def make
    dump_if_dif
    create_makefile(@libname)  # これも必要なときのみにしたいが...
    print "Compiling library #{@srcname}\n"
    system('make') || raise("Compilation failed.")
    libflname = @libname+'.so'
    if !File.exists?(libflname)
      raise("Library #{libflname} does not exist. May in another name?") 
    end
    libflname
  end
  DEF_PAT = /\s*\/\/\s*DEF\s*$/
  def proto
    proto = code.grep(DEF_PAT).collect{|l| 
      l.sub(DEF_PAT,"").gsub(/\s*[\w_]+\s*([,\)])/,'\1')
    }
    raise("Fucntion definition must end with '// DEF'") if proto.nil?
    proto
  end
end
if __FILE__ == $0
  module Test
    extend DL::Importable
    #< 拡張ライブラリコード >
    code = <<-'EOS'
      void test_str(const char *a)  // DEF
      {
        printf("%s\n",a);
      }
      void test_double(const double *a, int a_len)  // DEF
      {
        int i;
        for(i=0;i<a_len;i++){
          printf("%f\n", a[i]);
        }
      }
      double *negative2zero(const double *a, int a_len)  // DEF
      {
        int i;
        double *b;
        b = xmalloc(a_len*sizeof(double));
        for(i=0;i<a_len;i++){
          if (a[i] >= 0){
            b[i] = a[i];
          } else {
            b[i] = 0.0;
          }
        }
        return(b);
      }
    EOS
    #< DLによりモジュール関数に >
    ext = DynamicDL.new(code, self.to_s.downcase)
    dlload(ext.make)
    ext.proto.each{|prt| extern(prt)}
    #< NArray 処理用に、より便利なメソッドを定義 >
    alias _negative2zero_ negative2zero
    module_function :_negative2zero_
    def negative2zero(na)
      na = na.to_type(NArray::FLOAT) if na.typecode != NArray::FLOAT
      len = na.length
      ptr = _negative2zero_(na.to_s, len)
      str = ptr.to_s(len*DL.sizeof('d'))
      NArray.to_na(str, NArray::FLOAT, *na.shape)
    end
    module_function :negative2zero
  end
  require "narray"
  Test.test_str("Hello world")
  na = NArray[9.0,-3.5]
  Test.test_double(na.to_s,na.length)
  p Test.negative2zero(na)
end
    実行&解説
上記の dynamic.rb をダウンロードします。いま、カレントディレクトリには このファイルしかないとしましょう:
% ls -l 合計 4 -rw-r--r-- 1 horinout horinout 3757 4月 26 22:56 dynamicdl.rb
ここで、dynamicdl.rb を実行します。
% ruby dynamicdl.rb creating Makefile Compiling library test.c gcc -I. -I/usr/local/lib/ruby/1.8/i686-linux -I/usr/local/lib/ruby/1.8/i686-linux -I. -fPIC -g -O2 -c test.c gcc -shared -L'/usr/local/lib' -Wl,-R'/usr/local/lib' -o test.so test.o -ldl -lcrypt -lm -lc Hello world 9.000000 -3.500000 NArray.float(2): [ 9.0, 0.0 ]
メッセージをみると、Makefile が作られ、(動的に作られた)test.c という ファイルがコンパイルされ、ライブラリが作られていることがわかります。 そして、Hellow world 以下、実行結果が表示されています。
ここで、ディレクトリの中味を見ると、次のようになります:
% ls -l 合計 43 -rw-r--r-- 1 horinout horinout 3406 4月 30 19:43 Makefile -rw-r--r-- 1 horinout horinout 3757 4月 26 22:56 dynamicdl.rb -rw-r--r-- 1 horinout horinout 588 4月 30 19:43 test.c -rw-r--r-- 1 horinout horinout 16036 4月 30 19:43 test.o -rwxr-xr-x 1 horinout horinout 17899 4月 30 19:43 test.so*
DynamicDL は、ラッパーをたばねるモジュール定義の中で使います。ライブラ
リ名は DynamicDL.new の第2 引数できまります。ここでは、
Test というモジュール定義において、
ext = DynamicDL.new(code, self.to_s.downcase)
としていますので、小文字の test になります。C ソースは、
これに .c がついたもの。ライブラリファイル名は、多くのプラットフォーム
では .so がついたものとなるでしょう。
DynamicDL は、実行時のディレクトリに Makefile や C ソース、
ライブラリを作ります。
出来たファイルを消す場合、
make distclean
とします。それでも、test.c は残りますので、消したければ陽に消してくだ さい。
キーワード:[拡張ライブラリ] [NArray] [DynamicDL]
参照:[(Library) NArrayExt: NArrayの拡張ライブラリを動的に作る]