Help us understand the problem. What is going on with this article?

【Embeddable Python】C++からPythonを呼び出すWindowsソフトを作る

やりたいこと

C++でWindows用のリズムゲームを作っていたが、譜面の読み込みが面倒だったので、
1. C++ からPythonの関数を呼び出す
2. Pythonを使って譜面読み込みをする
3. 関数の戻り値として譜面を解釈した数値データをリストで返す
4. C++でリストをvectorに変換
をやってみる。ここでは簡単のため、譜面の数学的な処理は省略する。

下準備

デバッグシンボルの導入

Visual Studio 2019で開発をするため、デバッグ用のシンボルが必要になる。シンボルはPythonのインストーラを起動したときに、Advanced OptionsDownload debugging symbolsDownload debug binariesにチェックを入れることで導入できる。
001.png
Pythonのインストール場所はできるだけシンプルな場所にすることをお勧めする。(今回はC:\Program Files\Python36\)

Embeddable Pythonをダウンロード

Pythonの公式HPから、任意のバージョンのWindows x86-64 embeddable zip fileをダウンロード、解凍。開発するパソコンにインストールしてあるPythonとバージョンを合わせた方が無難だろう。

Visual Studioの設定

Pythonを導入したいプロジェクトで、「プロジェクト(P)」→「プロパティ」を開き「<プロジェクト名> プロパティ ページ」ウィンドウを開き、そのウィンドウの左上にある「構成(C)」を選択して、「すべての構成」に変更する。「構成プロパティ」→「VC++ディレクトリ」→「全般」→「インクルード ディレクトリ」を編集して、<Pythonをインストールしたディレクトリ>\includeを追加する。(今回はC:\Program Files\Python36\include)また、「ライブラリ ディレクトリ」を編集して、<Pythonをインストールしたディレクトリ>\libsを追加する。(今回はC:\Program Files\Python36\libs)

Embeddable Pythonの導入

プロジェクトのメインフォルダ(デフォルトでmain.cppなどが入っているフォルダ)に、先ほどダウンロードしたEmbeddable Pythonのpython3.dll,python36._pth,python36.dll,sqlite3.dll,vcruntime.dllを入れる。また、pythonフォルダを作成し、ダウンロードした残りのファイルをそこに入れる。
メインフォルダ
000.png
pythonフォルダ
002.png
ここで、メインフォルダに入れた、python36._pthを編集して、

python36._pth
python/python36.zip
.

にする。

いよいよコーディング

C++でPythonの初期化

公式リファレンスでPy_SetProgramNameを設定することを推奨していたので、そうした。
今回は自作したPythonの関数を使用したいので、Pythonのモジュール検索パスにカレントディレクトリを追加した。(7-9行目)

main.cpp
#include <Python.h>

int main(int argc, char *argv[]) {
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    Py_SetProgramName(program);
    Py_Initialize();
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(sys, PyUnicode_FromString("./"));
    //ここにコードを書いていく
    return 0;
}

Pythonを使って譜面ファイルを読み込み→処理→返す

譜面のファイルパスを動的に受け取りたかったので、引数にファイルパスを指定できるようにした。また、読み込む譜面の難易度も指定できるようにした。戻り値は、Pythonのリストで、[int<ノーツが流れてくるレーン>, float<ノーツが流れてくる時間>]が一つのノーツデータとしてリストにノーツ数ぶん入っている。つまり、[[int, float], [int, float], [int, float], ...]のようになっている。尚、一時的に計算の精度を高めるためにfloatになっているため、C++に読み込まれるときにはlong型にキャストされる。
このファイルはmain.cppと同じ階層に置く。ビルド後は<プロジェクト名>.exeと同じ階層に置く。

notegetter.py
import re

def getNoteData(filepath, difficulty):
    notes = []
    #ファイルを読み込む処理
    return notes

C++でPython関数を呼び出し

流れとしては、
1. 自作モジュールをインポート
2. 指定した関数を関数オブジェクトとして取得
3. 関数オブジェクトを呼び出し、戻り値をゲット
4. 戻り値がリストであることを確認してリストのサイズを調べる
5. 調べたサイズ分for文で中身をC++のlong型にキャストしながら取得。
6. 使わなくなったpRet変数の参照を減らす
どうやらPython型→C++型にできるのはfloat型とlong型だけらしいので、しょうがなくそれを使っている。
あと、引数はタプルでひとまとめにしてから渡すらしい。尚、引数が必要無い場合はそこをNULLにすればいい。
今回は簡単のため、ヘッダファイルにべた書きである。

getnotedata.hpp
#include <Python.h>
#include <vector>
#include <string>

std::vector<std::pair<int, long>> getNoteData(const std::string &filepath, const std::string &difficulty) {
    std::vector<std::pair<int, long>> notedata;
    PyObject *pName, *pModule, *pFunc, *pRet, *pArgs;
    pName = PyUnicode_DecodeFSDefault("notegetter");
    pModule = PyImport_Import(pName);

    if (pModule) {
        pFunc = PyObject_GetAttrString(pModule, "getNoteData");
        if (PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(2);
            PyTuple_SetItem(pArgs, 0, PyUnicode_FromString(filepath.c_str()));
            PyTuple_SetItem(pArgs, 1, PyUnicode_FromString(difficulty.c_str()));
            pRet = PyObject_CallObject(pFunc, pArgs);
            if (pRet && PyList_Check(pRet)) {
                Py_ssize_t len = PyList_Size(pRet);
                notedata.resize(len);
                for (Py_ssize_t i = 0; i < len; i++) {
                    PyObject *c = PyList_GetItem(pRet, i);
                    notedata[i].first = PyLong_AsLong(PyList_GetItem(c, 0));
                    notedata[i].second = PyLong_AsLong(PyList_GetItem(c, 1));
                }
            }
            Py_XDECREF(pRet);
        }
    }
    return notedata;
}
main.cpp
#include <Python.h>
#include <vector>
#include "getnotedata.hpp"

int main(int argc, char *argv[]) {
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    Py_SetProgramName(program);
    Py_Initialize();
    PyObject *sys = PyImport_ImportModule("sys");
    PyObject *path = PyObject_GetAttrString(sys, "path");
    PyList_Append(sys, PyUnicode_FromString("./"));
    //ここにコードを書いていく
    std::vector<std::pair<int, long>> notedata = getNoteData("./sampleMusic.txt", "normal");

    return 0;
}

これで一通りの処理は完成である。尚、ビルドした後のフォルダにも当然Pythonを導入しなくては動かないので、ビルド後は<プロジェクト名>.exeがあるフォルダで「Embeddable Pythonの導入」でやった操作をすることを忘れてはならない。あと、PythonではUTF-8が使われているため、注意が必要だ。

おわりに

C++とPythonの連携に関する情報は、Python公式のPython/C API リファレンスマニュアルを素直に読むのが良いだろう。また、参照カウントについてはよく調べることを勧める。それでは良いC++/Pythonライフを!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした