PythonからCの関数を呼び出す(Cの関数の共有ライブラリ化) Part1
どうも,筆者です.
今回は,Python.hを利用して,C言語で作成した関数をPythonから呼び出す方法について扱ってみる.
対象
ここでは,以下の二つの内容について取り扱う.
- 内部でstatic変数を用いる関数
- 引数として関数ポインタ(function pointer)を受け取る関数
参考サイト
参考にしたサイトを以下に示す.
Implementing C Function Callbacks to a Python Function - Python Cookbook [Book]
評価環境
今回は,Python環境とCの開発環境が含まれるDockerコンテナを用意し,その中で評価を行う. Dockerのバージョンは「19.03.8」,docker-composeのバージョンは「1.25.5」である.
また,Dockerを動かすマシンは,近くにあったRaspberry Pi 3 B+を用いる(ImageがARMベースになる).
事前準備
Dockerfileの用意
本題とは直接関係ないが,環境を構築する上で必要となる情報なので残しておく.
ここでは,作成したプログラムをホスト側と共有するため,コンテナ内で利用するユーザを作成する(基本的にrootで動作するため). また,ログイン時に作成したユーザで作業できるように工夫する.
FROM alpine:3.11 # set username ENV USERNAME guest_user # set home directory ENV HOMEDIR /home/${USERNAME} # set src directory (= work directory) ENV SRCDIR /src ENV NKF_VERSION 2.1.5 ENV NKF_URL http://jaist.dl.sourceforge.jp/nkf/70406/nkf-${NKF_VERSION}.tar.gz # create develop environment RUN apk update \ && apk add --no-cache bash tzdata vim sudo \ && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \ # install package && apk add --no-cache autoconf automake binutils build-base curl shadow python3 python3-dev \ # install nkf && cd /tmp \ && curl -sLO ${NKF_URL} \ && tar zxvf nkf-${NKF_VERSION}.tar.gz \ && cd nkf-${NKF_VERSION} \ && make \ && make install \ && cd / \ # add linux user && echo "Set disable_coredump false" >> /etc/sudo.conf \ && useradd -s /bin/bash -m ${USERNAME} \ && usermod -G wheel ${USERNAME} \ && sed -e 's;^# \(%wheel.*NOPASSWD.*\);\1;g' -i /etc/sudoers \ && sudo passwd -l root \ && sudo passwd -d root \ && mkdir -p ${SRCDIR} \ # create volume && ln -sf ${SRCDIR} ${HOMEDIR}/src \ && apk del shadow curl \ && rm -rf /root/.cache /var/cache/apk/* /tmp/* # copy startup script COPY ./start.sh /start.sh # set volume VOLUME ["${SRCDIR}"] # define user and work directory USER ${USERNAME} WORKDIR ${HOMEDIR} CMD ["/bin/bash", "/start.sh"]
起動時に利用するstart.shの内容を以下に示す.
#!/bin/bash { # change permission echo chmod 777 ${SRCDIR} # change owner echo chown ${USERNAME}:${USERNAME} ${SRCDIR} echo chown ${USERNAME}:${USERNAME} -R ${HOMEDIR} } | sudo su - trap_TERM() { echo "["$(date "+%Y/%m/%d-%H:%M:%S")"]" SIGTERM ACCEPTED exit 0 } trap 'trap_TERM' TERM while : do sleep 3 done
また,コンテナ起動に利用するdocker-compose.ymlファイルを以下に示す.
version: '3.4' services: develop_environment: build: context: ./develop_environment dockerfile: Dockerfile image: develop.env restart: always container_name: develop-environment volumes: - ./develop_environment/src:/src logging: driver: "json-file" options: max-size: "1m" max-file: "1"
ビルド,起動
一通り準備が完了した.以下のコマンドでimageをビルド,コンテナの作成を行う.
docker-compose build
docker-compose up -d
以下のコマンドでコンテナ内に入る.
# 必要に応じて,コンテナ名を確認しておく # docker ps -a docker exec -it develop-environment bash
ヘッダーとライブラリの確認
実装時とコンパイル時に用いるヘッダーとライブラリの場所を確認しておく.
# ======================================== # python3がインストールされている場所を確認 # ======================================== python3-config --prefix # ここでは,「/usr」と出力された # ======================================== # Python.hを探す # ======================================== # 以下のディレクトリに含まれることを確認 # /usr/include/python3.8 # ======================================== # Pythonのライブラリの確認 # ======================================== # 以下のディレクトリに含まれることを確認 # /usr/lib # libpython3.8.so, libpython3.8.so.1.0, libpython3.so
共有ライブラリの作成
Cの関数定義
ここでは,以下の三つの関数を用意した.
- static変数を参照しつつ,和を返却するadd関数
- static変数に値を設定するset_value関数
- 関数ポインタを引数にもつnewton_method関数
ディレクトリ構成
ディレクトリ構成を以下に示す.
. ---+--- include/custom.h | +--- src/ | | | +--- custom.c | | | +--- wrapper.c | +--- create_library.sh
ヘッダー
// custom.h #ifndef CUSTOM_H__ #define CUSTOM_H__ #include <stdint.h> extern int32_t add(int32_t x, int32_t y); extern void set_value(int32_t val); extern double newton_method(double x0, int32_t max_iter, double (*target_func)(double x)); #endif
関数
// custom.c #include <stdint.h> #include "custom.h" #define DELTA (1e-3) static int32_t g_val = 0; int32_t add(int32_t x, int32_t y) { int32_t z; z = x + y + g_val; return z; } void set_value(int32_t val) { g_val = val; } double newton_method(double x0, int32_t max_iter, double (*target_func)(double x)) { int32_t iter; double old_x, new_x, df; new_x = x0; for (iter = 0; iter < max_iter; iter++) { old_x = new_x; df = ((*target_func)(old_x + (double)DELTA) - (*target_func)(old_x - (double)DELTA)) / (double)DELTA * 0.5; new_x = old_x - (*target_func)(old_x) / df; } return new_x; }
Wrapper関数定義
まず,全体像を以下に示す.
// wrapper.c #include <stdint.h> #include "Python.h" #include "custom.h" PyObject *wrapper_add(PyObject *self, PyObject *args) { int32_t x, y, z; PyObject *ret = NULL; if (PyArg_ParseTuple(args, "ii", &x, &y)) { z = add(x, y); ret = Py_BuildValue("i", z); } return ret; } PyObject *wrapper_set_value(PyObject *self, PyObject *args) { int32_t val; PyObject *ret = NULL; if (PyArg_ParseTuple(args, "i", &val)) { set_value(val); ret = Py_BuildValue(""); } return ret; } static PyObject *py_object_function = NULL; static double alternative_obj_func(double x) { double ret_val = x; PyObject *arg, *result; // set argument arg = Py_BuildValue("(d)", x); // call function result = PyEval_CallObject(py_object_function, arg); // check result if (result && PyFloat_Check(result)) { ret_val = PyFloat_AsDouble(result); } Py_XDECREF(result); Py_DECREF(arg); return ret_val; } PyObject *wrapper_newton_method(PyObject *self, PyObject *args) { double x0; double est_x; int32_t max_iter; PyObject *target_func = NULL; PyObject *ret = NULL; if (PyArg_ParseTuple(args, "diO", &x0, &max_iter, &target_func)) { if (!PyCallable_Check(target_func)) { PyErr_SetString(PyExc_TypeError, "Need a callable object!"); } else { // set function pointer Py_INCREF(target_func); Py_XDECREF(py_object_function); py_object_function = target_func; // call function est_x = newton_method(x0, max_iter, alternative_obj_func); ret = Py_BuildValue("d", est_x); } } return ret; } static PyMethodDef custom_methods[] = { {"add", wrapper_add, METH_VARARGS, NULL}, {"set_value", wrapper_set_value, METH_VARARGS, NULL}, {"newton", wrapper_newton_method, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef custommodule = { PyModuleDef_HEAD_INIT, "custommodule", "", -1, custom_methods, }; PyMODINIT_FUNC PyInit_custommodule(void) { return PyModule_Create(&custommodule); }
ヘッダー定義
必要なヘッダーを読み込む.
#include <stdint.h> #include "Python.h" // Wrapperを作成するためのヘッダー #include "custom.h" // 自作のヘッダー
add関数のWrapper定義
add関数はint型の引数を二つ受け取り,和を計算後,int型の値を返却する.
// selfはPython側で呼び出す際のインスタンス(Cでは未使用),argsはCの関数の引数が渡される PyObject *wrapper_add(PyObject *self, PyObject *args) { int32_t x, y, z; PyObject *ret = NULL; if (PyArg_ParseTuple(args, "ii", &x, &y)) { // 引数をパース.「i」はint型を表す z = add(x, y); // Cの関数を呼び出す ret = Py_BuildValue("i", z); // 結果をPython側のオブジェクトに変換 } return ret; }
set_value関数のWrapper定義
set_value関数は,int型の引数を一つ受け取り,static変数に値を設定する.戻り値はない.
PyObject *wrapper_set_value(PyObject *self, PyObject *args) { int32_t val; PyObject *ret = NULL; if (PyArg_ParseTuple(args, "i", &val)) { // 引数をパース set_value(val); // Cの関数を呼び出す ret = Py_BuildValue(""); // void型の場合は,空文字列を指定し,返却 } return ret; }
newton_method関数のWrapper定義
引数に関数ポインタが含まれる例である.考え方としては,以下のようになる.
- 関数ポインタをPythonオブジェクトとして格納する領域をstatic変数として確保する.
- 関数ポインタとして渡された関数を実行するための関数(alternative_function)を定義する
- 引数をパースし,関数ポインタをstatic変数に格納後,alternative_functionを引数に渡し,Cの関数を呼び出す.
static PyObject *py_object_function = NULL; // 1. 関数ポインタをPythonオブジェクトとして格納する領域の確保 static double alternative_obj_func(double x) { // 2. alternative_functionの定義(フォーマットは,関数ポインタ側に合わせる) double ret_val = x; PyObject *arg, *result; // set argument arg = Py_BuildValue("(d)", x); // PyEval_CallObjectに渡す引数はタプルである必要があるらしい→"(d)"でタプルに変換 // call function result = PyEval_CallObject(py_object_function, arg); // py_object_functionには,事前に関数のアドレスを格納しておく(★で実施) // check result if (result && PyFloat_Check(result)) { ret_val = PyFloat_AsDouble(result); } Py_XDECREF(result); // 確保したオブジェクトを削除する...らしい(よく分かっていない) Py_DECREF(arg); return ret_val; } PyObject *wrapper_newton_method(PyObject *self, PyObject *args) { double x0; double est_x; int32_t max_iter; PyObject *target_func = NULL; // 関数ポインタ PyObject *ret = NULL; // "O"を指定することで,関数ポインタ(target_func)をPythonオブジェクトとして読み込む if (PyArg_ParseTuple(args, "diO", &x0, &max_iter, &target_func)) { // target_funcが呼び出し可能かを確認 if (!PyCallable_Check(target_func)) { PyErr_SetString(PyExc_TypeError, "Need a callable object!"); } else { // set function pointer Py_INCREF(target_func); // 引数で渡された関数の領域を確保 Py_XDECREF(py_object_function); // 事前に確保した関数の領域を解放(NULLを許容) py_object_function = target_func; // ★alternative_obj_funcの内部で利用するポインタに,引数で与えられた関数のアドレスを設定 // call function est_x = newton_method(x0, max_iter, alternative_obj_func); // target_funcの代わりにalternative_obj_funcを渡す ret = Py_BuildValue("d", est_x); } } return ret; }
moduleの作成
WrapperをPythonから呼び出す際の対応関係を定義し,moduleを作成する.
static PyMethodDef custom_methods[] = { // 左から順に,「python側の関数名」,「呼び出すCのWrapper関数名」,「引数タイプ」,「moduleのdocument(Pythonにおける__doc__)」 {"add", wrapper_add, METH_VARARGS, NULL}, // 位置引数のみを扱うため,METH_VARARGSを指定 {"set_value", wrapper_set_value, METH_VARARGS, NULL}, {"newton", wrapper_newton_method, METH_VARARGS, NULL}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef custommodule = { PyModuleDef_HEAD_INIT, // 常にPyModuleDef_HEAD_INITを指定 "custommodule", // module名(Pythonでimportする際の名称) "", // moduleのdocument -1, // メモリサイズ(よく分かっていない).今回は,内部状態を持つため,-1を指定しておく custom_methods, // method定義 }; PyMODINIT_FUNC PyInit_custommodule(void) { // moduleの作成 return PyModule_Create(&custommodule); }
ライブラリ作成
以下の順でライブラリを作成する.
この手続きを「create_library.sh」としてまとめる.
#!/bin/bash # Pythonのバージョン指定 python_version=3.8 # Pythonのprefixの取得 python_prefix=$(python3-config --prefix) # include directoryのパスの取得 python_include_path=${python_prefix}/include/python${python_version} # lib directoryのパスの取得 python_lib_path=${python_prefix}/lib cflags="" set -x # custom.cのコンパイル gcc -fpic ${cflags} -I./include -o custom.o -c src/custom.c # wrapper.cのコンパイル gcc -fpic ${cflags} -I./include -I${python_include_path} -o wrapper.o -c src/wrapper.c # custommodule.soの作成 gcc -L${python_lib_path} -lpython${python_version} -shared custom.o wrapper.o -o custommodule.so # struct PyModuleDefで定義したmodule名「custommodule」とする set +x
動作確認
スクリプトの準備
以下のようなpythonスクリプトをcustommodule.soと同じ階層に用意する.
import custommodule as c_mod # add function uses static variable # initial value of static variable is 0 c = c_mod.add(2, 3) print('c_mod.add(2, 3) = ', c) # 2 + 3 + 0 -> 5 c_mod.set_value(1) # set 1 to static variable print('c_mod.set_value(1)') c = c_mod.add(2, 3) print('c_mod.add(2, 3) = ', c) # 2 + 3 + 1 -> 6 # newton function uses function pointer f = lambda x: x * x - 2.0 # define target function est_x = c_mod.newton(2.0, 10, f) print('c_mod.newton(2.0, 10, f) = ', est_x) # sqrt(2.0) -> 1.4142135...
スクリプトの実行結果
実行結果を以下に示す.
c_mod.add(2, 3) = 5 c_mod.set_value(1) c_mod.add(2, 3) = 6 c_mod.newton(2.0, 10, f) = 1.414213562373095
期待通りの結果が得られている.