はじめに
標準モジュールのctypesを使うとpythonのコードからC言語の関数を呼び出せます。
しかし、C言語側で確保したリソースをctypesやpythonインタプリタが世話してくれる訳ではありません。ということは、mallocを使いっぱなしにするとメモリリークします。
その対策について書きます。
目次
メモリリークすることの簡単な確認
次のようなコードを書きます。
test1.c
#include <string.h> #include <stdlib.h> void *f(void) { void *ptr; char a[1024*1024] = {}; ptr = malloc(sizeof(char)*1024*1024); memcpy(ptr, a, 1024*1024); return ptr; }
見ての通り、スタック領域に確保したchar配列をmallocで確保した同サイズの領域にコピーしてreturnするというようなCのコードです*1。
以下のようにコンパイルします。linux系でgccでコンパイルすることを前提としています。
$ gcc -shared -fPIC test1.c -o test1.so
共有ライブラリ化したので、これを呼び出すpythonコードも作成します。test1.soと同一ディレクトリに置いてください。
call_test1.py
import time import ctypes def f(): lib = ctypes.cdll.LoadLibrary('./test1.so') lib.f() i = 0 while True: print("\r", i, end="") f() time.sleep(0.0001) i += 1
あとはターミナルから実行して、ターミナルに表示される数字を眺めながら、別ターミナルで立ち上げたtopコマンドかタスクマネージャなどでプロセスのメモリ消費量を監視します。もりもりとメモリ消費量が増えていくことがわかるはずです。納得したらctrl-cで止めてください。
対策
free()すりゃええ。
基本的にはC言語側でメモリ解放のための関数を用意してあげます。そして、Cの関数を呼んでpython側でほしい情報をpythonオブジェクトとして取得してから、C側で確保したメモリ領域を解放してあげるという流れです。mallocしたメモリ領域はどうせそのままpythonのオブジェクトとしては使えないと思うので、容赦なくfreeしてしまう訳です。
ptrをグローバル変数としてあげれば簡単です(というか他の方法は実質的にないはず)。
test2.c
#include <string.h> #include <stdlib.h> void *ptr; void *f(void) { char a[1024*1024] = {}; ptr = malloc(sizeof(char)*1024*1024); memcpy(ptr, a, 1024*1024); return ptr; } void free_ptr(void) { free(ptr); }
コンパイル手順
$ gcc -shared -fPIC test2.c -o test2.so
call_test2.py
import time import ctypes def f(): lib = ctypes.cdll.LoadLibrary('./test2.so') lib.f() lib.free_ptr() i = 0 while True: print("\r", i, end="") f() time.sleep(0.0001) i += 1
先ほどと同様に実行すると、今度はメモリリークしないことがわかるはずです。
もちろん、関数を呼ぶたびに毎回こんなことをやるのは手間です。また、そもそも、ctypesで共有ライブラリの関数をちゃんと使おうと思うと、引数や返り値の型を明示的に指定したりといった面倒な作業がついて回ります。なので、実際にはこれらの処理をまとめてwrapしたpythonの関数を作り、それを呼び出して処理することになると思います。
まとめ
これで安心してctypesでmallocできます。
参考リンク
ここを参考にしました。
c - Python: ctypes and garbage collection - Stack Overflow
*1:実は、mallocだけだと謎の最適化が働くのか、思うようにメモリリークしなかったので、わざと無駄なデータを書き込んでいます