0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Boost.PythonでC++のプログラムをいじくる(windows) #2

Last updated at Posted at 2025-02-11

この記事は#1の続きです。#1では仕組みメインで解説しています(結局お気持ち表明になってしまったことは否めない)。
この記事では、実装メインで解説するので気になる方はこちら

インクルードパスを設定する

#1でたくさん書きなぐったのでそっちを見てください。Boostのインストール時にディレクトリを指定していない場合は、解凍したBoostフォルダの中にあるboostフォルダのあるフォルダとstageフォルダの中にあるlibをディレクトリに入れましょう。CPythonのディレクトリも忘れずに設定しましょう。Boostを静的ライブラリとしてインストールした場合はプリプロセッサの定義か、インクルードする前にマクロで

#define BOOST_PYTHON_STATIC_LIB

と定義しましょう

embed(埋め込み)でHello World

先にコードから

文字列で実行

cpp main.cpp
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#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に書き換えるだけです。

ファイル名で実行

python sample.py
print('Hello World!! with Python')
cpp main.cpp
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#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(ダイナミックリンクライブラリ)の作成

cpp sample.h
#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();
cpp sample.cpp
#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からの呼び出し

python sample.py
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(ダイナミックリンクライブラリ)の作成

cpp sample.h
#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();
cpp sample.cpp
#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から呼び出し

python sample.py
import sys
# .pydのファイルのディレクトリを指定(includeパスを指定するのと同じ)
sys.path.append("/path/to/pyd(dll)")
# 生成した.pyd(dll)をインポート
import sample

print("num : " + str(sample.get()))

C++に埋め込んで実行

cpp main.cpp
// 静的ライブラリを使用する場合、動的ライブラリの場合は書かなくてよい
#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属性を付けることでメモリが同一アプリケーション内であれば共有されます。先ほどのプログラムで定義したnumextern属性を付与して、プラスアルファで変更が反映されているかどうか確かめるために変数をいじくれる関数を実装しましょうか。

dllファイル

cpp sample.h
#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);
cpp sample.cpp
#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から呼び出し

python sample.py
import sys
# .pydのファイルのディレクトリを指定(includeパスを指定するのと同じ)
sys.path.append("/path/to/pyd(dll)")
# 生成した.pyd(dll)をインポート
import sample

sample.assign(7)
print("num : " + str(sample.get()))

C++に埋め込んで実行

cpp main.cpp
#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ライブラリの作成

cpp core.h
#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);
}
cpp core.cpp
#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形式に変換

cpp sample.h
#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();
cpp sample.cpp
#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での呼び出し

python sample.py
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++上で埋め込んで実行

cpp main.cpp
#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ライフをお過ごしください~

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?