Pyrex / Cython で C++ の参照を扱う方法

またまた頭が悪いせいで予想以上に時間をかけてしまった。

例えば次のようなクラスを Pyrex (Cython) でラップすることを考える。

template<typename T_>
struct position: public boost::array<T_, 3>
{
    position()
    {
        (*this)[0] = 0;
        (*this)[1] = 0;
        (*this)[2] = 0;
    }

    position(T_ x, T_ y, T_ z)
    {
        (*this)[0] = x;
        (*this)[1] = y;
        (*this)[2] = z;
    }

    T_& x() {
        return (*this)[0];
    }

    const T_& x() const {
        return (*this)[0];
    }

    T_& y() {
        return (*this)[1];
    }

    const T_& y() const {
        return (*this)[1];
    }

    T_& z() {
        return (*this)[2];
    }

    const T_& z() const {
        return (*this)[2];
    }
};

このとき、x, y, z を Python のプロパティとして expose するにはどうすべか、という話。Pyrex (Cython) は基本的に C の文法しか理解しないので、次の宣言は通らない。

cdef extern from "position.hpp":
    ctypedef struct __impl_position "position<double>":
        double& x()
        double& y()
        double& z()

    __impl_position *__impl_position_new "new position<double>" (double x, double y, double z)

    void __impl_position_del "delete" (__impl_position *)

エラー内容 (Cython の場合):

cythoning test.pyx to test.cpp

Error converting Pyrex file to C:
------------------------------------------------------------
...
cdef extern from "position.hpp":
    ctypedef struct __impl_position "position<double>":
        double& x()
             ^
------------------------------------------------------------

test.pyx:3:14: Empty declarator

また、当然次のようなコードでは lvalue assignment だと文句を言う。

cdef extern from "position.hpp":
    ctypedef struct __impl_position "position<double>":
        double x()
        double y()
        double z()

    __impl_position *__impl_position_new "new position<double>" (double x, double y, double z)

    void __impl_position_del "delete" (__impl_position *)

cdef class Position:
    cdef __impl_position *pimpl

    def __cinit__(self, double x = 0, double y = 0, double z = 0):
        self.pimpl = __impl_position_new(x, y, z)

    def __dealloc__(self):
        __impl_position_del(self.pimpl)

    property x:
        def __get__(self): return self.pimpl.x()
        def __set__(self, val): self.pimpl.x() = val

    property y:
        def __get__(self): return self.pimpl.y()
        def __set__(self, val): self.pimpl.y() = val

    property z:
        def __get__(self): return self.pimpl.z()
        def __set__(self, val): self.pimpl.z() = val

エラー内容:

Error converting Pyrex file to C:
------------------------------------------------------------
...
    def __dealloc__(self):
        __impl_position_del(self.pimpl)

    property x:
        def __get__(self): return self.pimpl.x()
        def __set__(self, val): self.pimpl.x() = val
                                           ^
------------------------------------------------------------
test.pyx:22:44: Cannot assign to or delete this

仕方なく、次のようなヘルパーを作って解決。ちょっと黒魔術的な気がするけど。

#ifndef HELPER_HPP
#define HELPER_HPP

namespace helper {

template<typename T_>
inline void ref_set(T_& ref, const T_& val) { ref = val; }

} // namespace helper

#endif /* HELPER_HPP */

で、

cdef extern from "helper.hpp":
    void __helper_set_double "helper::ref_set<double>" (double, double)

cdef extern from "position.hpp":
    ctypedef struct __impl_position "position<double>":
        double x()
        double y()
        double z()

    __impl_position *__impl_position_new "new position<double>" (double x, double y, double z)

    void __impl_position_del "delete" (__impl_position *)

cdef class Position:
    cdef __impl_position *pimpl

    def __cinit__(self, double x = 0, double y = 0, double z = 0):
        self.pimpl = __impl_position_new(x, y, z)

    def __dealloc__(self):
        __impl_position_del(self.pimpl)

    property x:
        def __get__(self): return self.pimpl.x()
        def __set__(self, val): __helper_set_double(self.pimpl.x(), val)


    property y:
        def __get__(self): return self.pimpl.y()
        def __set__(self, val): __helper_set_double(self.pimpl.y(), val)

    property z:
        def __get__(self): return self.pimpl.z()
        def __set__(self, val): __helper_set_double(self.pimpl.z(), val)

ちゃんと動くかためしてみる。

Python 2.5.1 (r251:54863, Mar  7 2008, 04:10:12) 
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from test import Position
>>> p = Position()
>>> p.x = 1.0
>>> p.y = 2.0
>>> p.z = 3.0
>>> print p.x, p.y, p.z
1.0 2.0 3.0

今回のテストで使ったファイルは以下よりダウンロードできます。
pyrex-cxx-ref.tar.bz2