pyenv環境でBoost.Python
Boost.Python
Boost.PythonはC++でPython用拡張モジュールが非常に美しく作れるboostライブラリだ.
Boost.Python - 1.61.0
他の選択肢としてCython, SWIGなどが有名とは思うが,文法を眺めた結果C++側で完結して綺麗に書けそうな*1Boost.Pythonを選択した.
Quick Start
超簡単な例としてhelloworld
とadd
を挙げる.
C++側ではBoost.Pythonの文法に従ってこんな風に書く.
basic.cpp
#include <boost/python.hpp> int add(int lhs, int rhs) { return lhs + rhs; } void hello(void) { printf("hello,world\n"); } BOOST_PYTHON_MODULE(basic) { boost::python::def("add", add); boost::python::def("hello", hello); }
そしてこんな感じでコンパイル&リンクしてやれば basic.so
ができる.
$ g++ -I/path/to/boost_python_headers -I/path/to/python_headers -shared -fPIC -o basic.so basic.cpp -L/path/to/libboost_python -lboost_python -L/path/to/libpython -lpython{VERSION}
あとはpython側でimportしてやるだけで実行できる.うーん最高だ.
import basic basic.hello() #=> hello,world basic.add(2,3) #=> 5
注意点としては,BOOST_PYTHON_MODULE(module_name)と,*.soの名前は一致させる必要がある.モジュール名とライブラリ名が違うと以下のようにImportErrorとなる.
Traceback (most recent call last): File "<string>", line 1, in <module> ImportError: dynamic module does not define init function (initfoo)
他にも, C++で作ったオブジェクトを簡単にpython側に見せたり,便利機能が揃っている.うーん最高だ.
Exposing Classes - 1.61.0
ここまでは良い.
Boost.Pythonが落ちる
私は普段pyenvでインストールしたanacondaのpythonを使っているのだが,このpyenvのpythonを使う場合モジュール実行がどうもうまくいかない.
GitHub - yyuu/pyenv: Simple Python version management
PyThreadStateで落ちる
そのpythonを使って自作モジュールを実行すると(importはできるが)関数実行時にエラーを吐いて落ちる.
Fatal Python error: PyEval_SaveThread: NULL tstate [1] 52771 abort python
Initialization, Finalization, and Threads — Python 3.5.2 documentation
色々解決策を探していると,これはリンクしてるpythonライブラリと実行時のpythonで違うバージョンのpythonを使っていると生じる問題らしいことがわかった.
stackoverflow.com
オブジェクトがどのdllに依存しているかは
$ otool -L basic.so
で調べることができる.Linuxの場合はldd
が使える.
どうしてもpyenv環境で実行したい
systemのpythonで動くことは確認できたが,ここで諦めてもしかたないしやっぱりpyenv環境で(というかanacondaで)実行したい*3.
全部のpythonを揃える
最後に怪しいのはboost-pythonライブラリ自体のビルドするときのpythonだ.ということは,boost-python自体をビルドし直す必要がある. うーん面倒くさい.そこで,anacondaからconda installでboost-pythonが入れられることを思い出した.
Boost :: Anaconda Cloud
$ anaconda search -t conda boost-python
で好きなboost-python(osx-64ビルド)をconda install
で入れてやれば良い.
ただし,libstdc++
でなくlibc++
が使われているものを選ぶこと.
$ conda install -c ericdill boost-python=1.55
stackoverflow.com
condaで入れたlibboost_pythonをリンクしてやると,pyenv/anacondaの環境下でもBoost.Pythonが無事動作することを確認できた(めでたい).
ここがわからない
だがしかし,pyenvのpython(=$(PYENV_ROOT)/shims/python)を通常通り用いると今度はSEGVで落ちる問題が残る.
$ python -c "import basic; basic.add(2,3)" [1] 20920 segmentation fault python -c "import basic; basic.add(2,3)"
これはpyenv/shims/python
は使用するpythonに対する単なるシンボリックリンクではなく,bashスクリプトであることに原因がありそうだったのだが,pyenvのスクリプトを追っかけるのは辛い...
解決策としては以下のように${PYENV_ROOT}/shims/python
越しでなく直接実行時のpythonを指定すると問題なく動作することが一応わかった.
$ ~/.pyenv/versions/anaconda-2.4.0/bin/python -c "import basic; print basic.add(2,3)" $ 5
結論
pyenvやめましょう*4
まとめ
- boost_python自体のビルドとboost_python使ったプログラムでリンクするlibpythonと実行時のpythonを揃える.
- 実行時pythonはpyenv/shims経由でなく直接指定する
- pyenvやめれば万事解決
うーんやり始めたときは
$ g++ `python-config --includes` -fPIC -o basic.so basic.cpp `python-config --ldflags`
みたいな感じで良いだろうと思ってたけど,boost-python自体のビルド問題が絡んで泥臭くなってしまった.
もっと良い方法がないだろうか.
ここまで読んでてすごいわかりにくいと思ったので適当にmakefileを書いた. ディレクトリ構成はこういう感じ. *5 どうでも良くないけどいい加減cmake覚えたい.
proj_root ├── include │ ├── basic.h ├── src │ ├── basic.cpp ├── lib │ ├── basic.so Makefile
Makefile
TARGETS=basic.so BINDIR=lib SRCDIR=src OBJDIR=obj CXX=clang++ MD=mkdir INCLUDE_PATH=./include BOOST_INCLUDE_PATH=$(PYENV_ROOT)/versions/anaconda-2.4.0/include PYTHON_INCLUDE_PATH=$(PYENV_ROOT)/versions/anaconda-2.4.0/include/python2.7 LIBRARY_PATH=$(PYENV_ROOT)/versions/anaconda-2.4.0/lib CXXFLAGS=-O2 -Wall -Wextra -std=c++11 -fPIC -I$(INCLUDE_PATH) -I$(BOOST_INCLUDE_PATH) -I$(PYTHON_INCLUDE_PATH) LDFLAGS= -L$(LIBRARY_PATH) LIBS= -lboost_python -lpython2.7 SRCS=$(wildcard $(SRCDIR)/*.cpp) OBJS=$(SRCS:$(SRCDIR)/%.cpp=$(OBJDIR)/%.o) DEPS=$(SRCS:$(SRCDIR)/%.cpp=$(OBJDIR)/%.d) all:$(OBJDIR) $(BINDIR) $(BINDIR)/$(TARGETS) $(OBJDIR): @if [ ! -d $(OBJDIR) ]; then $(MD) $(OBJDIR); fi $(BINDIR): @if [ ! -d $(BINDIR) ]; then $(MD) $(BINDIR); fi $(BINDIR)/%.so: $(OBJS) $(CXX) -shared -o $@ $(OBJS) $(LDFLAGS) $(LIBS) @echo 'Linking Complete.' $(OBJDIR)/%.o:$(SRCDIR)/%.cpp $(CXX) -c $< $(CXXFLAGS) -o $@ @echo 'Compiled' $< 'Successfully.' clean: $(RM) $(BINDIR)/$(TARGETS) $(OBJS) $(DEPS) -include $(DEPS)