kumitatepazuru's blog

中学生のメモブログ。みんなの役に立ちたい。

C/C++で何もライブラリを使用せずにpythonを動かしてみる。

f:id:kumitatepazuru:20201206122322p:plain

久しぶりの更新。今回は結構特大記事。

環境

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++はほぼかけないし(読めるけど)書き方がちょっとあれ*1だったし、何よりもkeras(tensorflow)使いたいしということで、C++pythonを動かすことにした。

C++使わずにpythonを使えばいいじゃん。と考える人がいそうなので言っておくと、ベースのC++があって、それを改造している状況なのだ。そのプログラムが結構大きいので自力で変更するのは無限に時間がかかりそうだからC++pythonを使ってやろうと思っているのだ。

やり方を調べてみる。

まず、自分が知っているのは、boostを使用してpythonを実行する方法。boostは今回使うプログラムでも使ってるのでいいかも。

でも、boostで動かす方法はあまり情報がなく断念。

次は、Cython*2を使用してやる方法だ。しかし、実際にやってみると、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_Initializepythonを初期化、Py_Finalizeでリセットしてメモリを開放する。そして、その間にpythonに関するプログラムを記入する。 詳しくは

docs.python.org

そして、PyRun_SimpleString関数でpythonプログラムを実行できる。改行は\nで可能。また、今回はやらないが、ファイルから読み込みなどいろいろな方法がある。詳しくは

docs.python.org

ビルド

ここではコンソールでやる場合を説明する。

まず、Python.hを読み込ませなくてはいけない。でも、環境によって場所が違うのでpkg-configを使用する。Autotoolsやautomakeにはべんりな関数があるので使用するうとよい。 qiita.com

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_InitializePy_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を入れる。

Python Source Releases | Python.org

ここからソースコードのリンクをコピーする。 自分は、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が出る。

_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ディレクトリ内に共有ライブラリを生成する引数。

変数のCPPFLAGSLDFLAGSはそれぞれzlibincludelib絶対パスを代入する。

ビルド

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週間くらいかかった。誰かの役に立ってくれれば泣いて喜びますので宜しくおねがいします!!!!


個人的な質問等はこちらまで。

https://forms.gle/V6NRhoTooFw15hJdA

また、自分が参加しているRobocup soccer シミュレーションリーグのチームでは参加者募集中です!活動の見学、活動に参加したい方、ご連絡お待ちしております!

詳しくはこちら

kumitatepazuru.github.io

*1:この記事参照

clown.cube-soft.jp

*2:この記事参照qiita.com