はじめに
いつもははてなブログで投稿している@kumitatepazuruです。qiitaのアドベントカレンダーが空いていたので投稿してみました。
元記事:https://kumitatepazuru.hatenablog.com/entry/20201207/1607294720
環境
OS: ubuntu 20.10
CPU: Intel(R) Core(TM) i7-10510U CPU
GPU: CPU内蔵
python: 3.8.6
tensorflow: 2.3.1
keras: tensorflow内蔵
どーでもいい情報入っているかもしれんが定型文だからしょうがない()
経緯
この頃、C++をいじってるのだけれども、自分はC++はほぼかけないし(読めるけど)書き方がちょっとあれ(https://clown.cube-soft.jp/entry/20090708/p1 )だったし、何よりもkeras(tensorflow)使いたいしということで、C++でpythonを動かすことにした。
C++使わずにpythonを使えばいいじゃん。と考える人がいそうなので言っておくと、ベースのC++があって、それを改造している状況なのだ。そのプログラムが結構大きいので自力で変更するのは無限に時間がかかりそうだからC++でpythonを使ってやろうと思っているのだ。
やり方を調べてみる。
まず、自分が知っているのは、boostを使用してpythonを実行する方法。boostは今回使うプログラムでも使ってるのでいいかも。
でも、boostで動かす方法はあまり情報がなく断念。
次は、Cython(https://qiita.com/pashango2/items/45cb85390193d97523ca )を使用してやる方法だ。しかし、実際にやってみると、pythonが実行できるわけでわなくC/C++しか実行できず、pythonを書くと、セグメンテーション違反で止まることが発覚したのでこれもできず。
最後は、python標準でついているC APIを使用して、実行することができるし、情報もwikiが日本語でしっかりしてるのでこれがいいかも。
早速やってみる。
テストプログラム
#include <Python.h>
int main() {
Py_Initialize();
PyRun_SimpleString("print('test')"); //ここにpythonプログラムを書く。
Py_Finalize();
return 0;
}
Py_Initialize
でpythonを初期化、Py_Finalize
でリセットしてメモリを開放する。そして、その間にpythonに関するプログラムを記入する。
詳しくは
そして、PyRun_SimpleString
関数でpythonプログラムを実行できる。改行は\n
で可能。また、今回はやらないが、ファイルから読み込みなどいろいろな方法がある。詳しくは
ビルド
ここではコンソールでやる場合を説明する。
まず、Python.h
を読み込ませなくてはいけない。でも、環境によって場所が違うのでpkg-configを使用する。Autotoolsやautomakeにはべんりな関数があるので使用するうとよい。
https://qiita.com/tomoyuki-nakabayashi/items/cab28ff7ccd717ae89cd#dive-into-%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB
pythonが入っている場合は以下のコマンドで引数が返ってくる。
pkg-config python3 --cflags
# -I/usr/include/python3.8 -I/usr/include/x86_64-linux-gnu/python3.8
なので、以下のコマンドで読み込みができる。
gcc main.cpp `pkg-config python3 --cflags`
しかし、このままではライブラリがなくてエラーが吐かれてしまうので、ライブラリを追加する。(インストールは不要)
gcc main.cpp `pkg-config python3 --cflags` -lpython3.8
-python3.8
はインストールされているpythonのバージョンに変更してください。
これで、ビルドができて、a.out
というファイルができる。
実行すると、
$ ./a.out
test
このように実行ができた。
おまけ
別の関数でpythonを使用したい場合
別の関数でpythonを使用したい場合は以下のように、Py_Initialize
とPy_Finalize
でちゃんと囲まれていたらたとえファイルが違えども大丈夫。
// ------- main.cpp -------
#include <Python.h>
#include "test.h"
void hogehoge() {
PyRun_SimpleString("print('このプログラムはhogehoge関数で実行しています。')");
}
int main() {
Py_Initialize();
PyRun_SimpleString("print('このプログラムはmain関数で実行しています。')");
hogehoge();
test_func();
Py_Finalize();
return 0;
}
// ------- test.cpp -------
#include <Python.h>
void test_func() {
PyRun_SimpleString("print('このプログラムはtestファイル内のtest_func関数で実行しています。')");
}
// ------- test.h -------
void test_func();
$ gcc main.cpp test.cpp `pkg-config python3 --cflags` -lpython3.8
$ ./a.out
このプログラムはmain関数で実行しています。
このプログラムはhogehoge関数で実行しています。
このプログラムはtestファイル内のtest_func関数で実行しています。
importしたpythonライブラリの引き継ぎについて
これに関しても、ちゃんと囲まれてたら1度呼び出したら、どこでも使える。
テストコード
// ------- main.cpp -------
#include <Python.h>
#include "test.h"
void hogehoge() {
PyRun_SimpleString("time.sleep(1)\n"
"print('このプログラムはhogehoge関数で実行しています。',datetime.datetime.now())");
}
int main() {
Py_Initialize();
PyRun_SimpleString("import time, datetime");
PyRun_SimpleString("time.sleep(1)\n"
"print('このプログラムはmain関数で実行しています。',datetime.datetime.now())");
hogehoge();
test_func();
Py_Finalize();
return 0;
}
// ------- test.cpp -------
#include <Python.h>
void test_func() {
PyRun_SimpleString("time.sleep(1)\n"
"print('このプログラムはtestファイル内のtest_func関数で実行しています。',datetime.datetime.now())"
);
}
// ------- test.h -------
void test_func();
$ gcc main.cpp test.cpp `pkg-config python3 --cflags` -lpython3.8
$ ./a.out
このプログラムはmain関数で実行しています。 2020-12-06 16:19:45.493962
このプログラムはhogehoge関数で実行しています。 2020-12-06 16:19:46.495139
このプログラムはtestファイル内のtest_func関数で実行しています。 2020-12-06 16:19:47.496891
ちゃんと、メッセージあとの時間が違う。
エラーについて
ちゃんとエラーは出してくれる。PyRun_SimpleString
の場合だと、ファイル名は<string>
になる。
プログラムは停止せず実行される。
先程のmain関数を変更。
int main() {
Py_Initialize();
PyRun_SimpleString("import time, datetime");
PyRun_SimpleString("time.sleep(1)\n"
"print('このプログラムはmain関数で実行しています。',datetime.datetime.now())\n"
"raise ValueError");
hogehoge();
test_func();
Py_Finalize();
return 0;
}
$ gcc main.cpp test.cpp `pkg-config python3 --cflags` -lpython3.8
$ ./a.out
このプログラムはmain関数で実行しています。 2020-12-06 17:35:03.561291
Traceback (most recent call last):
File "<string>", line 3, in <module>
ValueError
このプログラムはhogehoge関数で実行しています。 2020-12-06 17:35:04.778844
このプログラムはtestファイル内のtest_func関数で実行しています。 2020-12-06 17:35:05.780182
別の環境でも実行したい場合
最小環境でも動かせるようにやってみる。今回はdockerを使用した。前提として、gccやmake等のビルドに必要なライブラリが入っていることを前提とする。
やり方としては、libフォルダにpythonを全部入れてその状態でパスを通してあげてビルドをする。
zlibのライブラリを持ってくる。
pythonのコンパイル・インストールにはzlib
が必要なのでzlib
をコンパイル・インストールする。
wget https://zlib.net/zlib-1.2.11.tar.gz
tar xzvf zlib-1.2.11.tar.gz
cd zlib-1.2.11/
./configure --prefix ~/lib/zlib/
make
make install
configure
で指定している--prefix ~/lib/
でインストール場所を指定している。環境によってパスは変更してください。zlib
ディレクトリ内のshare
ディレクトリはmanページファイルしか入っていないので削除しても問題はない。
pythonを入れる。
[https://www.python.org/downloads/source/:title]
ここからソースコードのリンクをコピーする。
自分は、3.8.6のgzip形式を使用する。
wget https://www.python.org/ftp/python/3.8.4/Python-3.8.4.tgz
tar -xzvf Python-3.8.4.tgz
cd Python-3.8.4/
export CPPFLAGS='-I/root/lib/zlib/include/'
export LDFLAGS='-L/root/lib/zlib/lib/'
./configure --prefix ~/lib/python3 --enable-shared
make
make install
注意
今回はpythonがコンパイルできればいいのでpython環境も最小構成だ。なので、make
が終わってから以下のwarningが出る。
The necessary bits to build these optional modules were not found:
_bz2 _curses _curses_panel
_dbm _gdbm _hashlib
_lzma _sqlite3 _ssl
_tkinter _uuid readline
To find the necessary bits, look in setup.py in detect_modules() for the module's name.
The following modules found by detect_modules() in setup.py, have been
built by the Makefile instead, as configured by the Setup files:
_abc atexit pwd
time
Failed to build these modules:
_ctypes
書いてあるとおり、いろいろ必要なライブラリがないらしい。今回は使わないからスルー。全部入れたいときは、いろいろなサイトに書かれてあるので各自確認してください。
注意終わり
こちらも、configure
で指定している--prefix ~/lib/
でインストール場所を指定している。環境によってパスは変更してください。同じようにpython3
ディレクトリ内のshare
ディレクトリはmanページファイルしか入っていないので削除しても問題はない。--enable-shared
は、コンパイル後のディレクトリ内のlib
ディレクトリ内に共有ライブラリを生成する引数。
変数のCPPFLAGS
とLDFLAGS
はそれぞれzlib
のinclude
とlib
の絶対パスを代入する。
ビルド
pkg-config python3 --cflags
は./lib/python3/include/python3.8
にあたる(環境によって違う)のでこのように改造。
gcc main.cpp test.cpp -I./lib/python3/include/python3.8 -lpython3.8
しかし、このままでは共有ライブラリの場所がわからなくなるので、パスを-L
で追加する。
ライブラリの場所は./lib/python3/lib/
にある。
gcc main.cpp test.cpp -I./python3/include/python3.8 -L./lib/python3/lib/ -lpython3.8
最終的にはこうなる。
実行
実行すると、以下のエラーが発生する。
./a.out: error while loading shared libraries: libpython3.8.so.1.0: cannot open shared object file: No such file or directory
なので、ライブラリのパスを環境変数で与えてやる。(同じ環境変数を使用している場合は、なんかいい感じに変えてください)
LD_LIBRARY_PATH=./lib/python3/lib/
で渡している。
# LD_LIBRARY_PATH=./lib/python3/lib/ ./a.out
このプログラムはmain関数で実行しています。 2020-12-06 11:21:24.547966
Traceback (most recent call last):
File "<string>", line 3, in <module>
ValueError
このプログラムはhogehoge関数で実行しています。 2020-12-06 11:21:25.549383
このプログラムはtestファイル内のtest_func関数で実行しています。 2020-12-06 11:21:26.550680
ちゃんと実行できた。
最後に
実行してるときの時刻見てもらえるとわかるけど、めっちゃ苦戦してる。これを実現するために、1週間くらいかかった。誰かの役に立ってくれれば泣いて喜びますので宜しくおねがいします!!!!