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
でした。
自分の環境に合わせて適宜インストールしてください。
$ apt install wget build-essential cmake python3-dev python3-pip
また以下のコマンドで Python のインストール位置とバージョンを確認しておきます。
$ which python3
/usr/bin/python3
$ python3 --version
Python 3.8.2
boost::python::numpyを使う場合はnumpyをインストールする必要があります。(私はこれを忘れていて苦労しました。)
$ pip3 install numpy
次にboostをダウンロード、インストールします。次の公式ドキュメントの5章を参考にインストールします。
Unix環境でのboostの始め方(英語)
boostのダウンロードページ
boostをダウンロードし、解凍し、解凍したフォルダの中に入ります。
$ wget (boostのダウンロードURL)
$ tar --bzip2 -xf boost_1_73_0.tar.bz2
$ cd boost_1_73_0
解凍したフォルダの中にある./bootstrap.sh
でインストール準備して、./b2
でビルドとインストールをします。
$ ./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のインクルードディレクトリとライブラリのパスがわかります。
$ 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
...
$ 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次元のサイズを返す関数を持つサンプルモジュールを作ってみます。
#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++
でコンパイルするには、以下のコマンドを実行します。
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を使います。
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の使い方については
などが詳しいです。これを見ればコンパイルの仕組みから、上で使っているfind_package
の仕組みまで一通りわかります。感謝!)
そしてこのsample.cpp
、CMakeLists.txt
を以下のようなディレクトリ構成で配置します。
.
|-- CMakeLists.txt
|-- build
`-- sample.cpp
build内でビルドします。
$ cd build
$ cmake ..
$ cmake --build .
そうするbuild内にsample.so
というファイル(共有ライブラリ)ができています。
そしてbuild内で以下のようなpythonスクリプトを書いて実行すると、この共有ライブラリをimport
してくれて、うまく動くことがわかります。
import sample
import numpy as np
a = np.array([1,2,3,4], dtype=np.float32)
print(a)
size=sample.size(a)
print(size)
$ python3 test.py
[1. 2. 3. 4.]
4
VSCodeでデバッグする
pythonのモジュールとして呼び出している限り、デバックしづらくて使いにくいですが、
実行可能ファイルとしてビルドできれば、VSCodeのCMake-Toolsなどを使えばデバッグが捗ります。
Visual Studio CodeのRemote ContainersでC++開発環境構築
を参考に、VSCodeのリモートデバッグ環境を整えます。
(上記記事中の環境のリポジトリを私が改良したリポジトリもあります。)
まずその環境上で次のようなディレクトリ構成を作ります。
.
|-- CMakeLists.txt
|-- build
`-- main.cpp
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()
を呼び出せば、実行可能ファイルとしてビルドできるようになります。
#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++が扱え、デバッグが捗ります。便利!
補足:過去の記事や参考サイト
Qiitaの過去記事や、外部サイトにBoost::pythonについての記事はありますが、
なかなか私が詰まった点の解説や、この記事で言われているようないわゆるモダンなcmakeの使い方をしている記事がなかったのでこの記事を作りました。
C++でPythonを拡張するためのBoost.NumPyチュートリアル(実践編)
この記事ではboost-numpyを使っていますが、boost-numpyはboost::pythonに統合され、今はdeprecatedとなってしまっています。
C++ で Python 用ライブラリーを作成する
PythonとC++間でのnumpy配列の受け渡し
これらの記事も大きく参考になりました。