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
例外を投げます。
例外をキャッチし、エラーを表示するようにしています。
global
と local
変数のデストラクタは、 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_AppendInittab
は Py_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)