作業中のメモ

よく「計算機」を使って作業をする.知らなかったことを中心にまとめるつもり.

PythonからCの関数を呼び出す(Cの関数の共有ライブラリ化) Part1

どうも,筆者です.

今回は,Python.hを利用して,C言語で作成した関数をPythonから呼び出す方法について扱ってみる.

対象

ここでは,以下の二つの内容について取り扱う.

  • 内部でstatic変数を用いる関数
  • 引数として関数ポインタ(function pointer)を受け取る関数

参考サイト

参考にしたサイトを以下に示す.

docs.python.org

docs.python.org

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定義

引数に関数ポインタが含まれる例である.考え方としては,以下のようになる.

  1. 関数ポインタをPythonオブジェクトとして格納する領域をstatic変数として確保する.
  2. 関数ポインタとして渡された関数を実行するための関数(alternative_function)を定義する
  3. 引数をパースし,関数ポインタを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);
}

ライブラリ作成

以下の順でライブラリを作成する.

  1. 自作関数custom.cのコンパイル
  2. Wrapper関数wrapper.cのコンパイル
  3. objectファイルをリンクし,shared libraryを作成

この手続きを「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

期待通りの結果が得られている.