Boost.Python の機能をざっと紹介してみる

Boost の一部ながらも「実用的」すぎるため、とかくテンプレートメタプログラミングを愛好する諸兄から黙殺されてきた不幸のライブラリ、Boost.PythonBoost.勉強会でこいつの魅力を伝えようと思ったのだけど、時間の都合で無理だったので、簡単に紹介してみたい。

Boost.Python の基礎

Boost.PythonC++ のクラスや関数をラップする Python モジュールを恐ろしく簡単に書けるようにする、強力なライブラリである。

特に、Pyrex や Cython と比べて何がうれしいのかというと、

  • Python側にいちいちラッパ関数を書かなくてよい (テンプレートにより自動的に定義される)
  • コンバータを登録することで、PythonC++の型の透過的な変換が容易にできる
  • C++ のクラスを分かりやすい形で Python のクラスとして見せることが可能

といった点が挙げられる。

とりあえず、簡単なサンプルを。

basic.cpp:

#include <boost/python.hpp>

int add(int lhs, int rhs)
{
    return lhs + rhs;
}

BOOST_PYTHON_MODULE(basic)
{
    using namespace boost::python;
    def("add", &add);
}

ここでは、2 つの引数を取り、その引数の合計値を返す関数 add() を定義し、それを Python のモジュール関数 "add" としてラップして公開している。

これを

g++ -I`python -c 'from distutils.sysconfig import *; print get_python_inc()'` -DPIC -shared -fPIC -o basic.so basic.cpp -lboost_python 

Mac OS X では

g++ -I`python -c 'from distutils.sysconfig import *; print get_python_inc()'` -DPIC -bundle -fPIC -o basic.so basic.cpp -lboost_python  -framework Python

Windows (Visual Studio) では (環境変数 INCLUDE と環境変数 LIB に Boost への参照が含まれていること、かつ autolink が有効なことが前提)

SET PYTHONHOME=[Pythonのインストールディレクトリ]
SET INCLUDE=%PYTHONHOME%\Include;%INCLUDE%
SET LIB=%PYTHONHOME%\Libs;%LIB%
CL /D_MBCS /EHsc /MD /LD /Febasic.pyd basic.cpp
MT -manifest basic.pyd.manifest -outputresource:basic.pyd;2

のようにしてコンパイルすると、basic.so (もしくは basic.pyd) という Python モジュールが生成される。

実際に動かしてみよう。

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import basic
>>> dir(basic)
['__doc__', '__file__', '__name__', '__package__', 'add']
>>> basic.add(1, 2)
3
>>>

確かに動いている。

int 型でないものを引数として渡した場合はどうなるかというと、

>>> basic.add("a", "b")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
    basic.add(str, str)
did not match C++ signature:
    add(int, int)

上記のように、きちんと型チェックが行なわれる。

C++ クラスの公開

C++ クラスを Python のクラスとしてラップするには、boost::python::class_<> を利用する。

次の例では、accumulator という C++ クラスのメソッド accumulator::operator() を Python 側の __call__() に、accumulator::value() を Python 側の value プロパティに対応づけしつつ、同名のクラスとして Python に公開している。

basic2.cpp:

#include <boost/python.hpp>

class accumulator {
public:
    int operator()(int v) {
        v_ += v;
        return v_;
    }

    int value() const {
        return v_;
    }

private:
    int v_;
};

BOOST_PYTHON_MODULE(basic2)
{
    using namespace boost::python;

    class_<accumulator>("accumulator")
        .def("__call__",
             &accumulator::operator())
        .add_property("value", &accumulator::value)
        ;
}

実際の実行例:

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import basic2
>>> a = basic2.accumulator()
>>> a(1)
1
>>> a(2)
3
>>> a(3)
6
>>> a(4)
10
>>> a.value
10
>>>

C++ オブジェクトを Python に返すには

Python 側からの呼び出しに対して C++ のオブジェクトを返すにはどうしたらよいだろうか。次の例を見てみよう。

returning_object.cpp:

#include <vector>
#include <boost/python.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector get_list() const {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list)
        .def("add", &foo::add)
        ;
}

int の vector を持ち、その vector に要素を追加する add() と、vector への参照を取得する get_list() が定義された foo というクラスを、そのまま Python のクラスとして公開するものだ。

これをビルド後、実行してみると、

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import returning_object
>>> f = returning_object.foo()
>>> f.add(1)
>>> f.add(2)
>>> f.get_list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: No to_python (by-value) converter found for C++ type: class std::vect
or<int,class std::allocator<int> >

のように、std::vector に変換するためのコンバータがないというエラーが出てしまうことが確認できるはずだ。

Boost.Python には、PythonC++ の型の相互変換を行うためのコンバータという機構が備わっており、すべての値の受け渡しにはこのコンバータが介在している。

コンバータには

の 2 種類があり、型に対応するコンバータを必要に応じていずれか、もしくは両方登録しておく必要がある。

ビルトインのコンバータとして、次の型のものが用意されている。

c++ Python
bool bool
signed char / unsigned char int
short / unsigned short int
int / unsigned int int
long / unsigned long int
long long / unsigned long long long
float float
double float
long double float
char str
const char* str
std::string str
std::wstring unicode
PyObject* -
std::complex complex
std::complex complex
std::complex complex

また、Python のクラスを boost::python::class_<> で定義すると、自動的に lvalue、rvalue 両コンバータが登録されるようになっている。

ということで、次のように std::vector をラップしてみる。

#include <vector>
#include <boost/python.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector get_list() const {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list)
        .def("add", &foo::add)
        ;

    class_<foo::int_vector>("int_vector")
        .def("__getitem__", (int const&(foo::int_vector::*)(foo::int_vector::size_type) const)&foo::int_vector::at)
        .def("__len__", &foo::int_vector::size)
        ;
}

これをコンパイルすると、次のようなエラーとなってしまう。

Microsoft(R) C/C++ Optimizing Compiler Version 15.00.30729.01 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

returning_object.cpp
C:\boost-1.40.0\include\boost/python/detail/caller.hpp(223) : error C2027: 認識できない型 'boost::python::detail::specify_a_return_value_policy_to_wrap_functions_returning<T>' が使われています。
        with
        [
            T=result_t
        ]

... (略) ...

gcc だと

... (略) ...
/usr/include/boost/python/detail/invoke.hpp:88: error: no match for call to ‘(const boost::python::detail::specify_a_return_value_policy_to_wrap_functions_returning<const int&>) (const int&)’
In file included from /usr/include/boost/python/object/function_handle.hpp:8,
                 from /usr/include/boost/python/converter/arg_to_python.hpp:19,
                 from /usr/include/boost/python/call.hpp:15,
                 from /usr/include/boost/python/object_core.hpp:12,
                 from /usr/include/boost/python/args.hpp:25,
                 from /usr/include/boost/python.hpp:11,
                 from returning_object.cpp:2:
... (略) ...

Compile time assertion の流儀を知らないと、ぱっと見何のエラーなのか分からないのだが、要するにここで Boost.Python が伝えたい内容というのは
「specify a return_value_policy to wrap functions returning "const int&"」
ということである。

ここで return_value_policy というものが出てくるのだが、これは参照やポインタの戻り値をどう取り扱うかを指定するポリシークラスのことだ。戻り値ポリシークラスは boost::python::return_value_policy<ポリシークラス>() のようにして def() の引数に指定する。

ポリシークラスには次のようなものがある。

boost::python::return_opaque_pointer ポインタ値をラップするクラス (自動的に登録される) のインスタンスを返す
boost::python::return_by_value 値をそっくりそのままコピーしたものを Python のオブジェクトとして返す
boost::python::copy_const_reference const 参照を値として返す
boost::python::copy_non_const_reference 非 const 参照を値として返す
boost::python::manage_new_object オブジェクトのポインタを、Python 側でラッパが破棄されるタイミングで delete するような Python のオブジェクトを返す
boost::python::return_self / boost::python::return_arg n 番目の引数の値をそのまま戻り値として返す。void を返すメソッドを Python 側でメソッドチェイン可能にしたいときに便利。C++ 関数の戻り値は無視される
boost::python::reference_existing_object 参照やポインタをデリファレンスしたものに対して操作を行うラッパークラス (自動的に登録される) のインスタンスを返す。ライフサイクル管理は行われないので、dangling pointer が発生しないという保証がなければ使うべきではない。そうでなければ、下記の return_internal_reference<> を利用する
/* 例 */ def("test", &foo::test, return_value_policy<copy_const_reference>());

また、戻り値ポリシークラス return_value_policy<> の他に、custodian-and-ward (被後見人と後見人) 機構に基づいたポリシークラスを指定することができる。これは、戻り値のライフサイクルとメソッドの属しているオブジェクトのライフサイクルを一致させることができる機構だ。つまり、戻り値のオブジェクトが生きている限り、それを返したメソッドの属するオブジェクトが破棄されないことを保証することができるので、あるオブジェクトの内部に抱えているオブジェクトを戻り値とするときに便利である。これは、boost::python::return_internal_reference() などとして指定する。

/* 例 */ def("test", &foo::test, return_internal_reference<>());

これに従って、上のコードも次のように書き換えてみる。

#include <vector>
#include <boost/python.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector get_list() const {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list)
        .def("add", &foo::add)
        ;

    class_<foo::int_vector>("int_vector")
        .def("__getitem__", (int const&(foo::int_vector::*)(foo::int_vector::size_type) const)&foo::int_vector::at, return_value_policy<copy_const_reference>())
        .def("__len__", &foo::int_vector::size)
        ;
}

コンパイルが成功した後で、実際に実行してみることで、正しく戻り値を扱えていることが分かると思う。

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import returning_object
>>> f = returning_object.foo()
>>> f.add(1)
>>> f.add(2)
>>> l = f.get_list()
>>> l
<returning_object.int_vector object at 0x0000000001E6F438>
>>> len(l)
2
>>> l[0]
1
>>> l[1]
2

このコードだと、get_list() を呼ぶたびに int_vector のコピーが生成されてしまうので、それが望ましい挙動でない場合には boost::python::return_internal_reference<> を利用する。

#include <vector>
#include <boost/python.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector const& get_list() const {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list, return_internal_reference<>())
        .def("add", &foo::add)
        ;

    class_<foo::int_vector>("int_vector")
        .def("__getitem__", (int const&(foo::int_vector::*)(foo::int_vector::size_type) const)&foo::int_vector::at, return_value_policy<copy_const_reference>())
        .def("__len__", &foo::int_vector::size)
        ;
}

これを実行して試してみると、

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import returning_object
>>> returning_object.foo()
<returning_object.foo object at 0x0000000001FE3908>
>>> f = returning_object.foo()
>>> f.add(1)
>>> f.add(2)
>>> l = f.get_list()
>>> len(l)
2
>>> f.add(3)
>>> len(l)
3

のように、foo.get_list() の戻り値は、クラス foo のインスタンスの中にある int_vector を参照しつづけていることが分かる。

なお、__setitem__() を実装するには以下のようにする。

#include <vector>
#include <boost/python.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector& get_list() {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

void foo_setitem(foo::int_vector& self, foo::int_vector::size_type idx, int value)
{
    self[idx] = value;
}

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list, return_internal_reference<>())
        .def("add", &foo::add)
        ;

    class_<foo::int_vector>("int_vector")
        .def("__getitem__", (int const&(foo::int_vector::*)(foo::int_vector::size_type) const)&foo::int_vector::at, return_value_policy<copy_const_reference>())
        .def("__setitem__", &foo_setitem)
        .def("__len__", &foo::int_vector::size)
        ;
}

実行例:

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import return_object
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named return_object
>>> import returning_object
>>> f = returning_object.foo()
>>> f.add(1)
>>> l = f.get_list()
>>> l[0]
1
>>> l[0] = 2
>>> l[0]
2

indexing suite

std::vector<> などはよく使われるクラスなのに、毎度このように自分でラッパを手書きするのは非常に煩雑である。この手間を省くため、Boost.Python には suite と呼ばれる機構が用意されている。これは、def() 一つで Python クラスのメンバをまとめて追加できるようにする仕組みで、def() の引数に関数ポインタの代わりに suite クラスを指定すればいいようになっている。

Boost.Python には、標準で提供されている suite クラスとして indexing_suite というものがある。これは、std::vector<>、std::map<> といったクラスをラップするメソッド群を提供する suite 集である。

これを使うと、上のコードは以下のようにシンプルにできる。

#include <vector>
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector& get_list() {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

BOOST_PYTHON_MODULE(returning_object)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list, return_internal_reference<>())
        .def("add", &foo::add)
        ;

    class_<foo::int_vector>("int_vector")
        .def(vector_indexing_suite<foo::int_vector>())
        ;
}

カスタムコンバータを登録する

以上の例では、std::vector<> のラッパークラスを定義し、それを Python 側に公開していたが、現実的には、そのように特別なラッパーを用意せず、単純に Python のリストとして見せたいこともある。このような場合は自分でコンバータを実装し、Boost.Python に登録してやる必要がある。

lvalue converter

まずは lvalue converter から見てみよう。

lvalue_converter.cpp:

#include <vector>
#include <boost/python.hpp>
#include <boost/foreach.hpp>
#include <boost/range/value_type.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector const& get_list() const {
        return v_;
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

template<typename T_>
class vector_to_pylist_converter {
public:
    typedef T_ native_type;

    static PyObject* convert(native_type const& v) {
        namespace py = boost::python;
        py::list retval;
        BOOST_FOREACH(typename boost::range_value<native_type>::type i, v)
        {
            retval.append(py::object(i));
        }
        return py::incref(retval.ptr());
    }
};

BOOST_PYTHON_MODULE(lvalue_converter)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list, return_value_policy<copy_const_reference>())
        .def("add", &foo::add)
        ;
    to_python_converter<foo::int_vector, vector_to_pylist_converter<foo::int_vector> >();
}

このように、lvalue converter の実装は非常に単純で、convert() という静的メソッドを持つクラスと変換元の型を to_python_converter<> の引数に指定してやるだけでいい。

このコードの実行例は以下のようになる。

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import lvalue_converter
>>> f = lvalue_converter.foo()
>>> f.add(1)
>>> f.add(2)
>>> f.get_list()
[1, 2]
rvalue converter

一方で、rvalue converter はやや複雑である。

以下の例では、引数として sequence が渡されたら、それを int_vector に変換し、中のものをすべてメンバ v_ の要素として追加する、オーバロードされた関数 add() を追加している。

#include <vector>
#include <new>
#include <algorithm>
#include <boost/python.hpp>
#include <boost/foreach.hpp>
#include <boost/range/value_type.hpp>

class foo {
public:
    typedef std::vector<int> int_vector;

public:
    int_vector const& get_list() const {
        return v_;
    }

    void add(int_vector const& that) {
        std::copy(that.begin(), that.end(), std::back_inserter(v_));
    }

    void add(int v) {
        v_.push_back(v);
    }

private:
    int_vector v_;
};

template<typename T_>
class vector_to_pylist_converter {
public:
    typedef T_ native_type;

    static PyObject* convert(native_type const& v) {
        namespace py = boost::python;
        py::list retval;
        BOOST_FOREACH(typename boost::range_value<native_type>::type i, v)
        {
            retval.append(py::object(i));
        }
        return py::incref(retval.ptr());
    }
};

template<typename T_>
class pylist_to_vector_converter {
public:
    typedef T_ native_type;

    static void* convertible(PyObject* pyo) {
        if (!PySequence_Check(pyo))
            return 0;

        return pyo;
    }

    static void construct(PyObject* pyo, boost::python::converter::rvalue_from_python_stage1_data* data)
    {
        namespace py = boost::python;
        native_type* storage = new(reinterpret_cast<py::converter::rvalue_from_python_storage<native_type>*>(data)->storage.bytes) native_type();
        for (py::ssize_t i = 0, l = PySequence_Size(pyo); i < l; ++i) {
            storage->push_back(
                py::extract<typename boost::range_value<native_type>::type>(
                    PySequence_GetItem(pyo, i)));
        }
        data->convertible = storage;
    }
};

BOOST_PYTHON_MODULE(rvalue_converter)
{
    using namespace boost::python;

    class_<foo>("foo")
        .def("get_list", &foo::get_list, return_value_policy<copy_const_reference>())
        .def("add", (void(foo::*)(int))&foo::add)
        .def("add", (void(foo::*)(foo::int_vector const&))&foo::add)
        ;
    to_python_converter<foo::int_vector, vector_to_pylist_converter<foo::int_vector> >();
    converter::registry::push_back(
        &pylist_to_vector_converter<foo::int_vector>::convertible,
        &pylist_to_vector_converter<foo::int_vector>::construct,
        boost::python::type_id<foo::int_vector>());
}

実際に実行してみると、

Python 2.6.2 (r262:71605, Apr 14 2009, 22:46:50) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import rvalue_converter
>>> f = rvalue_converter.foo()
>>> f.add(1)
>>> f.add(2)
>>> f.get_list()
[1, 2]
>>> f.add([3, 4])
>>> f.get_list()
[1, 2, 3, 4]

のようになり、python の list と int_vector がシームレスに扱えるようになっていることがわかる。

rvalue converter を実装するクラスには、convertible() と construct() という 2 つの静的メソッドがあることが期待される。

convertible() は、PyObject* として渡された Python のオブジェクトが、変換可能かどうかを判断するメソッドだ。もし変換できない場合は nullptr を、変換可能な場合は nullptr 以外のポインタを返せばよい。

construct() は、実際に変換を行う関数だ。第 2 引数の boost::python::converter::rvalue_from_python_stage1_data* は次のようなメンバを持つ struct で、convertible() の戻り値が convertible に、construct 自身へのポインタが construct に格納されるようになっている。

struct rvalue_from_python_stage1_data
{
    void* convertible;
    constructor_function construct;

};

rvalue converter の返す値を格納するメモリ領域は boost::python::converter::rvalue_from_python_stage1_data の後に続くアドレスにあらかじめ確保されているので、ここに placement new を使って変換後のオブジェクトを配置する。

アドレスを取得するには、次のテンプレートクラスを用い、

template <class T>
struct rvalue_from_python_storage
{
    rvalue_from_python_stage1_data stage1;

    // Storage for the result, in case an rvalue must be constructed
    typename python::detail::referent_storage<
        typename add_reference<T>::type
    >::type storage;
};
/* 例 */ reinterpret_cast<boost::python::converter::rvalue_from_python_storage<foo>*>(data)->storage.bytes

のようにする。

そして、最終的な変換結果を boost::python::converter::rvalue_from_python_stage1_data の convertible に入れたのち、Boost.Python 側に処理を戻せばよい。

もし、実際に何らかの変換操作をしない限り変換が可能かどうかが分からない場合は、convertible() の中である程度変換を行ってしまい、その結果を戻り値として返し convertible に入れておくという手がある。

また、変換後の値自体が渡された (ラッパークラス等の) Python オブジェクトの中に含まれる場合は、convert() を省略し、直接 convertible() の戻り値として、そのオブジェクトへのポインタのポインタを返す。

最後に、コンバータの登録は、

    boost::python::converter::registry::push_back(
        &<convertible() へのポインタ>,
        &<construct() へのポインタ>,
        boost::python::type_id<変換後のC++の型>());

とする。つまり、関数名を convertible や construct とする必要はなく、また 2 つを同じクラスの静的メソッドとして実装する必要もないのだが、慣例としてそうしておくとよい。

まとめ

  • 関数を登録するには boost::python::def()
  • クラスを登録するのは boost::python::class_<...>("...") と def()、add_property()、add_static_property() のメソッドチェイン
  • 2 つの indexing suite
  • コンバータ
    • lvalue converter は boost::python::to_python_converter<> で
    • rvalue converter は boost::python::converter::registry::push_back(...) で登録

仮想関数を持つクラスを Python で継承可能にする方法、オーバロードされた関数名の扱い、プロパティの扱い、pickle サポートなど、説明していない事項も多いが、とりあえずこれだけ知っていれば Boost.Python を使ってそれなりにプログラミングできるはずである。詳細は Boost.Python のリファレンスを参照してください。