Cython:如何包装 C++ class,其中 public 成员变量是自定义对象?

Cython: How do I wrap a C++ class where public member variables are custom objects?

问题:

为了说明,我将使用 simple Rectangle tutorial in the Cython docs。假设我添加了一个自定义 C++ 对象 Coord:

//Rectangle.h
class Coord {
public:
    int x, y;
    Coord(int x0, int y0);
    ~Coord();
    void move(int dx, int dy);

    //Omitted the rest for brevity...
};

并且我在Rectangle中做了两个public成员变量来表示矩形的左下角和右上角:

//Rectangle.h
class Rectangle {
public:
    Coord lower_left, upper_right;          //added this

    //Omitted the rest for brevity...
}; 

如果我想将 Python 中的 Coord 作为 python 对象进行操作,我该怎么做?这似乎是一件很基本的事情,但我还没有在网上找到任何 tutorials/guides 来说明如何做到这一点。


我尝试了什么:

所以在我的尝试中,我尝试将 Coord 包装为 PyCoord 以便 PyRectangle 镜像 c++ 矩形对象。在定义中,我公开了 c++ Coord 并将它们添加为 Rectangle 的成员变量,如下所示:

#rect.pyx
cdef cppclass Coord:                        #added this cppclass
    Coord(int, int) except +
    void move(int, int)

cdef cppclass Rectangle:
    Rectangle(int, int, int, int) except +
    Coord lower_left, upper_right           #added these member variables

#The rest omitted for brevity...

这是我在包装器中尝试做的事情:

#rect.pyx
cdef class PyCoord:
    cdef Coord *thisptr

    def __cinit__(self):
        self.thisptr = new Coord(0,0)
    def __dealloc__(self):
        del self.thisptr
    def move(self, int dx, int dy):
        self.thisptr.move(dx, dy)

cdef class PyRectangle:
    cdef Rectangle *thisptr
    cdef PyCoord lower_left, upper_right    #want to manipulate these in Python

    def __cinit__(self, int x0, int y0, int x1, int y1):
        self.thisptr = new Rectangle(x0, y0, x1, y1)
        self.lower_left.thisptr = &(self.thisptr.lower_left)
        self.upper_right.thisptr = &(self.thisptr.upper_right)

#The rest omitted for brevity...

所以上面的第一个问题是当我重新分配 lower_leftupper_right 时存在内存泄漏,因为 new Coord(0,0) 从未被删除,对吗?但主要的想法是能够从 Python:

做这样的事情

Python 翻译

>>> from rect import *
>>> a = PyRectangle(1,1,2,2)
>>> a.upper_right.move(1,1)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'move'

但是如您所见,我 运行 遇到了问题。实现这一目标的最佳方法是什么?一定有一些标准的方法可以做到这一点,对吧?


编辑: 所以看起来我可以访问像这样的原始 C++ 成员变量:

#rect.pyx
cdef cppclass Coord:
    int x, y                                #exposed x and y to cython
    Coord(int, int) except +
    void move(int, int)

cdef class PyCoord:
    cdef Coord *thisptr

    property c_x:                   #can use this to access C++ member x
        def __get__(self):
            return self.thisptr.x
        def __set__(self, int i):
            self.thisptr.x = i

    property c_y:                   #can use this to access C++ member y
        def __get__(self):
            return self.thisptr.y
        def __set__(self, int i):
            self.thisptr.y = i

    def __cinit__(self):
        self.thisptr = new Coord(0,0)
    def __dealloc__(self):
        del self.thisptr
    def move(self, int dx, int dy):
        self.thisptr.move(dx, dy)

因此,我可以在解释器中这样做:

Python 翻译

>>> from rect import *
>>> a = PyCoord()
>>> a.c_x = 1
>>> a.c_y = 2
>>> print ("(%s, %s)"%(a.c_x, a.c_y))
(1, 2)

但自定义对象类型仍然不成功。我不能在PyRectangle中使用property到get/setCoord,或者至少不知道如何正确使用。

您需要一种包装 Coord 的方法,它不会声明对内存的所有权。 PyCoord 的以下修改包含对实际所有者的引用,既表明 PyCoord 不应该释放内存,也为了让实际所有者在 PyCoord 期间保持活动状态还活着。

cdef class PyCoord:
    cdef Coord *thisptr
    cdef object owner # None if this is our own

    def __cinit__(self):
        self.thisptr = new Coord(0,0)
        owner = None
    cdef set_ptr(self,Coord* ptr,owner):
        if self.owner is None:
            del self.thisptr
        self.thisptr = ptr
        self.owner = owner
    def __dealloc__(self):
        # only free if we own it
        if self.owner is None:
            del self.thisptr
    def move(self, int dx, int dy):
        self.thisptr.move(dx, dy)

您可以使用属性为 PyRectangle

提供漂亮的界面
cdef class PyRectangle:
    cdef Rectangle *thisptr

    def __cinit__(self, int x0, int y0, int x1, int y1):
        self.thisptr = new Rectangle(x0, y0, x1, y1)

    property lower_left:
        def __get__(self):
            c = PyCoord()
            # for some reason I couldn't do two levels of pointer access at once...
            # so we use a temporary
            thisptr = self.thisptr
            c.set_ptr(&thisptr.lower_left,self)
            return c
    # lower_right looks identical

注意:处理许多不同的对象所有权情况肯定是 Boost Python 考虑过的事情(我认为 SWIG 也考虑过)所以如果你经常这样做,他们可能会提供一个稍微更优雅的解决方案。