FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

C++にPythonを埋め込んで対話モードっぽく使う方法まとめ

現在の自作ISSでは、対話インタフェースとしてLuaを使っている。Pythonでも同じようなことが出来ると聞いていたが、Luaを採用した理由は、「Pythonよりも軽いから」だった。 どっちにしろPythonの高度な機能は使う気が無いし、Luaの軽い環境でさくさくISSを制御できるようになれば十分と思っていたのでPythonは遠慮していたが、仕事でインタプリタのような形式でデバッガを用意する必要が生じたので、これを気にPythonインタフェースを利用してC++アプリにPythonの対話モードを追加し、さらに所望の関数を動作させる方法について調べてみた。

Pythonをダウンロードする

Pythonのバージョンには、2.7を利用した。使ったのはWindows版で、C++の開発環境としてはVisual Studio Express 2015を利用した。

www.python.org

ダウンロードしてインストールすると、C:\Python27にインストールされる。今やりたいことはVisual Studioで読み込んでC++に組込みたいので、ライブラリとヘッダファイルの位置を確認する。

  • ライブラリ : C:\Python27\libs
  • ヘッダファイル : C:\Python27\include

C++から読み出すためには、まず何をすれば良いか

Pythonのドキュメントを調査していると、いろいろ関数が用意されていることが分かった。ファイルからPythonスクリプトを読み込んで実行する(PyRun_SimpleFileなど)はあるが、対話モードとして動作させたいときはどうするんだろう? といろいろ探していると、Py_Main()という関数があることが分かった。

超高レベルレイヤ — Python 2.7ja1 documentation

日本語の翻訳レベルが酷すぎるので、英語版を参照してみる。

The main program for the standard interpreter. This is made available for programs which embed Python.

という訳で、まずはとりあえずPy_Main()を呼び出せばインタプリタが実行できる訳だ。 ちなみに、マニュアルに書いてある通り、同様の環境からPythonファイルを指定して実行したい場合、C言語のargc, argvをそのまま渡してやることによって実現可能だ。

int main(int argc, char **argv)
{
    Py_Initialize();

    python_initialize();

    Py_Main(argc, argv);

    Py_Finalize();
    return 0;

}

実行すると、Pythonインタフェースが起動する。これで、まずは環境を構築できた。

Pythonインタフェースに関数を追加する。

Pythonインタフェースから呼び出せるC++関数を追加するためには、まずはmoduleを作成し、そこにPythonから呼び出せるインタフェース関数を登録、インタフェース関数からC++の関数を呼び出すようにする。

まずは、初期化の関数を以下のように書いてみた。

void python_initialize()
{
    Py_InitModule ("tci", tci_methods);
    PyRun_SimpleString("def funcA(a, b):\n    return tci.py_funcA(a, b)");
    PyRun_SimpleString("def funcB(a, b):\n    return tci.py_funcB(a, b)");

    return;
}

funcAとfuncBを、Pythonから実行できるインタフェースとして登録する。funcAはtciモジュールのpy_funcAを、funcBはtciモジュールのfuncBを呼び出す訳だ。 py_funcAとpy_funcBが実際に何を呼ぶかについては、テーブルを作って管理する。

static PyMethodDef tci_methods[] = {
    { "py_funcA", hello_add, METH_VARARGS, "Add (A, B) := A+B    " },
    { "py_funcB", cpp_funcB , METH_VARARGS, "HogeHoge  " },
    { NULL, NULL, 0, NULL }        /* Sentinel */
};

これにより、py_funcAが呼ばれたときはC++の実装としてhello_add()、 py_funcBが呼ばれたときには、C++の実装としてcpp_funcB()が呼びだされる。 これらのC++の関数で、Pythonから読んだときの引数を解析するためには、PyArg_ParseTuple()を利用する。詳しくは

引数の解釈と値の構築 — Python 2.7ja1 documentation

にちゃんと書いてあるので確認すること。 必要でらあれば、さらにサブモジュール関数を呼び出すこともできる。

これにより、PythonインタプリタからC++の実装を呼び出すことができるようになる。もちろん最初に作成したtciモジュールをインポートしないと動かないので、Py_Main()を実行する前に挿入しておくこと。

int main(int argc, char **argv)
{
    Py_Initialize();

    python_initialize();
    PyRun_SimpleString("import tci");
    Py_Main(argc, argv);

    Py_Finalize();
    return 0;

}

...

PyObject* hello_add(PyObject* self, PyObject* args)
{
    int x, y, g;

    if (!PyArg_ParseTuple(args, "ii", &x, &y))
        return NULL;
    g = x+y;
    return Py_BuildValue("i", g);
}

Pythonインタフェースから呼び出してみる。

>>> funcA(1, 2)
3

実際の呼び出しの手順としては、funcA(1,2)→tci.py_funcA(1,2)→hello_add(PyObject args)→1+2 となっている訳だ。ちょっと複雑だが、これでとりあえず動作するようになった!

PyRun_SimpleFileがWindows上でクラッシュするときの対処方法

これは自分も引掛った。

bytes.com

つまり、下記のように記述すれば回避できるってことだ。

        PyObject* PyFileObject = PyFile_FromString(argv[1], "r");
        PyRun_SimpleFile(PyFile_AsFile(PyFileObject), argv[1]);