静かなる名辞

pythonとプログラミングのこと


【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終端の文字列等を渡すのであれば、これらを使えば良さそうです。ただし、本当に生のバイナリデータを渡すのであれば、前回の記事の方法でやった方が良いのかな? という気も少しするので、けっこう微妙なところだと思います。