藻ログ

都会でOLをしています

pyenv環境でBoost.Python

概要

OS Xのpyenv環境でBoost.Pythonしようとしたらハマった話

Boost.Python

Boost.PythonC++Python用拡張モジュールが非常に美しく作れるboostライブラリだ.
Boost.Python - 1.61.0
他の選択肢としてCython, SWIGなどが有名とは思うが,文法を眺めた結果C++側で完結して綺麗に書けそうな*1Boost.Pythonを選択した.

d.hatena.ne.jp

Quick Start

超簡単な例としてhelloworldaddを挙げる.
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が使える.

libpythonと実行時のpythonを揃える

以上を考慮して, -L/path/to/libpython と実行時のpythonを揃えると,systemのpython*2 では動作確認することができた.
しかし,pyenvのanaconda以下のlibpythonをリンクしても,pyenv環境下では同様にPyEval_SaveThread: NULLで落ちてしまい実行できなかった.

どうしても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-pythonosx-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)

*1:Cythonはpython側でも変な文法でラッパを書かないといけないのがつらそうだった

*2:/System/Library/Frameworks/Python.framework/ のpythonのこと

*3:systemのpython使ってないので,またnumpyだのmatplotlibだの設定するの面倒

*4:別にvirtualenvだけでよくない?ダメ?

*5:libとobjは勝手にできるので作らなくていい