読者です 読者をやめる 読者になる 読者になる

たまに書きます。

気になって調べたことを書いていきます。まずはAboutページをご覧ください。

C/C++からCythonを介してPythonの関数を呼び出す話

下で技術系以外のことも書いて行こうと主張した直後でまた技術系の話です。
Cythonについては、PythonからC/C++の関数を呼び出す為のツールで、これについては情報が至る所にあるのでそちらを参照して欲しい。
最近はオライリーから本も出たようだ。
Amazon.co.jp: Cython ―Cとの融合によるPythonの高速化: Kurt W. Smith, 中田 秀基, 長尾 高弘: 本

これを使えば計算コストのかかる部分、例えば二重のループなどをC言語で書いておき、それをPythonから呼び出すことで、フロントエンドはPythonの皮をかぶったまま、中は以外に速い、ということが実現できる。
最近関わっていたプロジェクトでいろいろと書いていたのは、この逆のパターンで、「Cythonでラップされた[C/C++で書かれた]関数から、Cythonを介してPythonの関数を呼び出す方法」である。
このような状況が必要になる機会というのは、そう滅多にはないと思う。自分が関与していたのは、常微分方程式(Ordinal Differential Equation: ODE)を数値積分して行く際、その時々で呼ばれるdx/dtを返す関数をPythonでコールバックとして登録しておき、その値を埋めたら再びC++のODEソルバーに戻ってステップして行く、というものだった。
(sympyなり、別のツールを使えば良いのかもしれないが、今回はプロジェクト上の都合で他のモジュールとの連携などもあり、こうせざるを得なかった)

使用した方法に関しての元ネタは、stackoverflow.com

では方法論に入ろう。
最終的に実行できるようにしたいコードは、以下のようになる。へボいが、エッセンスはこれで一通り詰まっている。

import callfromcpp

def func(n):
    return n * 2;

cb = callfromcpp.PyFrontend(func)
print cb.callback(5.0)

このcb.callbackという部分が、Cで書かれた関数を呼び出して、そのなかからPythonで書かれたfuncという関数を呼び出す関数である。

1、まずはバックエンドを書く。
ここで使用したバックエンドのコードは以下のよう。
backend.hpp

#ifndef __GUARD
#define __GUARD

class cpp_backend {
public:
    // This is wrapper of Python fuction.
    typedef double (*method_type)(void *param, void *callback_func);

    // Constructor
    cpp_backend(method_type, void *user_data);
    // Destructor
    virtual ~cpp_backend();

    double callback_python(void *parameter);

private:
    method_type method_;
    void *python_callback_pointer_;
};

#endif

backend.cpp

#include "backend.hpp"

cpp_backend::cpp_backend(method_type method, void *callback_func)
    : method_(method), python_callback_pointer_(callback_func)
{}

cpp_backend::~cpp_backend()
{}

double cpp_backend::callback_python(void *parameter)
{
    return this->method_(parameter, python_callback_pointer_);
}

これは単体では動かないが、pythonのヘッダーやモジュールに依存している訳ではないので、

$> g++ backend.cpp

としたらコンパイルは通るはずだ(ただし、_mainが無いといってリンカーに怒られる)。

次にcythonに渡すファイルを作成する。

from cython.operator cimport dereference as deref
import sys
# referenced from
# http://stackoverflow.com/questions/5242051/cython-implementing-callbacks

ctypedef double (*method_type)(void *param, void *user_data)

cdef extern from "backend.hpp":
    cdef cppclass cpp_backend:
        cpp_backend(method_type method, void *callback_func)
        double callback_python(void *parameter)

cdef double scaffold(void *parameter, void *callback_func):
    return (<object>callback_func)(<object>parameter)


cdef class PyFrontend:
    cdef cpp_backend *thisptr

    def __cinit__(self, pycallback_func):
        self.thisptr = new cpp_backend(scaffold, <void*>pycallback_func)

    def __dealloc__(self):
        if self.thisptr:
            del self.thisptr

    cpdef double callback(self, parameter):
        return self.thisptr.callback_python(<void*>parameter)

最後にこれらをビルドする為のsetup.pyが以下。

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

source = [
    'backend.cpp',
    './front.pyx'
    ]

setup(
    cmdclass = dict(build_ext = build_ext),
    ext_modules = [
        Extension(
            'callfromcpp',
            source,
            language='c++',
        )
    ]
)

ここでの重要なのは、front.pyの中で定義されている、scaffoldという関数だ。
文字通り、この関数はC++からPythonの関数を呼び出すときの足場としての役割を果たすものだ。これを介すことで、C++Python処理系に依存することなく、関数の形を特定することができる。逆に言えば、Pythonで書いたコールバックの引数が増えるときや、配列を扱うときなどはここで変換を行ってやる必要がある。
上記を行った上で、最初に示したtest.pyが

$> python test.py
10.0

と返ってくれば正解だ。