【python】ctypesのcreate_string_buffer()を使ってみる
はじめに
以前の記事で、ctypesでバイト列や文字列を受け渡しする方法について述べました。
【python】ctypesでバイト列や文字列を受け渡しする - 静かなる名辞
しかし、ctypesに存在しているcreate_string_buffer()と
create_unicode_buffer()には触れませんでした。
使ってみたら便利だったので紹介します。
これらは何なのか
変更可能なバッファをpython側で作成します。
これの良さそうなところは、python側のインタプリタでリソースが管理されるであろうことです。そのため、処理が簡単になります。
先に断っておきますが、具体的な仕様はドキュメントを参照してください。この記事はあくまでも簡単に使ってみるだけです。
create_string_buffer
まずC言語で次のような関数を書きます。
lib1.c
void reverse_bytes(int n, char *s1, char *s2) { int i; for (i=0; i<n; i++) { s2[i] = s1[n-i-1]; } } // ファイル名がlib1.cなら「gcc -shared -fPIC lib1.c -o lib1.so」のようにコンパイルしてください
アホみたいに単純なコードですが、これですべてです。s1の内容を反転した内容をs2に書き込みます。
python側では、以下のような呼び出しを用意します。
import ctypes lib = ctypes.cdll.LoadLibrary('./lib1.so') lib.reverse_bytes.argtypes = (ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p) def reverse_bytes(buf): n = len(buf) str_buf = ctypes.create_string_buffer(buf) lib.reverse_bytes(n, buf, str_buf) return str_buf.value if __name__ == "__main__": print(reverse_bytes(b"hoge")) # => b'egoh'
これでちゃんと実行されます。
create_unicode_buffer
これも基本的に同じです。
lib2.c
#include <wchar.h> void reverse_str(int n, wchar_t *s1, wchar_t *s2) { int i; for (i=0; i<n; i++) { s2[i] = s1[n-i-1]; } } // コンパイル手順:$ gcc -shared -fPIC lib2.c -o lib2.so
python側のコード。
import ctypes lib = ctypes.cdll.LoadLibrary('./lib2.so') lib.reverse_str.argtypes = (ctypes.c_int, ctypes.c_wchar_p, ctypes.c_wchar_p) def reverse_str(buf): n = len(buf) str_buf = ctypes.create_unicode_buffer(buf) lib.reverse_str(n, buf, str_buf) return str_buf.value if __name__ == "__main__": print(reverse_str("hoge")) # => egoh
わーい直感的に使える!
速度比較
前回は組み込みに負けていましたが、今度はどうなるでしょうか。
import ctypes import timeit lib = ctypes.cdll.LoadLibrary('./lib2.so') lib.reverse_str.argtypes = (ctypes.c_int, ctypes.c_wchar_p, ctypes.c_wchar_p) def reverse_str(buf): n = len(buf) str_buf = ctypes.create_unicode_buffer(buf) lib.reverse_str(n, buf, str_buf) return str_buf.value if __name__ == "__main__": s = "hogefugaほげふが"*(10**3) print(s[::-1] == reverse_str(s)) print(timeit.timeit(lambda : reverse_str(s), number=10**3)) print(timeit.timeit(lambda : s[::-1], number=10**3)) """ => 結果 True 0.060752346995286644 0.00984983000671491 # 参考:mallocで実装した前回の結果 True 0.046704520999810484 0.009301142999902368 """
かえって遅くなる。悲しい。
まとめ
何はともあれ簡単にpython側のGCに管理を委ねることができるとわかりました。コードの見通しはよくなりますね。
NULL終端の文字列等を渡すのであれば、これらを使えば良さそうです。ただし、本当に生のバイナリデータを渡すのであれば、前回の記事の方法でやった方が良いのかな? という気も少しするので、けっこう微妙なところだと思います。