ctypes
PythonからC言語の動的ライブラリを使用する。
PythonとCの連携方法
PythonからCのコードを使うには、複数の方法がある。
-
- やや昔の記事なので、今ではBoost.PythonでもNumPyが使えたりと若干の違いはあるが、情報としてまとまっている
いろいろあるが、ctypesとBoost.Pythonを取り上げると、
Boost.Python | ctypes | |
---|---|---|
形態 | C++ライブラリ | Python標準ライブラリ |
PythonからCを利用 | ○ | ○ |
CからPythonを利用 | ○ | × |
Cのメモリ管理(GC) | 面倒見てくれる | 見てくれない? |
Cの記述・コンパイル | 必要 | 自作するなら必要 |
Cコードを触る・コンパイルすることを厭わなければ、Boost.Pythonの方が汎用性が高い。
既存のライブラリを変更せず利用するだけならPythonだけで完結するためctypesの方が手軽……
とはいえC側での型定義などを調べる必要はあるし、複雑なクラスはほぼそのまま構造をPythonで再定義してやらなければならないので、あくまで簡単な処理を呼び出すのが現実的。
ctypes利用方法
あくまで動的ライブラリ(Windowsでは.dll, Linuxでは.so)を読み込むもので、静的ライブラリ(.lib)は読み込めない。
# 唐突なNim proc add(x, y: cdouble): cdouble {.exportc, dynlib.} = return x + y
> nim c -d:release --app:lib sample.nim
これで、add()
という関数を持ったsample.dllができる。exportc, dynlib
あたりがdll化に必要らしい。
で、これをPythonから利用する。
単純には使用できず、型を教えてやる必要がある。
Pythonでの型のメモリ構造を、Cでの型のメモリ構造に互換性のある形で渡さなければいけないが、.dll自体は関数の引数が何の型かなんて情報は持っていない。それをctypesを通して教えることで、Pythonの方で正しく変換してから渡すようになる。これを指定しないと、整数を渡したら浮動小数で解釈され、てんで違った結果が帰ってきたりする。
import ctypes as C lib = C.cdll.LoadLibrary('sample.dll') lib.add.argtypes = [C.c_double, C.c_double] # 引数は2つ、double型ですよー lib.add.restype = C.c_double # 返値はdouble型ですよー res = lib.add(3, 8) # int型整数を渡しても print(res) # => 11.0 doubleでちゃんと解釈されて返ってくる
NumPy
numpy.ndarrayも渡せると便利。NumPyモジュールに、NumPyの型をCと連携させるための指定方法が準備されている。
ただ、1次元配列をCの関数に渡すのはできたが、2次元配列になったり、返値として受け取る方法がどうにもよくわからず。ポインタムヅカシイ。
ひとまず渡す方をメモ。
# 1次元配列を合計して返すだけ # NumPy配列はopenArray[]で受け取る proc arraySum(xxx: openArray[float]): float {.exportc, dynlib.} = return xxx.sum
openArrayは、Cにコンパイルされるとき、実は引数が追加されている。
nimcache
フォルダにできるsample2.c
を見ると、
// ... N_LIB_EXPORT N_CDECL(NF, arraySum)(NF* xxx, NI xxxLen_0) { // ←ここ NF result; { result = (NF)0; result = sum_o0A2tEZqgAaJbmC0q9asV1g(xxx, xxxLen_0); goto BeforeRet_; }BeforeRet_: ; return result; } // ...
NF* xxx
はNimで定義した変数だが、その次にNI xxxLen_0
が追加されている。これは、配列の長さを示すもの。
で、ctypesで関数を呼ぶときには、NimではなくこのCに添った引数を与えることになる。
import ctypes as C import numpy as np lib = C.cdll.LoadLibrary('sample2.dll') lib.arraySum.argtypes = [np.ctypeslib.ndpointer(flags='C'), C.c_int] lib.arraySum.restype = C.c_double a = np.array([1, 2, 3, 4], dtype=np.float) res = lib.arraySum(a, len(a)) print(res) # => 10.0