LoginSignup
26
22

More than 5 years have passed since last update.

C++ プログラムに pybind11 で Python を組み込んでみる

Posted at

pybind11 を使用して、 C++ プログラムに Python を埋め込む方法です。

Python から C++ コードを呼ぶ方法は、以下が参考になるかと思います。

pybind11 では C++ コードから Python を呼ぶこともできるようなので、試してみました。

Python 環境

Python3 を前提としていますが、呼び出す Python ライブラリの関数を変えれば、Python2 でも動くと思います。

セットアップ

pybind11 はヘッダーオンリーなので、ヘッダーをコピーしてくるだけです。
後は、ソースコードに pybind11 をインクルードしてください。

#include <pybind11/pybind11.h>
#include <pybind11/eval.h>

Python.h は直接インクルードしないほうがいいです。
後述しますが、 Windows 環境で問題が起こることがあります。

C++ コードから Python コードを実行する

もととなるコードを以下のようなものです。

#include <pybind11/eval.h>
#include <pybind11/pybind11.h>

#include <iostream>

namespace py = pybind11;

int main(int argc, char** argv) {
    wchar_t* program = Py_DecodeLocale(argv[0], nullptr);
    if (program == nullptr) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);
    Py_Initialize();

    try {
        auto global = py::dict(py::module::import("__main__").attr("__dict__"));
        auto local = py::dict();
        // ここにコードを記述していく
    } catch (py::error_already_set& e) {
        std::cout << "Python error.\n" << e.what() << "\n";
    }

    Py_Finalize();
    PyMem_RawFree(program);

    return 0;
}

pybind11 は、 Python コードに失敗した際、py::error_already_set 例外を投げます。
例外をキャッチし、エラーを表示するようにしています。

globallocal 変数のデストラクタは、 Py_Finalize が呼び出される前に実行しなければなりませんので注意してください。

簡単なサンプル

以下で説明されいてるコードを実行します。
https://docs.python.org/3/extending/embedding.html#very-high-level-embedding

py::eval<py::eval_single_statement>("from time import time,ctime", global, local);
py::eval("print('Today is', ctime(time()))", global, local);

Python の1行文は py::eval_single_statement を使います。
式は py::eval_expr を使います。 (デフォルト)

変数を定義する

local["x"] = 100;
py::eval<py::eval_single_statement>("y = 200", global, local);
py::eval("print('x + y =', x + y)", global, local); // x + y = 300

変数名をキーとして、値を代入するだけです。

値の取得

py::eval<py::eval_single_statement>("z = x + y", global, local);
auto z = local["z"].cast<int>();
std::cout << "cpp: z = " << z << "\n"; // cpp: z = 300

変数名をキーとして値を取得し、変換したい型へ cast を呼び出します。

関数定義と呼び出し

py::eval<py::eval_statements>(
    "def func_01():\n"
    "   print('func_01: call')\n",
    global, local);
auto func_01 = local["func_01"];
func_01(); // func_01: call

関数名をキーとして関数オブジェクトを取得し、 operator () で呼び出します。
call() という関数もあるのですが、使用は推奨されていないようです。

関数へ引数を渡す

py::eval<py::eval_statements>(
    "def func_02(a, b):\n"
    "   print('func_02: {} + {} = {}'.format(a, b, a + b))\n",
    global, local);
auto func_02 = local["func_02"];
func_02(123, 456);     // func_02: 123 + 456 = 579
func_02("abc", "efg"); // func_02: abc + efg = abcdefg

便利ですね。

関数の戻り値を取得する

py::eval<py::eval_statements>(
    "def func_03(a, b):\n"
    "   return a + b\n",
    global, local);
auto func_03 = local["func_03"];
auto result = func_03(123, 456);
std::cout << "cpp: func_03 result "
      << py::str(result).cast<std::string>() << "\n"; // cpp: func_03 result 579

戻り値に何が入っているかわからないと仮定し py::str で文字列に変換しています。

プログラム内でモジュールを用意する

pybind11 でモジュールを作成し、 C++ のコードを呼ぶ方法は、上記リンクに詳しく書いてあります。

C++ プログラムにモジュールを埋め込むこともできます。
https://docs.python.org/3/extending/extending.html#the-module-s-method-table-and-initialization-function

初期化関数の用意

pybind11 のマクロ PYBIND11_PLUGIN を使用せずに、初期化関数を作成します。
定義しているクラスは、 C++ のプログラムと連携させるサンプルに使用します。

class Job {
public:
    std::string GetName() const { return m_name; }
    void SetName(const std::string& name) { m_name = name; }

private:
    std::string m_name;
};

class Person {
public:
    std::string GetName() const { return m_name; }
    void SetName(const std::string& name) { m_name = name; }

    std::shared_ptr<Job> GetJob() const { return m_job; }
    void SetJob(const std::shared_ptr<Job>& job) { m_job = job; }

private:
    std::string m_name;
    std::shared_ptr<Job> m_job;
};

namespace py = pybind11;

PyMODINIT_FUNC PyInit_sample() {
    py::module m("sample", "pybind11 module sample.");

    py::class_<Job, std::shared_ptr<Job>> job(m, "Job");
    job.def(py::init<>()).def_property("name", &Job::GetName, &Job::SetName);

    py::class_<Person, std::shared_ptr<Person>> person(m, "Person");
    person.def(py::init<>())
        .def_property("name", &Person::GetName, &Person::SetName)
        .def_property("job", &Person::GetJob, &Person::SetJob);

    return m.ptr();
}

初期化関数の登録

int main(int argc, char** argv) {
    wchar_t* program = Py_DecodeLocale(argv[0], nullptr);
    if (program == nullptr) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }

    PyImport_AppendInittab("sample03", PyInit_sample03);

    Py_SetProgramName(program);
    Py_Initialize();

    py::module::import("sample03");

PyImport_AppendInittabPy_Initialize より前に呼ぶようにしてください。

C++ プログラムとの連携

try {
    auto global = py::dict(py::module::import("__main__").attr("__dict__"));

    // Python で関数を作成 (Name=Hoge, Job=Teacher)
    py::eval<py::eval_statements>(
        "import sample\n"
        "def initialize_person(p):\n"
        "   job = sample.Job()\n"
        "   job.name = 'Teacher'\n"
        "   p.name = 'Hoge'\n"
        "   p.job = job\n",
        global);
    {
        auto person = std::make_shared<Person>();
        global["initialize_person"](person);
        std::cout << "Name : " << person->GetName() << "\n";           // Name : Hoge 
        std::cout << "Job  : " << person->GetJob()->GetName() << "\n"; // Job  : Teacher
    }

    // 関数を変更 (Name=Foo, Job=Programmer)
    py::eval<py::eval_statements>(
        "import sample\n"
        "def initialize_person(p):\n"
        "   job = sample.Job()\n"
        "   job.name = 'Programmer'\n"
        "   p.name = 'Foo'\n"
        "   p.job = job\n",
        global);
    {
        auto person = std::make_shared<Person>();
        global["initialize_person"](person);
        std::cout << "Name : " << person->GetName() << "\n";           // Name : Foo
        std::cout << "Job  : " << person->GetJob()->GetName() << "\n"; // Job  : Programmer
    }
} catch (py::error_already_set& e) {
    std::cout << "Python error.\n" << e.what() << "\n";
}

C++ 側でオブジェクトを用意し、 Python 側で値を設定しています。

Windows 環境の注意

Windows 環境で #include <python.h> を行うと、でバックビルド時にデバッグ版のライブラリをリンクするようになります。
デバッグ版はインストール時に追加しておかなければなりません。
また、Python のデバッグ版は、モジュール等に「_d」をつけなければなりません。

pybind11 の <pybind11/pybind11.h> では、デバッグビルド時でも、リリース版のライブラリをリンクするような設定が行われます。
<Python.h> を先にインクルードすると意味がなくなるので、注意してください。

Windows 環境で CMake を使用する際の注意

CMake には Python 環境を見つける FindPythonLibs があります。
これを使用すると、 Windows 環境ではデバッグビルド時に、デバッグ版のライブラリをリンクするようになってしまいます。
その状態で pybind11 を使用すると、リンクできなくなるので注意が必要です。

CMake を使用する場合は、 pybind11 に付属している PythonLibsNew を使用するか、 pybind11Tools を使用してください。

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/pybind11/cmake")
set(Python_ADDITIONAL_VERSIONS 3.7 3.6 3.5 3.4)
find_package(PythonLibsNew ${PYBIND11_PYTHON_VERSION} REQUIRED)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/pybind11/cmake")
include(pybind11Tools)

pybind11Tools には、モジュールを簡単に定義できる pybind11_add_module があります。
これを使えば、作成されるモジュールの拡張子を .pyd にしてくれます。

pybind11_add_module(module_sample module_sample.cpp)
26
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
22