この記事は#1の続きです。#1では仕組みメインで解説しています(結局お気持ち表明になってしまったことは否めない)。
この記事では、実装メインで解説するので気になる方はこちら
インクルードパスを設定する
#1でたくさん書きなぐったのでそっちを見てください。Boostのインストール時にディレクトリを指定していない場合は、解凍したBoostフォルダの中にあるboostフォルダのあるフォルダとstageフォルダの中にあるlibをディレクトリに入れましょう。CPythonのディレクトリも忘れずに設定しましょう。Boostを静的ライブラリとしてインストールした場合はプリプロセッサの定義か、インクルードする前にマクロで
#define BOOST_PYTHON_STATIC_LIB
と定義しましょう
embed(埋め込み)でHello World
先にコードから
文字列で実行
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
namespace cpy = boost::python;
int main() {
// パイソンのインタープリンタの初期化
Py_Initialize();
try {
// __main__ モジュールの名前空間で検索する C++でいう namespace に近い
cpy::object object = cpy::import("__main__").attr("__dict__");
// 実行する
cpy::exec("print('Hello World!! with Python')", object);
}
catch (const cpy::error_already_set&) {
// エラーを吐いた場合にエラーのログをプリントする
PyErr_Print();
}
// パイソンのインタープリンタの終了処理
Py_Finalize();
}
意味に関してはコメントアウトで書いてある通りです。こんな感じで実行します。
しかしこれだといちいち文字列を書かなくてはいけないので不便です。ということで.pyファイルから実行できるようにしましょう。といってもexecのところをexec_fileに書き換えるだけです。
ファイル名で実行
print('Hello World!! with Python')
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
namespace cpy = boost::python;
int main() {
// パイソンのインタープリンタの初期化
Py_Initialize();
try {
// __main__ モジュールの名前空間で検索する C++でいう namespace に近い
cpy::object global = cpy::import("__main__").attr("__dict__");
cpy::object local = cpy::import("__main__").attr("__dict__");
// 実行する
cpy::exec_file("sample.py", global, local);
}
catch (const cpy::error_already_set&) {
// エラーを吐いた場合にエラーのログをプリントする
PyErr_Print();
}
// パイソンのインタープリンタの終了処理
Py_Finalize();
}
両者ともにファイル名のところが正しければ、コンパイルが成功するとメッセージが出力されるはずです。簡単ですね。
extend(拡張)でHello World
今度はC++の関数をPythonでインポートしてHello Worldしましょう。埋め込みのほうはディレクトリの指定がめんどくさいぐらいでしたがこっちは少し難しいです。
dll(ダイナミックリンクライブラリ)の作成
#pragma once
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
#ifdef SAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif
// 宣言
extern "C" SAMPLE_API void hello();
#define SAMPLE_EXPORTS
#include "sample.h"
namespace cpy = boost::python;
// 定義
extern "C" SAMPLE_API void hello() {
std::cout << "Hello World!! with C++" << std::endl;
}
// エクスポートする関数を定義 (モジュール名)
BOOST_PYTHON_MODULE(sample) {
cpy::def("hello", hello);
}
これをコンパイルして.dllファイルを生成します。生成方法や書いてあることの意味(ほぼおまじないだけど)を知りたければ、動的リンクライブラリと検索してください。ほぼおまじないなので、なんか意味わからないのが多いですが、すぐに慣れると思います。
.dllというと動的リンクのほかに動的ロードが出てきますが、全くの別物なので注意してください。
.dllを生成したら拡張子を.dllから.pydに無理やり変更します。ファイルが使えなくなるかもと脅されますが、無視しても問題ありません。
それではいよいよPythonのコードを書いていきます
Pythonからの呼び出し
import sys
# .pydのファイルのディレクトリを指定(includeパスを指定するのと同じ)
sys.path.append("/path/to/pyd(dll)")
# 生成した.pyd(dll)をインポート
import sample
sample.hello()
うまくいけば
Hello World!! with C++
と出力されるはずです。
C++とPythonのプログラムのメモリの共有
さて、本題に入りましょう。もう忘れかけてるかもしれませんが、これをゲームの構文として利用したいんですよ。ということで実装する必要がある機能としては
- 埋め込みで実行
- 値を取得する
- Pythonで書き換えた値をC++側で取得
埋め込みで実行に関しては一番最初に実装しました。次の値を取得もここまでくればそこまで難しくないです。問題は一番下ですね。
値を取得
C++の変数の値を取得できるプログラムを作ります。といっても、C++の関数をPythonにエクスポートできたので同じようなことをすればいいだけです。
dll(ダイナミックリンクライブラリ)の作成
#pragma once
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
#ifdef SAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif
int num = 8;
// 宣言
extern "C" SAMPLE_API void hello();
extern "C" SAMPLE_API int get();
#define SAMPLE_EXPORTS
#include "sample.h"
namespace cpy = boost::python;
// 定義
extern "C" SAMPLE_API void hello() {
std::cout << "Hello World!! with C++" << std::endl;
}
extern "C" SAMPLE_API int get() {
return num;
}
// エクスポートする関数を定義 (モジュール名)
BOOST_PYTHON_MODULE(sample) {
cpy::def("hello", hello);
cpy::def("get", get);
}
Pythonから呼び出し
import sys
# .pydのファイルのディレクトリを指定(includeパスを指定するのと同じ)
sys.path.append("/path/to/pyd(dll)")
# 生成した.pyd(dll)をインポート
import sample
print("num : " + str(sample.get()))
C++に埋め込んで実行
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
namespace cpy = boost::python;
int main() {
// パイソンのインタープリンタの初期化
Py_Initialize();
try {
// __main__ モジュールの名前空間で検索する C++でいう namespace に近い
cpy::object global = cpy::import("__main__").attr("__dict__");
cpy::object local = cpy::import("__main__").attr("__dict__");
// 実行する
cpy::exec_file("sample.py", global, local);
}
catch (const cpy::error_already_set&) {
// エラーを吐いた場合にエラーのログをプリントする
PyErr_Print();
}
// パイソンのインタープリンタの終了処理
Py_Finalize();
}
ここまでくればもう簡単ですね。見なくてもできると思います。
- 埋め込みで実行
- 値を取得する
- Pythonで書き換えた値をC++側で取得
Pythonで書き換えた値をC++側で取得
問題はここです。さてどうしようか...
まず、dll
の仕様としては複数のファイル間で同一のアプリケーションであればメモリを共有することができます。つまり、あるファイルでの値の変更がリンクしている別のファイルでも反映されるということですね。
ということで是非ともこれを使いたいですが、Boost.Pythonはどのような扱いなのでしょうか?別アプリケーションでの実行扱いであれば別の方法を探す必要があります。
でBoost 公式ドキュメントを見ると、同一アプリケーション上で実行するということです!!やったー!
これは確定演出ということでウキウキ気分で実装できます。
まず、変数をただ定義するだけではメモリは共有されませんextern
属性を付けることでメモリが同一アプリケーション内であれば共有されます。先ほどのプログラムで定義したnum
にextern
属性を付与して、プラスアルファで変更が反映されているかどうか確かめるために変数をいじくれる関数を実装しましょうか。
dll
ファイル
#pragma once
#define BOOST_PYTHON_STATIC_LIB
#include <iostream>
#include <boost/python.hpp>
#ifdef SAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif
// extern を変数に使用する場合、宣言と定義を別で行う必要がある
// 宣言
extern int num;
// 宣言
extern "C" SAMPLE_API void hello();
extern "C" SAMPLE_API int get();
extern "C" SAMPLE_API void assign(int _n);
#define SAMPLE_EXPORTS
#include "sample.h"
namespace cpy = boost::python;
// 定義
int num = 0;
// 定義
extern "C" SAMPLE_API void hello() {
std::cout << "Hello World!! with C++" << std::endl;
}
extern "C" SAMPLE_API int get() {
return num;
}
extern "C" SAMPLE_API void assign(int _n) {
num = _n;
}
// エクスポートする関数を定義 (モジュール名)
// Python側で使えるようにラップする
BOOST_PYTHON_MODULE(sample) {
cpy::def("hello", hello);
cpy::def("get", get);
cpy::def("assign", assign);
}
assign
関数は引数がありますが特に記述する必要はなく、同じようにエクスポートすればいいです。簡単でいいですね~
あと、動的ライブラリのロードのためにsample.lib
を依存ファイルに含める必要があります。ディレクトリとかいいかんじにやってください。
Pythonから呼び出し
import sys
# .pydのファイルのディレクトリを指定(includeパスを指定するのと同じ)
sys.path.append("/path/to/pyd(dll)")
# 生成した.pyd(dll)をインポート
import sample
sample.assign(7)
print("num : " + str(sample.get()))
C++に埋め込んで実行
#define BOOST_PYTHON_STATIC_LIB
#include <sample.h>
#include <boost/python.hpp>
int main() {
namespace cpy = boost::python;
Py_Initialize();
try {
cpy::object global = cpy::import("__main__").attr("__dict__");
cpy::object local = cpy::import("__main__").attr("__dict__");
cpy::exec_file("sample.py", global, local);
}
catch (const cpy::error_already_set&) {
PyErr_Print();
}
Py_Finalize();
std::cout << "num : " << get() << std::endl;
}
こちらも同じくさっきの.lib
ファイルを含める必要があるのに加えて、インクルードディレクトリを指定する必要があります。ヘッダーファイルのディレクトリを指定してあげましょう
で結果は..........
num : 7
num : 0
はい?
はい。まあそういうことです。
最初こうなった時、さっきも言ったようにソースが少ないのでGitHubCopilotに頼り切ってたんですけど、一人で切れ散らかしてました。
一体なんでこうなったのか。
原因
ポイントはライブラリのロードです。
C++側では.lib
ファイルを使ってやり取りしましたが、Python側では.dll
ファイルを使ってリンクしています。おそらくこれら2つのファイルが違うようで、別ライブラリでリンクしているのでメモリが共通で確保されなかったということです。
よって、両方ともどうにかして.lib
ファイルからリンクすればメモリがうまく共有されそうです。
解決策
Python側はデフォルトではどう頑張っても.dll
です。ライブラリを使うことでうまくロードできるかもしれませんが呼び出しのオーバーヘッドが発生しますし、学習コストがかかります。
じゃあどうするのかというと、方法は1つしかありません。C++を通じてロードするということです。
ということでもう1つ動的リンクライブラリを追加して、ここで.lib
をロードするのはどうでしょうか。一応C++側からの呼び出しです。図示するとこんな感じ
core.lib
で値をいろいろ変更して、それをsample.pyd(.dll)
Python向けにラップしつつ、.dll
形式に変換することで、Python側でもうまく呼び出せるようにしつつ、大元は同じになり、うまくメモリを共有できそうです。ということでこれを実装してきましょう。
実装
core
ライブラリの作成
#pragma once
#include <iostream>
#ifdef DLL_EXPORTS
#define DLL_SETS __declspec(dllexport)
#elif DLL_IMPORTS
#define DLL_SETS __declspec(dllimport)
#endif
extern int num;
namespace _core {
extern "C" DLL_SETS void init();
extern "C" DLL_SETS void assign(int _n);
extern "C" DLL_SETS int get(int& _n);
}
#define DLL_EXPORTS
#include "core.h"
using namespace _core;
// 定義
int num = 0;
// 定義
DLL_SETS void _core::init()
{
num = 0;
}
DLL_SETS void _core::assign(int _n)
{
num = _n;
}
DLL_SETS int _core::get()
{
return num;
}
Python向けにラップしつつ、.pyd
形式に変換
#pragma once
#define BOOST_PYTHON_STATIC_LIB
#include <core.h>
#include <boost/python.hpp>
#ifdef SAMPLE_EXPORTS
#define SAMPLE_API __declspec(dllexport)
#else
#define SAMPLE_API __declspec(dllimport)
#endif
// 宣言
extern "C" SAMPLE_API void ass(int _num);
extern "C" SAMPLE_API void print();
#define DLL_EXPORTS // エラーいっぱい吐く場合はこれを記述すれば治るかも?
#define SAMPLE_EXPORTS
#include "sample.h"
using namespace _core;
// 定義
extern "C" SAMPLE_API void ass(int _num) {
assign(_num);
}
extern "C" SAMPLE_API void print() {
std::cout << "num : " << get() << std::endl;
}
// Python向けにラップ
BOOST_PYTHON_MODULE(sample) {
namespace cpy = boost::python;
cpy::def("ass", ass);
cpy::def("print", print);
}
ライブラリのお作法は忘れずにやってください。めんどくさいので割愛します。
Pythonでの呼び出し
import sys
sys.path.append("C:/Users/rayrk/source/repos/boost_OpenGL/lib/Python")
import sample
# 代入
sample.ass(7)
# 現在の値を取得(Python側)
print("num : " + str(sample.get()))
C++上で埋め込んで実行
#define BOOST_PYTHON_STATIC_LIB
#include <core.h>
#include <boost/python.hpp>
int main() {
namespace cpy = boost::python;
Py_Initialize();
try {
cpy::object global = cpy::import("__main__").attr("__dict__");
cpy::object local = cpy::import("__main__").attr("__dict__");
cpy::exec_file("sample.py", global, local);
}
catch (const cpy::error_already_set&) {
PyErr_Print();
}
Py_Finalize();
std::cout << "num : " << get() << std::endl;
}
さあ、結果ですが
num : 7
num : 7
という感じに出力されたら成功です。
- 埋め込みで実行
- 値を取得する
- Pythonで書き換えた値をC++側で取得
まとめ
いかがでしたか?実行速度にやや懸念はありますが、ほかの言語の拡張や埋め込みと比較しても圧倒的に直感的な記述で行うことができます。スクリプトエンジンは実装に非常にコストがかかるので時間短縮につながります。ゲームエンジンもどきの個人製作においてこれはデカいですね。ということで楽しいBoost.Python
ライフをお過ごしください~