純 Python で C のライブラリを呼び出す(Linux inotifyを例に)
Python は Batteries included 哲学のもと、豊富なモジュールが標準で組み込まれていますので、大抵のことは標準モジュールを使えばできますし、そうでなくても、どこかの誰かが Python binding を作ってくれているはずですので、Cのライブラリを直接呼び出すような必要はほとんどありません。
ただ、今回、Cのコンパイル無しに(≒モジュールの追加インストール無しに) inotify を使いたい、という事情があり、直接 libc を呼び出す binding を作りました。コード自体はすごく簡単なのですが、概念が若干わかりにくかったので、今日の記事はその備忘録です。
ctypes と struct を使う!
使うモジュールは主に ctypes です。加えて、inotify では、read() で構造体の読み込みを行うため、read で読んだバイナリ文字列をパースするために struct も必要になりました。
いずれも、公式の標準ライブラリドキュメントに詳細なチュートリアルがあるので、じっくり読み込めば理解はできます。(やることがマニアックなので、ちゃんと頭を回しながら読まないと辛いとおもいますが。)
ctypes でのライブラリ読み込み
まずは、目的とするライブラリを読み込みます。
ファイル名などが正確に分かっている場合はそれを指定して
libc = CDLL("libc.so.6")
などとします。
同名のライブラリでもバージョンが違うとAPIが変わる可能性があるため、バージョン番号まで含めた指名をする必要があります。
バージョンはどれでもよく、そのシステムにインストールされているものを使うのであれば、find_libray()を使って
libc = CDLL(find_library("c"))
といったやりかたができます。(が、もちろん、バージョンが違うと、元々欲しかったAPIが無かったり、変わっている可能性はありますので注意が必要です。)
関数の宣言
ライブラリが export している関数は、読み込んだ dll オブジェクトの属性としてアクセスできます。
print libc.time(None)
簡単な用途であれば、このまま使えばよいのですが、引数のチェックも働きませんし、戻り値も int とみなして扱われるため、このままでは不都合な場合もあると思います。
ですので、大抵の場合は、一旦関数プロトタイプをつくって、呼び出し用の外部関数オブジェクトをつくってやるのがよいでしょう。
inotify_add_watch = CFUNCTYPE(c_int, c_int, c_char_p, c_uint32)( ("inotify_add_watch", libc), ((1, "fd"), (1, "pathname"), (1, "mask")) )
これで、 inotify_add_watch() 関数が使えるようになります。
CFUNCTYPE に与えている引数は、先頭が戻り値の型で、残りは引数の型です。CFUNCTYPEが返す関数に対して与えている引数は、第一引数が (シンボル名, dllオブジェクト) のタプルで、第二引数は引数の情報を表す (フラグ, パラメータ名, デフォルト値) タプルのリストになります。(各引数で、パラメータ名とデフォルト値は省略可能)
フラグは、1: 入力パラメータ 2: 出力パラメータ 4: デフォルトが整数0 の組み合わせの整数です。(ポインタを渡してそこに書かせるような関数なら 2 を指定するわけですね。)
パラメータ名は、関数を呼ぶときの名前付き引数として使えますし、デフォルト値まで指定しておけば、引数を省略して呼べるようになります。
ここで、引数の型として c_char_p などを使っていますが、これらは ctypes モジュールで定義されています。構造体などを自分で定義することもできますので、その時は公式リファレンスを参照のこと。
また、「戻り値が負の値だったら exception を raise する」とか、「出力パラメータの引数の値を関数の戻り値として返す」といったちょっと高度な結果処理をしたい場合、errcheck 属性を使うことで実現できます。こちらも公式リファレンス参照。
pure-python の inotify binding
というわけで、上記のテクニックを使って inotify の binding をつくってみました。
#!/usr/bin/env python2 from ctypes import CDLL, CFUNCTYPE, c_char_p, c_int, c_uint32 from ctypes.util import find_library import argparse import os import struct import sys __libc = CDLL(find_library("c")) inotify_init = CFUNCTYPE(c_int)(("inotify_init", __libc), ()) inotify_init1 = CFUNCTYPE(c_int, c_int)(("inotify_init", __libc), ((1, "flags"),)) inotify_add_watch = CFUNCTYPE(c_int, c_int, c_char_p, c_uint32)( ("inotify_add_watch", __libc), ((1, "fd"), (1, "pathname"), (1, "mask")) ) inotify_rm_watch = CFUNCTYPE(c_int, c_int, c_int)( ("inotify_rm_watch", __libc), ((1, "fd"), (1, "wd")) ) IN_ACCESS = 0x00000001 IN_MODIFY = 0x00000002 IN_ATTRIB = 0x00000004 IN_CLOSE_WRITE = 0x00000008 IN_CLOSE_NOWRITE = 0x00000010 IN_OPEN = 0x00000020 IN_MOVED_FROM = 0x00000040 IN_MOVED_TO = 0x00000080 IN_CREATE = 0x00000100 IN_DELETE = 0x00000200 IN_DELETE_SELF = 0x00000400 IN_MOVE_SELF = 0x00000800 IN_UNMOUNT = 0x00002000 IN_Q_OVERFLOW = 0x00004000 IN_IGNORED = 0x00008000 IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO IN_ONLYDIR = 0x01000000 IN_DONT_FOLLOW = 0x02000000 IN_EXCL_UNLINK = 0x04000000 IN_MASK_ADD = 0x20000000 IN_ISDIR = 0x40000000 IN_ONESHOT = 0x80000000 def _decode_flag(flag): flag_names = [x for x in globals().keys() if x.startswith('IN_')] flag_names.remove('IN_CLOSE') flag_names.remove('IN_MOVE') flag_names.sort(key=lambda x: globals()[x]) r = [] for n in flag_names: if globals()[n] & flag != 0: r.append(n) return r def main(): def integer(x): return int(x, base=0) parser = argparse.ArgumentParser() parser.add_argument('path', nargs='?', default='.', help='directory or file to watch') parser.add_argument('-f', '--flag', action='append', default=[], choices=[x for x in globals().keys() if x.startswith('IN_')], metavar='FLAG', help='event name to watch') parser.add_argument('-m', '--mask', default=0, type=integer, help='numeric event mask to watch') parser.add_argument('-a', '--all', action='store_const', dest='mask', const=0xfff, help='watch all event(-m 0xfff)') args = parser.parse_args() mask = args.mask for x in args.flag: mask |= globals()[x] if mask == 0: mask = IN_ACCESS print 'watching {} for inotify events: {}'.format( args.path, ",".join(_decode_flag(mask))) fd = inotify_init() wd = inotify_add_watch(fd, args.path, mask) try: while True: buf = os.read(fd, 4096) i = 0 fmt = 'iIII' fmt_size = struct.calcsize(fmt) while i < len(buf): wd, mask, cookie, name_len = struct.unpack_from(fmt, buf, i) i += fmt_size name = buf[i:i+name_len] i += name_len print("{:08x}({}) {} {}".format( mask, ",".join(_decode_flag(mask)), cookie, name)) except KeyboardInterrupt: pass inotify_rm_watch(fd, wd) os.close(fd) if __name__ == '__main__': sys.exit(main())
binding 本体は先頭の 20行ほどで終わっていて、残りは定数定義と、単体実行時の main() 関数になります。
このスクリプトを -a オプション付きで実行すると、カレントディレクトリに対するすべてのファイルアクセスを監視しますので、試してみてください。(他のオプションは -h で)