(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の拡張ライブラリを動的に作る]