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

## はじめに
いつもははてなブログで投稿している@kumitatepazuruです。qiitaのアドベントカレンダーが空いていたので投稿してみました。

元記事：https://kumitatepazuru.hatenablog.com/entry/20201207/1607294720

## 環境

OS: ubuntu 20.10<br>
CPU: Intel(R) Core(TM) i7-10510U CPU<br>
GPU: CPU内蔵<br>
python: 3.8.6<br>
tensorflow: 2.3.1<br>
keras: tensorflow内蔵<br>

どーでもいい情報入っているかもしれんが定型文だからしょうがない()

## 経緯
この頃、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が日本語でしっかりしてるのでこれがいいかも。

## 早速やってみる。

### テストプログラム


```cpp
#include <Python.h>

int main() {
    Py_Initialize();
    PyRun_SimpleString("print('test')"); //ここにpythonプログラムを書く。
    Py_Finalize();
    return 0;
}

```
`Py_Initialize`でpythonを初期化、`Py_Finalize`でリセットしてメモリを開放する。そして、その間にpythonに関するプログラムを記入する。
詳しくは

https://docs.python.org/ja/3.8/c-api/init.html


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

https://docs.python.org/ja/3/c-api/veryhigh.html

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

まず、`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が入っている場合は以下のコマンドで引数が返ってくる。

```sh
pkg-config python3 --cflags

# -I/usr/include/python3.8 -I/usr/include/x86_64-linux-gnu/python3.8
```
なので、以下のコマンドで読み込みができる。

```sh
gcc main.cpp `pkg-config python3 --cflags`
```
しかし、このままではライブラリがなくてエラーが吐かれてしまうので、ライブラリを追加する。（インストールは不要）

```sh
gcc main.cpp `pkg-config python3 --cflags` -lpython3.8
```
`-python3.8`はインストールされているpythonのバージョンに変更してください。

これで、ビルドができて、`a.out`というファイルができる。
実行すると、

```sh
$ ./a.out
test
```
このように実行ができた。

## おまけ
### 別の関数でpythonを使用したい場合
別の関数でpythonを使用したい場合は以下のように、`Py_Initialize`と`Py_Finalize`でちゃんと囲まれていたらたとえファイルが違えども大丈夫。

```cpp
// ------- 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();
```

```sh
$ gcc main.cpp test.cpp `pkg-config python3 --cflags` -lpython3.8
$ ./a.out
このプログラムはmain関数で実行しています。
このプログラムはhogehoge関数で実行しています。
このプログラムはtestファイル内のtest_func関数で実行しています。
```

#### importしたpythonライブラリの引き継ぎについて
これに関しても、ちゃんと囲まれてたら1度呼び出したら、どこでも使える。

テストコード

```cpp
// ------- 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();
```

```sh
$ 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関数を変更。

```cpp
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;
}
```


```sh
$ 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`をコンパイル・インストールする。

```sh
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形式を使用する。

```sh
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`にあたる（環境によって違う）のでこのように改造。


```sh
gcc main.cpp test.cpp -I./lib/python3/include/python3.8 -lpython3.8
```
しかし、このままでは共有ライブラリの場所がわからなくなるので、パスを`-L`で追加する。
ライブラリの場所は`./lib/python3/lib/`にある。

```sh
gcc main.cpp test.cpp -I./python3/include/python3.8 -L./lib/python3/lib/ -lpython3.8
```
最終的にはこうなる。

#### 実行
実行すると、以下のエラーが発生する。

```sh
./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/`で渡している。

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