ctypes ~pythonでCをイジる(Macで)~

ctypesって??

ctypesとは、pythonc言語のライブラリを操作するためのライブラリです.
FFI(Foreign Function Interface)というんですね, こういうもののこと.
ドキュメントはココにあります.
このスライドも参考になりました.

似たようなものでCythonというのもありますが,
純粋なpythonスクリプトでCの関数を実行できることがctypesのメリットです.
pythonのプログラムを高速化するのだ!!ということであればCythonかもしれませんが,
ちょっとしたデバッグ用に使うようなときはctypesでサクっと書いた方が良さそうです.

なんでctypes??

もともとAndroidのJNIで使用するためのCのプログラムを書いていました.
で, デバッグの過程でそれを可視化したいなと思っていたのですが,
描画なんてpythonでサクっとやってしまいたいと思うのが男心ですよね.
そんな経緯で調べてみたらctypesを知りまして,
使ってみたら思った通りサクっとできて幸せになりました.

簡単にではありますが, ctypesについて紹介させていただきます.

使い方

MacとWindowsでライブラリのロードの方法がかわってきます.
今回は手元のMacbook Pro(Mavericks)で実行してるので, その前提ですすめていきます.
ソースコードGitHubにおいてあります.

動的ライブラリのコンパイル

まずはCのコードをコンパイルします.
Macの場合はso形式が必要となりますので

gcc ctypes_sample.c -shared -o libctypessample.so  

という感じでコンパイルします.

ライブラリのロード

まずは, 先ほどコンパイルしたライブラリを読み込みます.
Macでやっているので,
ctypes.cdll.LoadLibrary(libname)
を使用します.

from ctypes import *

libc = LoadLibrary("libctypessample.so")

という感じになりますね.

引数も戻り値もない関数

ここからはpythonのコードを見ていきます.
ひたすらいろいろな関数を実行していきますので, ざっと見てみてください.

では試しに, コンソール出力するだけの関数を呼び出してみましょう.

libc.print_with_no_arg()  

コンソール出力はこのように.

[native] print_with_no_arg called

たったこれだけですが, 確かにCの関数を呼び出すことができました!!

引数を持つ関数

先ほどの関数より少しだけ進化した, 引数を1つとってコンソール出力する関数です.

libc.print_with_int_arg(c_int(100))  
libc.print_with_float_arg(c_float(12.34))  

[native] print_with_int_arg called, arg:100
[native] print_with_float_arg called, arg:12.340000

はい, 正しく出力できています.
ここでc_int, c_floatが出てきましたが, これらはctypesの基本データ型です.
データ型については公式ドキュメントに任せることにします.

また, 実は, c_int(100)の部分を100としても実行できます.
ただし, 後述するbyref(className)を使用した値渡しを行いたいときなどは,
上記の基本データ型でなければ正しく実行できません.

戻り値もある関数

今度は, 引数でとったint型の値に1を足す関数です.

added = libc.add_one(c_int(100))  
print "orig:%d, added:%d" % (100, added)  

[native] add_one called
orig:100, added:101

関数の戻り値がc_int型ではなくpythonの整数型になっていることがわかりますね.

参照渡し

上の関数と似ているのですが, 引数をポインタとして渡し, そのポインタに1を足します.

num_c = c_int(100)  
libc.add_one_to_ptr(byref(num_c))  
print "orig:%d, added:%d" % (100, num_c.value)  

[native] add_one_to_ptr called
orig:100, added:101

でてきました, byref(className)です.
これを使うことで, 今回であればc_int型のポインタを作ることが出来ます.
もうひとつ, ctypesの基本データ型から値を取り出す場合, valueメンバにアクセスします.
上記のnum_c.valuenum_cとした場合, 当然TypeErrorで怒られます.

配列を扱う

少しずつ楽しくなってきましたね. 今度は配列を扱ってみます.
int型配列の各要素に1を足すというCの関数を実行しています.

IntArray10 = c_int * 10  
i_arr = [i for i in xrange(10)]  
i_arr_c = IntArray10(*i_arr)  
print "before:", [i_arr_c[i] for i in xrange(10)]  
libc.add_one_in_array(i_arr_c, 10)  
print "after:", [i_arr_c[i] for i in xrange(10)]  

before: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[native] add_one_to_array called
after: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

ctypesで配列を扱うときは, 基本データ型の配列の型をつくることからはじまります.
例では, 10個のc_int型からなるIntArray10を定義し, インスタンス化しています.

構造体を扱う

さて, Cでプログラムを書いていたら, 当然構造体をつかいますよね.
ctypesは構造体も簡単に扱うことができます.

Cのプログラムでは, このような構造体を定義しています.

typedef struct {  
    float x;  
    float y;  
    float z;  
} FPOINT_3D;

まずは, 上記の構造体を受け取り, その内容をコンソール出力するプログラムを考えます.
Cの構造体に対応するクラスをpython側で定義します.

class FPoint_3D(Structure):  
    _fields_ = [  
            ("x", c_float),  
            ("y", c_float),  
            ("z", c_float)]  
  
    def __init__(self, x, y, z):  
        self.x = x  
        self.y = y  
        self.z = z  

このように, ctypes.Structureを親に持つクラスを定義します.
そして, _fields_を[("メンバ名", 基本データ型), ...]の形式で記述します.
初期化しやすいように, def __init__も定義しました.
呼び出すプログラムは非常にシンプルになります.

point_3d = FPoint_3D(1.1, 2.2, 3.3)  
libc.print_fpoint3d(point_3d)  

[native] print_fpoint3d x:1.100, y:2.200, z:3.300

確かに自作の構造体を渡すことができました.

構造体のポインタを扱う

今度は同様の構造体のポインタを渡し, 各メンバに1を足す関数を呼び出します.

point_3d = FPoint_3D(1.1, 2.2, 3.3) 
print "before: x:%.3f, y:%.3f, z:%.3f" % (point_3d.x, point_3d.y, point_3d.z) 
libc.add_one_in_fpoint3d(byref(point_3d))  
print "after: x:%.3f, y:%.3f, z:%.3f" % (point_3d.x, point_3d.y, point_3d.z)  

before: x:1.100, y:2.200, z:3.300
[native] add_one_in_fpoint3d
after: x:2.100, y:3.200, z:4.300

おそらく予想通りの呼び出し方でしょう.
任意の構造体であってもbyref()でポインタとして扱うことが出来ます.

構造体の配列を扱う(ついでに描画もしちゃう)

一応, 用意した最後の関数です.
今回は少しかわっていて, まず, set_random_to_array()関数を呼び出し,
CのstaicなIPOINT_2D型配列の各要素の各メンバにランダムな0から9までの値をセットします.
そして, その配列をpython側で用意した配列にコピーするという処理になります. IPOINT_2Dはint型のx,yからなる構造体で, 2次元の方が描画が楽だという理由で用意しました.
python側のIPoint_2Dクラスの実装は想像通りです.

libc.set_random_to_array()  

IPoint_2DArray100 = IPoint_2D*100  
point_2d_arr = IPoint_2DArray100(*(IPoint_2D(0,0) for i in xrange(100)))  
libc.cpy_array(point_2d_arr)  

基本データ型の配列と同様に, 型名 * 長さで配列型を定義し, インスタンス化しています.
ついでに, matplotlibを使って結果をプロットしてみましょう.

f:id:curlnoodle:20131230221136p:plain

このデータ自体に意味がある訳ではないのですが,
Cでの処理結果を取得してビジュアルで確認したりできるんですね.
pythonだとmatplotlibやnumpyやscipyなどデータを処理するのに便利なモジュールが
たくさんそろっているのでサクっといじるのにもってこいですよね.

最後に

長くなりましたがctypesの使い方の参考になれば幸いです.
ブログもちょいちょい更新していける様がんばります.

ありがとうございました.

詳説 Cポインタ

詳説 Cポインタ