2018/05/04

PythonとC++間でのnumpy配列の受け渡し

はじめに


Pythonで作ったnumpy.ndarrayをC++側で加工して、Pythonに返す方法を試します。

PythonからCの関数を呼び出す方法は色々あるようですが、 今回は、numpyを操作したいので、Boost C++ Librariesを使ってみます。

準備


Boost C++ Librariesをインストールします。
https://www.boost.org/doc/libs/1_67_0/more/getting_started/unix-variants.html
の第1節に従ってダウンロードし、第5節に従ってビルドします。

方法(動的リンク版)


まず、C++のコードです。ファイル名は test1.cpp とします。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <boost/python/numpy.hpp>
namespace p = boost::python;
namespace np = boost::python::numpy;

void plus_scalar(np::ndarray &a, float b)
{
  auto N = a.shape(0);
  auto *p = reinterpret_cast<float*>(a.get_data());
  for(auto v=p; v!=p+N; ++v){*v=*v+b;}
}

BOOST_PYTHON_MODULE(test1)
{
  Py_Initialize();
  np::initialize();
  p::def("plus_scalar", plus_scalar);
}
Python側から第1引数で渡される1次元NumPy配列に第2引数で渡される値を足すだけの関数plus_scalarを定義しています。

このtest1.cppをコンパイルします($はシェルのプロンプトです)。

$ g++ --std=c++14 --shared -fPIC -I$BOOST_PATH/include/ -I/usr/include/python2.7/ test1.cpp -L$BOOST_PATH/lib -lboost_numpy27 -lboost_python27 -o test1.so
$BOOST_PATHはBoost C++ Librariesをインストールしたパスです。

ライブラリのパスが通っていないので、

$ export LD_LIBRARY_PATH=$BOOST_PATH/lib
を実行し、.soをロードできるようにしておきます。

これをPythonから呼び出します。次のようにPythonスクリプト t1.py を作成します。

1
2
3
4
5
6
7
8
9
import sys
sys.path.append('.')
import test1
import numpy as np

a = np.array([1,2,3], dtype=np.float32)
print(a)
test1.plus_scalar(a, 3.0)
print(a)

そして、実行します。

python t1.py
すると、
[ 1.  2.  3.]
[ 4.  5.  6.]
が出力されます。確かに、3.0が足されています。

方法(静的リンク版)


動的リンク版では LD_LIBRARY_PATH を設定しましたが、使いにくいのでBoost関連のライブラリだけでも静的リンクしておきましょう。次のようにすることで.soを作ることができます。
g++ --std=c++14 --shared -fPIC -I$BOOST_PATH/include/ -I/usr/include/python2.7/ test2.cpp $BOOST_PATH/lib/libboost_numpy27.a $BOOST_PATH/lib/libboost_python27.a -o test2.so

もし、次のエラー

/usr/bin/ld: $BOOST_PATH/lib/libboost_numpy27.a(ndarray.o): 再配置 R_X86_64_32 (`.rodata.str1.8' に対する) は共有オブジェクト作成時には使用できません。-fPIC を付けて再コンパイルしてください。
$BOOST_PATH/lib/libboost_numpy27.a: error adding symbols: 不正な値です
collect2: error: ld returned 1 exit status
が出て、リンクできないときは、エラーメッセージの通りにBoostをビルドし直します。 具体的には、[4]を参考に、
$ ./b2 --clear
$ ./b2 cxxflags=-fPIC cflags=-fPIC install
のように、cxxflagsを追加してビルドし直します。cflagsは不要かもしれません。なお、ここで追加したオプション名は、--cxxflagsでも、-cxxflagsでもなく、ハイフン無しの単なるcxxflagsです。

test2.cppは

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <boost/python/numpy.hpp>

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

void plus_scalar(np::ndarray &a, float b)
{
  auto N = a.shape(0);
  auto *p = reinterpret_cast<float*>(a.get_data());
  for(auto v=p; v!=p+N; ++v){*v=*v+b;}
}

BOOST_PYTHON_MODULE(test2)
{
  Py_Initialize();
  np::initialize();
  p::def("plus_scalar", plus_scalar);
}
t2.pyは
1
2
3
4
5
6
7
8
9
import sys
sys.path.append('.')
import test2
import numpy as np

a = np.array([1,2,3], dtype=np.float32)
print(a)
test2.plus_scalar(a, 3.0)
print(a)
です。test1がtest2に変わっただけです。
$ python t2.py
を実行すると、動的リンク版のときと同様に
[ 1.  2.  3.]
[ 4.  5.  6.]
と出力されます。

参考


[1] http://d.hatena.ne.jp/nihohi/20120306/1331002942
[2] http://tadaoyamaoka.hatenablog.com/entry/2017/05/25/234934
[3] https://stackoverflow.com/questions/10968309/how-to-import-python-module-from-so-file
[4] https://stackoverflow.com/questions/27848105/how-to-force-compilation-of-boost-to-use-fpic

0 件のコメント :