(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__ == $0end にはさまれた部分) が利用例になってます。最後の

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 は、そのうちもっと強化して 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 は残りますので、消したければ陽に消してくだ さい。