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