窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

Pythonのctypesを使ってネイティブライブラリを使う

また備忘録として.

前回は NuGet でプラットフォームごとにネイティブライブラリを同梱してそれを使うラッパーのパッケージを作る方法 - 窓を作っては壊していた人のブログ という感じでC# でネイティブライブラリをラップして使うようなことを紹介しました. 私はC# が好きだし,Xamarin Workbooks などがあるため REPL があるため C# だけでもいいかなと思っていたのですが,Python もあるとやっぱり楽だなぁということを思うようになりました.

ということで Python でラップするとどうなるの?ということをメモしていきたいと思います.

おことわり

今回は 16.16. ctypes — Pythonのための外部関数ライブラリ — Python 3.6.1 ドキュメント を使いましたが,今ネイティブライブラリをラップしたり速度を求めるなら一般的には Cython: C-Extensions for Python を使います. 今回 ctypes を使った理由としては,Cython を入れることが困難な環境(そんな環境あるのかよという感じですが… Windows ユーザーでも今は簡単に入りますよね?たぶん)とかでも簡単に使えるからと言う理由です. 自分の研究室のメンバーがあまり Python に詳しくなさそう()というのもあるので,とりあえずどんな制限下でも使えないといけないという…

ネイティブライブラリの準備

もはやここは gcc の使い方とか,Windows で言ったら cl.exelink.exe の使い方の話になるので省略.

github.com

このリポジトリmakefilebuild_windows.bat を見れば大体わかります. Windows の nmake が使いたかったのですが,あまり理解できていないので cl.exe と link.exe を愚直に叩くやり方で頑張っています.

今回は以下のようなライブラリを想定してやってみます.

void mod_1d_array(double *arr, int size)
{
    for (int i = 0; i < size; i++) arr[i] *= 2;
}

int sum_1_to_value(int value)
{
    return value + 1;
}

void value2times(int *value)
{
    *value *= 2;
}

ネイティブライブラリを使う

とりあえず流れを追ってみましょう.

ライブラリをロードする

import ctypes

my_clibrary = ctypes.cdll.LoadLibrary(YOUR_C_DYNAMIC_LIBRARY_PATH)

my_clibrary に先程(?)用意したライブラリをロードしたものを格納します(語弊があるけど気にしないでください).

この my_clibrary の attribute にロードしたライブラリのシンボルが含まれているので,以下のようにして対象の関数を呼び出すことが出来ます.

関数を呼び出してみる

result = my_clibrary.sum_1_to_value(1)

この result には 2 が格納されていると思います.

非常に簡単,Python から C のライブラリが呼び出せるとか最高!という感じですが,まだまだ序の口,簡単な型だから出来たわけです.

それでは参照渡しとかそういうのをやってみましょう…と言いたいところですが,ちょっと待ってください. 引数の数だったり,引数の型,戻り値の型とかをちゃんとアノテーションしておかないと,後から見た時にわかりづらくないですか?

実際 ctypes を使う時は引数を誤ってしまった場合など,セグメンテーションフォルトで落ちたりします,つらいですよね. しかし予め型の定義をしておくと渡したパラメータがその型と互換性があるか,キャスト可能かなどの判定を行った後に呼び出されるので非常に安心です.

ということで,型の定義をしてみましょう.

型定義で安心して使えるようにする

今回用意した3つの関数について定義してみようと思います.

# void mod_1d_array(double *arr, int size) の場合
my_clibrary.mod_1d_array.restype = None
my_clibrary.mod_1d_array.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_int]

# int sum_1_to_value(int value)
my_clibrary.sum_1_to_value.restype = ctypes.c_int
my_clibrary.sum_1_to_value.argtypes = [ctypes.c_int]

# void value2times(int *value)
my_clibrary.value2times.restype = None
my_clibrary.value2times.argtypes = [ctypes.POINTER(ctypes.c_int)]

こんな感じになります. ポイントとしては,配列やポインタなどの参照渡しの場合は,対象の型を POINTER で囲ってあげればポインタであるということが明示することが出来ます.

これだけでぐっと ctypes を使った時のネイティブライブラリへのアクセスが安全なものになります. Jupyter Notebook とかで開発している時はものすごい助けられました.

また副産物としては,このように attribute にアクセスするので,もしもそんな関数がシンボルとして定義されていない場合は例外を吐きます. ネイティブライブラリを実装している時にシンボルの公開忘れなどがあった場合そこで気づくことも出来ます(今回自分で実装していたライブラリで Windows でのシンボル公開を忘れているものがあったのにこれで気づくことが出来た).

それでは今回定義したもので安全に関数を呼び出してみましょう.

参照を伴う関数の呼び出し

まずはシンプルな変数の参照渡しをやってみましょう.

value = ctypes.c_int(10)
my_clibrary.value2times(ctypes.byref(value))
print(value.value)

20が出力されたでしょうか. このように値の参照渡しは byref によって行います.

さて,次は配列です. これはすこし厄介です. 上記の value2times は配列とかでもなんでもないものを渡すだけだったので byref だけで良かったのですが,今回は配列の確保なども行わなければなりません.

今回は 1 ~ 10 の値が入った配列を mod_1d_array で操作してみます.

my_array = list(range(1, 10 + 1)) # Python2.x ならこれでいいけど,3.x だと list(range(1, 10 + 1)) とかにしないとだったりする?
my_c_array = (ctypes.c_double * len(my_array))(*my_array)
my_clibrary.mod_1d_array(my_c_array, len(my_array))
print(list(my_c_array))

2.0, 4.0, …. みたいになったと思います.

Cライブラリに配列を渡す時は 目的の型 * 配列の長さ という を作って,そのインスタンスを作ります. ここでは ctypes.c_double * len(my_array) としていて,この場合 c_double_Array_10 という型になります.

そのためここでは my_c_array という c_double_Array_10 という型のインスタンス*my_array のようにリストを展開して初期化しています. ポインタ型と 1d-Array は型の互換があるので,そのまま渡しても自動的にキャストされてそのまま使うことが出来ています.

こんな感じでやり方さえ覚えてしまえば配列だろうが簡単に渡すことが出来ます.

早いけどおわりに

今回は ctypes を使ってネイティブライブラリを使う初歩をお伝えしました.

実際の環境ではこんな簡単なライブラリじゃなくてもっと型定義がややこしいものなどたくさんあると思います. そんな時は公式ドキュメントをちゃんと読んでみるとヒントがあったりします.

また構造体や2次元配列みたいのは少しテクニックが必要だったりします.

それについては実際に私がラップした

github.com

とかを見ていただければと思います.

毎回途中まで記事書いて力尽きるのやめたい

没ネタ

頑張って書こうとしたけど,まとめるのが面倒でソース見てって思ったもの

  • 多次元配列に関してはサポートモジュールを作っておくと楽
inner_type = (c_double * inner_length)()
outer_type = (inner_type * outer_length)()
buffer = outer_type()

for i in range(outer_length):
    buffer[i] = inner_type()

とか二次元配列用意する時に書くのアホらしすぎる.

またこれは c_double_outer_length_array_inner_length_array みたいな型定義になって, POINTER(POINTER(c_double)) の型とマッチしなくて,型チェックで弾かれてしまう.

だからこれを cast という形で整えてあげる必要がある. これも一気にやるサポートモジュールを作ると楽.

  • ライブラリ名とかを列挙して LoadLibrary し易い形に

__init__.py でやったこと. ctypes ではないけれども,pkg_resources を使うことによって,そのライブラリがあるパスを特定出来るので,安全にファイルを探すことが出来る.

  • 配布のための setup.py で pre-build を含ませる

Python の C拡張 を今回は使ったわけではないので,同梱したりするのが難しかった. これも ctypes とは関係ないけれども,setup.py で引っ張ってくるようにしたらある程度楽になると思う.