Python 2.5から、ctypesというライブラリが標準ライブラリに追加されました。このライブラリは、Cの関数をPython用にラッピングしてくれるライブラリで、PythonからCのライブラリ(.soとか.aとか.dllとか、環境による)中の関数を「直接」呼び出すことができます。
似たような仕組みには、SWIGや、PythonのC拡張モジュールなどがありますが、ctypesはそれらの仕組みと比べ、圧倒的に手軽なのが売りです(だと思います)。
たとえば、Python 2.5以上であれば、インタラクティブモード上で、
のように、いきなりlibcのputs関数を呼び出すことができます(インタラクティブモードなので、戻り値の「13」が表示されています)。
似たような仕組みには、SWIGや、PythonのC拡張モジュールなどがありますが、ctypesはそれらの仕組みと比べ、圧倒的に手軽なのが売りです(だと思います)。
たとえば、Python 2.5以上であれば、インタラクティブモード上で、
>>> from ctypes import *
>>> libc = CDLL("libc.so.6")
>>> libc.puts("hello ctypes")
hello ctypes
13
のように、いきなりlibcのputs関数を呼び出すことができます(インタラクティブモードなので、戻り値の「13」が表示されています)。
もちろん引数も渡せます。
>>> libc.printf("My name is %s and I'm %d years old\n", "rgoura", 18)
My name is rgoura and I'm 18 years old
39
戻り値も手軽に扱えます。
整数と文字列は気にせず利用することができるよう工夫されており、その他のデータ型や、C特有の概念である構造体やポインタについても、工夫すれば大抵のことができるようになっています。
Hyper EstraierのAPIをctypes経由で呼び出す
さて、私は過去の自分宛のメールをHyper Estraierでインデックス化して検索できるようにしているのですが、先日、日常で使うちょっとしたプログラムをPythonで書いてみよう、と思い立ちました。
Hyper EstraierにはJava, Ruby, PerlのAPIバインディングは標準で提供されているのですが、Python用のバインディングはhyper_estraier_wrappersというサブプロジェクトとして扱われています。じゃあそれを使えばいいじゃん、という話なんですが、かねてからctypesをいじってみたかったので、あえて ctypesをいじってみることにしました。
手始めに、Hyper Estraierの「プログラミングガイド」にある、「サーチャのサンプル」(http://hyperestraier.sourceforge.net/pguide-ja.html#searcher)をpython+ctypesに移植してみたところ、結果は良好で、サンプルの意図通りに動作しました。
そこで、以下に移植した結果を示し、ちょっと迷ったところやおもしろかったところ、わからなかったところにコメントをしていきたいと思います。基本的に上記の「サーチャのサンプル」を一行一行置き換える形で移植しましたので、見比べていただければわかりやすいと思います。
実際のコードとコメント
1 #!/usr/bin/python2.5
2 # vim:fileencoding=utf_8
3 import sys
4 from ctypes import *
5
6 # enums were in 'estraier.h'
7 ESTDBREADER = 1
8
9 est = CDLL("libestraier.so")
10
11 ecode = c_int()
12
13 # データベースを開く
14 db = est.est_db_open("casket", ESTDBREADER, byref(ecode))
15 if not db:
16 func = est.est_err_msg
17 func.restype = c_char_p
18 print >>sys.stderr, "error: %s" % func(ecode)
19 sys.exit(1)
20
21 # 検索条件オブジェクトを生成する
22 cond = est.est_cond_new()
23
24 # 検索条件オブジェクトに検索式を設定する
25 est.est_cond_set_phrase(cond, "rainbow AND lullaby")
26
27 # データベースから検索結果を得る
28 func = est.est_db_search
29 func.restype = POINTER(c_int) # int *は基本型としては定義されてない
30 resnum = c_int()
31 result = func(db, cond, byref(resnum), None)
32
33 # 各該当文書を取得して表示する
34 for i in xrange(0, resnum.value):
35 # 文書オブジェクトを取得する
36 doc = est.est_db_get_doc(db, result[i], 0)
37 if not doc:
38 continue
39
40 # 属性を表示する
41 func = est.est_doc_attr
42 func.restype = c_char_p
43 value = func(doc, "@uri")
44 if value:
45 print "URI: %s" % value
46
47 value = func(doc, "@title")
48 if value:
49 print "Title: %s" % value
50
51 # 本文を表示する
52 texts = est.est_doc_texts(doc)
53 for j in xrange(0, est.cblistnum(texts)):
54 func = est.cblistval
55 func.restype = c_char_p
56 value = func(texts, j, None)
57 print "%s\n" % value
58
59 # 文書オブジェクトを破壊する
60 est.est_doc_delete(doc)
61
62 # 検索結果を破壊する
63 libc = CDLL("libc.so.6")
64 libc.free(result)
65
66 # 検索条件オブジェクトを破棄する
67 est.est_cond_delete(cond)
68
69 # データベースを閉じる
70 res = est.est_db_close(db, byref(ecode))
71 if not res:
72 func = est.est_err_msg
73 func.restype = c_char_p
74 print >>sys.stderr, "error: %s" % func(ecode)
75 sys.exit(1)
76
77 sys.exit(0)
7行目:
enum定数は、必要なものをヘッダファイルから書き写したりしてくるしかないと思います。
14行目:byref(ecode)のところ
(C言語の&ecodeのような)参照渡しをするためには、byref(ecode)を使います。
16行目:
ctypesの若干面倒なところで、何も指定しない場合、関数の戻り値はc_int型(整数型)です。c_int型以外の戻り値を設定するためには、関数オブジェクトのrestypeプロパティに戻り値の型を設定する必要があります。そのため関数オブジェクトをいったんfuncに代入しています。この例では、エラー文字列を返してもらうため、17行目にcharポインタを戻り値に設定しています。
22行目:
est_cond_newの戻り値は検索条件オブジェクト(ESTCOND型)であるにもかかわらず、この場合condはpython側からはc_int 型になります。が、今回のプログラムではpython側でcondの値を表示したりする計算したりする機能は存在せず、アドレスさえ保持されていればさしつかえないため、特に16行目のようなことは行っていません。14行目、36行目、52行目なども同様です。
29行目:POINTER(c_int)のところ
char *を表すc_char_pなどはctypesの基本データ型として定義されていますが、int *を表す基本データ型はありません。一瞬迷いましたが、POINTER(c_int)で大丈夫でした。
34行目:resnum.valueのところ
c_int型の値をPython側から使うためには、valueメンバを参照します。
36行目:result[i]のところ
POINTER(c_int)型のデータは、添え字インデックスで各要素にアクセスできます。
62行目:
元のプログラムでは、
free(result);
となっている箇所です。pythonにはfreeは無いので(あったとしても違う場所を解放すると思われます)、libcからfreeを呼び出しました。
感想
途中でCのプログラム内容を再現できない内容が出現して挫折するのではないかと思いましたが、無事再現できました。この手軽さはすごい。
ただし、私の未熟さも祟って、この規模のコードでさえ上記の姿に完成するまでにSegmentation faultに何度か見舞われ、普段からいかにぬるい風呂に浸かっているかを思い知らされました(^^;
もはやバインディングはいらない?というのはあきらかに言い過ぎですが、ちょっとしたプログラムに活用できるかもしれませんね。
(参考)
Hyper Estraier 「プログラミングガイド」(サーチャのサンプル)
http://hyperestraier.sourceforge.net/pguide-ja.html#searcher
ctypesのチュートリアル
http://www.python.jp/doc/release/lib/ctypes-ctypes-tutorial.html
>>> libc.printf("My name is %s and I'm %d years old\n", "rgoura", 18)
My name is rgoura and I'm 18 years old
39
戻り値も手軽に扱えます。
>>> print "%d letters" % libc.strlen("HDE")
3 letters
整数と文字列は気にせず利用することができるよう工夫されており、その他のデータ型や、C特有の概念である構造体やポインタについても、工夫すれば大抵のことができるようになっています。
Hyper EstraierのAPIをctypes経由で呼び出す
さて、私は過去の自分宛のメールをHyper Estraierでインデックス化して検索できるようにしているのですが、先日、日常で使うちょっとしたプログラムをPythonで書いてみよう、と思い立ちました。
Hyper EstraierにはJava, Ruby, PerlのAPIバインディングは標準で提供されているのですが、Python用のバインディングはhyper_estraier_wrappersというサブプロジェクトとして扱われています。じゃあそれを使えばいいじゃん、という話なんですが、かねてからctypesをいじってみたかったので、あえて ctypesをいじってみることにしました。
手始めに、Hyper Estraierの「プログラミングガイド」にある、「サーチャのサンプル」(http://hyperestraier.sourceforge.net/pguide-ja.html#searcher)をpython+ctypesに移植してみたところ、結果は良好で、サンプルの意図通りに動作しました。
そこで、以下に移植した結果を示し、ちょっと迷ったところやおもしろかったところ、わからなかったところにコメントをしていきたいと思います。基本的に上記の「サーチャのサンプル」を一行一行置き換える形で移植しましたので、見比べていただければわかりやすいと思います。
実際のコードとコメント
1 #!/usr/bin/python2.5
2 # vim:fileencoding=utf_8
3 import sys
4 from ctypes import *
5
6 # enums were in 'estraier.h'
7 ESTDBREADER = 1
8
9 est = CDLL("libestraier.so")
10
11 ecode = c_int()
12
13 # データベースを開く
14 db = est.est_db_open("casket", ESTDBREADER, byref(ecode))
15 if not db:
16 func = est.est_err_msg
17 func.restype = c_char_p
18 print >>sys.stderr, "error: %s" % func(ecode)
19 sys.exit(1)
20
21 # 検索条件オブジェクトを生成する
22 cond = est.est_cond_new()
23
24 # 検索条件オブジェクトに検索式を設定する
25 est.est_cond_set_phrase(cond, "rainbow AND lullaby")
26
27 # データベースから検索結果を得る
28 func = est.est_db_search
29 func.restype = POINTER(c_int) # int *は基本型としては定義されてない
30 resnum = c_int()
31 result = func(db, cond, byref(resnum), None)
32
33 # 各該当文書を取得して表示する
34 for i in xrange(0, resnum.value):
35 # 文書オブジェクトを取得する
36 doc = est.est_db_get_doc(db, result[i], 0)
37 if not doc:
38 continue
39
40 # 属性を表示する
41 func = est.est_doc_attr
42 func.restype = c_char_p
43 value = func(doc, "@uri")
44 if value:
45 print "URI: %s" % value
46
47 value = func(doc, "@title")
48 if value:
49 print "Title: %s" % value
50
51 # 本文を表示する
52 texts = est.est_doc_texts(doc)
53 for j in xrange(0, est.cblistnum(texts)):
54 func = est.cblistval
55 func.restype = c_char_p
56 value = func(texts, j, None)
57 print "%s\n" % value
58
59 # 文書オブジェクトを破壊する
60 est.est_doc_delete(doc)
61
62 # 検索結果を破壊する
63 libc = CDLL("libc.so.6")
64 libc.free(result)
65
66 # 検索条件オブジェクトを破棄する
67 est.est_cond_delete(cond)
68
69 # データベースを閉じる
70 res = est.est_db_close(db, byref(ecode))
71 if not res:
72 func = est.est_err_msg
73 func.restype = c_char_p
74 print >>sys.stderr, "error: %s" % func(ecode)
75 sys.exit(1)
76
77 sys.exit(0)
7行目:
enum定数は、必要なものをヘッダファイルから書き写したりしてくるしかないと思います。
14行目:byref(ecode)のところ
(C言語の&ecodeのような)参照渡しをするためには、byref(ecode)を使います。
16行目:
ctypesの若干面倒なところで、何も指定しない場合、関数の戻り値はc_int型(整数型)です。c_int型以外の戻り値を設定するためには、関数オブジェクトのrestypeプロパティに戻り値の型を設定する必要があります。そのため関数オブジェクトをいったんfuncに代入しています。この例では、エラー文字列を返してもらうため、17行目にcharポインタを戻り値に設定しています。
22行目:
est_cond_newの戻り値は検索条件オブジェクト(ESTCOND型)であるにもかかわらず、この場合condはpython側からはc_int 型になります。が、今回のプログラムではpython側でcondの値を表示したりする計算したりする機能は存在せず、アドレスさえ保持されていればさしつかえないため、特に16行目のようなことは行っていません。14行目、36行目、52行目なども同様です。
29行目:POINTER(c_int)のところ
char *を表すc_char_pなどはctypesの基本データ型として定義されていますが、int *を表す基本データ型はありません。一瞬迷いましたが、POINTER(c_int)で大丈夫でした。
34行目:resnum.valueのところ
c_int型の値をPython側から使うためには、valueメンバを参照します。
36行目:result[i]のところ
POINTER(c_int)型のデータは、添え字インデックスで各要素にアクセスできます。
62行目:
元のプログラムでは、
free(result);
となっている箇所です。pythonにはfreeは無いので(あったとしても違う場所を解放すると思われます)、libcからfreeを呼び出しました。
感想
途中でCのプログラム内容を再現できない内容が出現して挫折するのではないかと思いましたが、無事再現できました。この手軽さはすごい。
ただし、私の未熟さも祟って、この規模のコードでさえ上記の姿に完成するまでにSegmentation faultに何度か見舞われ、普段からいかにぬるい風呂に浸かっているかを思い知らされました(^^;
もはやバインディングはいらない?というのはあきらかに言い過ぎですが、ちょっとしたプログラムに活用できるかもしれませんね。
(参考)
Hyper Estraier 「プログラミングガイド」(サーチャのサンプル)
http://hyperestraier.sourceforge.net/pguide-ja.html#searcher
ctypesのチュートリアル
http://www.python.jp/doc/release/lib/ctypes-ctypes-tutorial.html
Leave a comment