Boost.PythonおよびBoost.NumpyをWindowsで使うまで

こんにちはtatsyです。

C++とPythonを糊付けするものとしてBoost.PythonとBoost.Numpyというものがあるのですが、多くの記事はPythonからC++のコードを呼び出すことを主眼としていて、僕のようにC++からNumpyの線形ソルバーを呼びたいとか言う人は少ないみたいです。

Boost.PythonやBoost.Numpyを使う利点、特にWindows上で使う利点はPythonをビルドしているVisual Studioのバージョンに依存しないということだと思います。

通常PythonのC APIは使っているPythonをビルドしたものと同じVisual Studioのバージョン(3.4ならVC2010, 3.5ならVC2015)を使ってビルドしないといけないので、いちいち複数のバージョンのVisual Studioを入れないといけなかったりします。

参考ページ: Windows での Python 2.7/3.4 の拡張モジュールビルド環境

というわけで、今回は、Boost.Numpyを使ってC++からサクサクと連立一次方程式を解くまでをインストールから順に解説していきたいと思います。

今回、実験した環境は

  • Windows 7 64 bit
  • Visual Studio 2015
  • Python 3.4
  • Boost 1.59.0

です。

Python, Numpyのインストール


Pythonは本家のサイトからお好きなバージョンを落としてください。今回の記事では私がPython3系を使っていることもあって、3系のための解説になってしまいますのでご了承ください。

次にNumpyですが、pipとかだと環境によってビルドが通らなかったりするので、おとなしく非公式アーカイブからNumpyをいただいてきます。

こちらで配布されている**.whl**ファイルを

$ pip install xxxx.whl

のように呼んであげると、インストールできます。

Anacondaを使う場合

AnacondaあるいはMinicondaの環境でBoost Numpyを使いたい場合には少し工夫が必要です。ここでは新しくBoostという仮想環境を作る前提で話を進めます。

まず、仮想環境を作り、そこにPython(ここでは3.5)とNumpyをインストールします。

conda create -n Boost python=3.5 numpy

そうすると(Anacondaのディレクトリ)/envs/Boostというディレクトリがこの仮想環境でのPythonのホーム・ディレクトリになります。

通常のPythonのインストールの際には必要ないのですが、仮想環境を使う場合には、Pythonのホーム・ディレクトリとライブラリのディレクトリを環境変数に追加してあげる必要があります。

変数名: PYTHONHOME
値: (Anacondaのディレクトリ)/envs/Boost

変数名: PYTHONPATH
値: (Anacondaのディレクトリ)/envs/Boost/DLLs;(Anacondaのディレクトリ)/envs/Boost/Lib;(Anacondaのディレクトリ)/envs/Boost/Lib/site-packages

これで以下は通常のPythonと同じ設定でBoost.Pythonが使えるようになります。

Boostのインストール


まずはBoostのサイトにアクセスをしてBoostの最新バージョン(2015年11月6日現在)である1.59.0をダウンロードしてきます。

そうしたら、Lhaplusか何かでzipやtar.gz等を解凍します。するとboost_1_59_0というフォルダができるので、これを適当な場所に置きます。

僕は「C:/Libraries/boost_1_59_0」に配置していますので、以下はその前提でいきます。

そうしたら、Boostフォルダのルート・ディレクトリにあるbootstrap.batをコマンド・プロンプトから実行します。

数分くらい時間がかかると思いますが、そうすると同じくルート・ディレクトリにbjam.exeb2.exeという実行ファイルが出来ると思います。

僕の環境で実行した結果はこんな感じになりました。

boost_folder

Boost.PythonおよびBoost.Numpyのビルド


先ほど作成したb2.exeを使うとBoostに初期状態から同梱されているBoost.Pythonはビルド可能です。

Boost.Numpyは同梱されていないのでGitレポジトリから落としてきて、一緒にビルドできるように設定します。まずはBoost.NumpyのレポジトリをCloneしてきます。

$ git clone https://github.com/ndarray/Boost.Numpy.git

Cloneしてきたフォルダの中にはいくつかのファイルとフォルダが含まれていますが、この中で重要なのはboostlibsというフォルダです。この2つのフォルダをコピーして、Boostのルート・ディレクトリでペーストします。

次に、b2でビルドするためにいくつかの設定を行います。まず、(Boostのルート)/libs/numpyにあるsrcというフォルダをbuildという名前に変更します。これはBoostに同梱されているビルドスクリプトが**(Boostのルート)/libs/(パッケージ名)/build/Jamfile**というビルド・スクリプトを使ってビルドしているためです。

続いて、今名前を変更したbuildフォルダの中にあるJamfileをエディタで開いて、1点変更をします。

変更前

local full-cmd =
  $(python-interpreter)" -c \"from numpy.distutils import misc_util; print ':'.join(misc_util.get_numpy_include_dirs())\" " ;

変更後

local full-cmd =
  $(python-interpreter)" -c \"from numpy.distutils import misc_util; print(':'.join(misc_util.get_numpy_include_dirs()))\" " ;

ここでは、Boost.NumpyについているJamfileがPython2風に書かれているので、それをPython3風に変更しています。

ここまで出来たら、いよいよビルドです。コマンドは単純で、

$ b2 address-model=64 -j2 --prefix=. install

です。なお、僕の環境は64ビットでしたのでaddress-modelを64にしていますが、32ビットの場合には、この数字を32に変えればOKです。

なお、-j2は並列コンパイルに使うCPUコアの数を**–prefix**はBoostのルート・ディレクトリを指定するのに使います。今回はBoostのルート・ディレクトリで上記のコマンドを実行しているので、ドット1つになります。

コマンドを実行後10数分待つと、ビルドが完了するはずです。

注) 一応Boost.NumpyにはCMakeのファイルが付いているのですが、これを使ってビルドするとマルチスレッドとマルチスレッド・デバッグの間でライブラリの競合が起こってリンクできないという問題が発生したため、今回は他のライブラリと一緒にb2でビルドします。

Boost.Numpyを使ってC++から関数を呼び出す


ライブラリのインストールが終わったところで、次は自分のプログラムからBoost.Numpyを使ってNumpyの関数を呼び出します。

まず、ビルドをするためにVisual Studioのプロジェクトを作ります。プロジェクトが出来たら、プロジェクトのプロパティから「構成プロパティ」→「VC++ディレクトリ」を開き、以下の変更を施します。

インクルード ディレクトリに以下を追加。

  • C:/Python34/include
  • C:/Libraries/boost_1_59_0 (Boostのルートディレクトリ)

ライブラリ ディレクトリに以下を追加。

  • C:/Python34/libs
  • C:/Libraries/boost_1_59_0/lib

続いて、「リンカー」→「入力」を開いて、「追加の依存ファイル」に以下のライブラリを追加します。

  • Python34.lib
  • libboost_numpy-vc140-mt-1_59.lib (Releaseの場合)

これでインクルードとライブラリの設定は完了です。次にコードを書きます。今回、書いたコードは以下のような感じです。

#define BOOST_PYTHON_STATIC_LIB
#include <Python.h>
#include <boost/python.hpp>
#include <boost/numpy.hpp>

#include <iostream>

namespace py = boost::python;
namespace np = boost::numpy;

int main() {
    Py_Initialize();
    np::initialize();

    // Matrix A
    py::tuple shapeA = py::make_tuple(2, 2);
    np::ndarray A = np::zeros(shapeA, np::dtype::get_builtin<double>());
    A[0][0] = 1.0;
    A[0][1] = 2.0;
    A[1][0] = 3.0;
    A[1][1] = 4.0;

    // Vector b
    py::tuple shapeB = py::make_tuple(2);
    np::ndarray b = np::zeros(shapeB, np::dtype::get_builtin<double>());
    b[0] = 3.0;
    b[1] = 7.0;

    // Prepare numpy.linalg.solve
    py::object solve = py::import("numpy").attr("linalg").attr("solve");

    // Solve
    np::ndarray x = py::extract<np::ndarray>(solve(A, b));
    int dims = x.shape(0);
    for (int i = 0; i < dims; i++) {
        std::cout << py::extract<double>(x[i]) << std::endl;
    }
}

いくつかポイントを解説します。

1行目

BOOST_PYTHON_STATIC_LIBというマクロは、Boostのライブラリを静的にリンクすることを明示するものです。これをつけないとDLLに依存した実行ファイルが出来てしまうそうです。

12, 13行目

これはPythonおよびNumpyを使うための諸々の初期化をやってくれる関数です。呼ばないと実行時エラーになるので注意してください。

16行目

Boost.Numpyの現在の実装では、std::tupleなどをサイズとして渡せないので、boost::python::tuple型のサイズを示すtupleを作っています。

17行目

見ての通りですが、要素が全て0の2次元配列を作っています。

18-21行目

単に要素を入れているだけです。このようにかなり直感的な書き方ができるのは嬉しいです。

24-27行目

行列の作成と同じようにしてベクトルを作っています。

30行目

呼び出す関数を用意しています。

33行目

関数を呼び出して連立方程式を解いています。戻り値はboost::python::object型なので、それをboost::numpy::ndarray型に変換しています。

36行目

結果を表示するために要素をdouble型として取り出しています。

実行結果

1
1

はい、きちんと計算できています。

まとめ


今回はBoost.PythonおよびBoost.Numpyを使ってC++からPythonの関数を呼び出してみました。

以前、通常のC APIをラップしたクラスを自前で作っていたのですが、その時はメモリ関係の謎のクラッシュに度々悩まされていたので、Boost.Python、Boost.Numpyは偉大だなと思います。

またWindowsでPythonのInteropをするときにVisual Studioのバージョンを気にしなくてもいいというのもありがたいです。

というわけで、今回の記事はこれで終わります。もし、上手くいかないなどの問題がありましたらコメントいただけると嬉しいです。

最後までお読みいただきありがとうございました。