boost::python::numpyを使う、VSCodeでデバッグする

c++でPythonを拡張したり、Numpyのndarrayをc++のvectorに変換したいというニーズがあると思います。
そういったときにBoost::pythonやboost::python::numpyは便利です。
(最近はpybind11というのも流行ってるみたいです。早く知りたかった...)

また、実はboost pythonは実行可能ファイルとしてビルドできるので、例えばVSCodeなどでデバッグできます。

必要なパッケージなどから1から解説していきます。
# 環境
|||備考|
|:----:|:------:|:------:|
|OS|ubuntu 20.04|docker 上で起動|
|python|3.8.2||
|boost|1.73.0||
|cmake | 3.16.3||


# 準備
まず必要パッケージのインストールをします。

今回の記事で必要となるパッケージは`wget`、`build-essential`、`cmake`、`python3-dev`、`python3-pip`でした。
自分の環境に合わせて適宜インストールしてください。

```bash
$ apt install wget build-essential cmake python3-dev python3-pip 
```
また以下のコマンドで Python のインストール位置とバージョンを確認しておきます。

```bash
$ which python3
/usr/bin/python3
$ python3 --version
Python 3.8.2
```

boost::python::numpyを使う場合はnumpyをインストールする必要があります。(私はこれを忘れていて苦労しました。)

```bash
$ pip3 install numpy
```

次にboostをダウンロード、インストールします。次の公式ドキュメントの5章を参考にインストールします。

[Unix環境でのboostの始め方(英語)](https://www.boost.org/doc/libs/1_74_0/more/getting_started/unix-variants.html)
[boostのダウンロードページ](https://www.boost.org/users/history/version_1_73_0.html)

boostをダウンロードし、解凍し、解凍したフォルダの中に入ります。

```bash
$ wget (boostのダウンロードURL)
$ tar --bzip2 -xf boost_1_73_0.tar.bz2
$ cd boost_1_73_0
```
解凍したフォルダの中にある`./bootstrap.sh`でインストール準備して、`./b2`でビルドとインストールをします。

```bash 
$ ./bootstrap.sh --with-libraries=python --with-python=python3 --with-python-version=3.8
$ ./b2 install -j8
```
`--with-libraries=python`と指定することで、`boost::python`だけがビルド、インストールされ、時間が節約できます。
また`--with-python-version`で先程確認したpythonのバージョンを指定します。

(Anacondaやpyenvなど別環境の場合は追加で別の設定が必要だと思われます。当記事はコンテナ中で構築してるのでいくらでも環境を汚してもいいかなというスタンスでやっています。)

デフォルトでは`/usr/`以下にboostがインストールされました。
`find`コマンドを使うことでboostのインクルードディレクトリとライブラリのパスがわかります。


```bash
$ find /usr/ -name "*boost*" | grep include
/usr/include/c++/9/bits/boost_concept_check.h
/usr/local/include/boost
/usr/local/include/boost/chrono/typeof/boost
...
```

```bash
$ find /usr/ -name "*boost*" | grep lib
...
/usr/local/lib/cmake/boost_numpy-1.73.0/libboost_numpy-variant-static-py3.8.cmake
/usr/local/lib/cmake/boost_numpy-1.73.0/boost_numpy-config-version.cmake
/usr/local/lib/cmake/boost_numpy-1.73.0/libboost_numpy-variant-shared-py3.8.cmake
/usr/local/lib/libboost_numpy38.a
/usr/local/lib/libboost_python38.so.1.73
/usr/local/lib/libboost_numpy38.so.1.73
...
```

# 実際にndarrayを使うモジュールを作ってみる

実際にndarrayの第1次元のサイズを返す関数を持つサンプルモジュールを作ってみます。

```sample.cpp
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
//placeholdersの衝突を回避するため(要調査)
#include <boost/python/numpy.hpp>
namespace py = boost::python;
namespace np = boost::python::numpy;

auto size(np::ndarray &a)
{
  auto N = a.shape(0);
  return N;
}

BOOST_PYTHON_MODULE(sample)
{
  Py_Initialize();
  np::initialize();
  py::def("size", size);
}
```

これを`g++`でコンパイルするには、以下のコマンドを実行します。

```bash
g++ --shared -fPIC \
	-I$BOOST_INCLUDE_DIR -I/usr/include/python3.8/ \
	sample.cpp -Xlinker -rpath -Xlinker $BOOST_LIB_DIR \
	-lboost_python38 -lboost_numpy38 -o sample.so
```
`$BOOST_INCLUDE_DIR`はboostのインクルードパス、`$BOOST_LIB_DIR`はboostのライブラリパスで、環境によって違うと思います。私の環境の場合、さきほど`find`コマンドを使って確かめたようにそれぞれ`/usr/local/include/boost/`、`/usr/local/lib/`でした。

環境によってコマンドを変えなければならないし、毎回このコマンドを打つのも大変ですのでCMakeを使います。

```CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(boost-python-test)

find_package(Python3 COMPONENTS Development)
find_package(Boost COMPONENTS python38 numpy38 REQUIRED)
# ここでpython38となっているのはboostインストール時に--with-python-version=3.8としたからだと思われます。
# ここも環境によって変えなくてはならないかもしれません

# デバッグ用：解決したパスを表示
message("## Boost_LIBRARIES:  ${Boost_LIBRARIES}")  
message("## Boost_INCLUDE_DIRS: ${Boost_INCLUDE_DIRS}")
message("## Python3_INCLUDE_DIRS: ${Python3_INCLUDE_DIRS}") 
message("## Python3_LIBRARIES :${Python3_LIBRARIES}")

add_library(sample SHARED sample.cpp)
target_include_directories(sample PRIVATE ${Python3_INCLUDE_DIRS})
target_link_libraries(sample PRIVATE ${Boost_LIBRARIES} ${Python3_LIBRARIES})
# target_link_libraries(sample PRIVATE ${Boost_LIBRARIES})だけでも動く。理由不明。
set_target_properties(sample PROPERTIES PREFIX "") # 接頭辞'lib'を省略するため
```
(g++の使い方や、cmakeの使い方については

- @shohiroseさんの[CMakeの使い方（その１）](https://qiita.com/shohirose/items/45fb49c6b429e8b204ac)
- [勝手に作るCMake入門 その1 基本的な使い方 | かみのメモ](https://kamino.hatenablog.com/entry/cmake_tutorial1)

などが詳しいです。これを見ればコンパイルの仕組みから、上で使っている`find_package`の仕組みまで一通りわかります。感謝！)

そしてこの`sample.cpp`、`CMakeLists.txt`を以下のようなディレクトリ構成で配置します。

```
.
|-- CMakeLists.txt
|-- build
`-- sample.cpp
```
build内でビルドします。

```
$ cd build
$ cmake ..
$ cmake --build .
```
そうするbuild内に`sample.so`というファイル(共有ライブラリ)ができています。
そしてbuild内で以下のようなpythonスクリプトを書いて実行すると、この共有ライブラリを`import`してくれて、うまく動くことがわかります。

```test.py
import sample
import numpy as np

a = np.array([1,2,3,4], dtype=np.float32)
print(a)
size=sample.size(a)
print(size)
```

```bash
$ python3 test.py 
[1. 2. 3. 4.]
4
```

# VSCodeでデバッグする

pythonのモジュールとして呼び出している限り、デバックしづらくて使いにくいですが、
実行可能ファイルとしてビルドできれば、VSCodeのCMake-Toolsなどを使えばデバッグが捗ります。

[Visual Studio CodeのRemote ContainersでC++開発環境構築](https://qiita.com/dbgso/items/6e2f317d6d13bdc387f8)
を参考に、VSCodeのリモートデバッグ環境を整えます。
(上記記事中の環境のリポジトリを私が改良した[リポジトリ](https://github.com/SolKul/vscode-cpp-devcontainer)もあります。)

まずその環境上で次のようなディレクトリ構成を作ります。

```
.
|-- CMakeLists.txt
|-- build
`-- main.cpp
```


```CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(boost-python-test)

find_package(Python3 COMPONENTS Development)
find_package(Boost COMPONENTS python38 numpy38 REQUIRED)

add_executable(main_app main.cpp)
target_include_directories(main_app PRIVATE ${Python3_INCLUDE_DIRS})
target_link_libraries(main_app PRIVATE ${Boost_LIBRARIES} ${Python3_LIBRARIES})
```

`main.cpp`で以下のように、main関数の先頭で`Py_Initialize()`と`np::initialize()`を呼び出せば、実行可能ファイルとしてビルドできるようになります。

```main.cpp
#include <iostream>
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
//placeholdersの衝突を回避するため(要調査)
#include <boost/python/numpy.hpp>
namespace py = boost::python;
namespace np = boost::python::numpy;

int main()
{
  Py_Initialize();
  np::initialize();
  double v[] = {1.3 , 2.4 , -5.3};
  int v_size = 3;
  py::tuple shape = py::make_tuple(v_size);
  py::tuple stride = py::make_tuple(sizeof(double));
  np::dtype dt = np::dtype::get_builtin<double>();
  np::ndarray output = np::from_data(&v[0], dt, shape, stride, py::object());
  np::ndarray output_array = output.copy();
  
  std::cout << "c++ array address::" << std::endl
          << std::hex << &v[0] << std::endl;
  
  auto *p = reinterpret_cast<double*>(output.get_data());
  std::cout << "ndarray address ::" << std::endl
          << std::hex << p << std::endl;
  return 0;
}
```

この状態で、VSCodeのCMake-Toolsのデバッグ機能を使えば、ブレークポイントを設定できたり、変数を覗けたりします。下の画像のようにVSCodeのDEBUG CONSOLEを使えば、まるでインタプリタ言語のようにC++が扱え、デバッグが捗ります。便利！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466677/ec4b7be2-2307-b640-a5ee-7fa6f037beee.png)


# 補足：過去の記事や参考サイト

Qiitaの過去記事や、外部サイトにBoost::pythonについての記事はありますが、
なかなか私が詰まった点の解説や、[この記事](https://qiita.com/shohirose/items/5b406f060cd5557814e9)で言われているようないわゆるモダンなcmakeの使い方をしている記事がなかったのでこの記事を作りました。

[C++でPythonを拡張するためのBoost.NumPyチュートリアル(実践編)](https://qiita.com/termoshtt/items/81eeb0467d9087958f7f)
この記事ではboost-numpyを使っていますが、boost-numpyはboost::pythonに統合され、今はdeprecatedとなってしまっています。

[C++ で Python 用ライブラリーを作成する](https://www.xsim.info/articles/BoostPython/How-to-make-Python-module-with-BoostPython.html)
[PythonとC++間でのnumpy配列の受け渡し](http://bluewidz.blogspot.com/2018/05/pythonnumpy.html)
これらの記事も大きく参考になりました。