如何用Cython封装一个C++类?

31 投票
4 回答
22779 浏览
提问于 2025-04-17 10:39

我有一个C++的类,它由一个.ccp文件和一个.h文件组成。这个类可以编译(我可以写一个主方法来成功使用它)。我想知道怎么用Cython把这个类包装起来,让它在Python中也能用。

我看过文档,但有些地方不太明白。文档里提到要生成cpp文件,但我尝试按照文档操作时,我已经存在的cpp文件就被覆盖了……

我在pyx文件里应该放些什么呢?有人告诉我需要放类的定义,但具体要放多少呢?只放公共方法就可以吗?

我需要一个.pxd文件吗?我不太明白这个文件什么时候需要,什么时候又不需要。

我试着在#python的IRC频道问这些问题,但没有人回答我。

4 个回答

3

Cython 主要是用来进行 C 语言开发的。如果你想把 C++ 和 Python 结合起来,我推荐你使用 Boost.Python。他们的文档非常好,可以帮助你很快上手。

22

虽然Cython通常是用来处理C语言的,但它也可以生成C++代码。在编译的时候,你只需要加上--cplus这个选项。

现在,为一个类创建一个包装器很简单,和为一个结构体包装差不多。主要的区别在于声明extern,但其实差别不大。

假设你有一个类MyCppClass,它在mycppclass.h文件里。

cdef extern from "mycppclass.h":
    cppclass MyCppClass:
        int some_var

        MyCppClass(int, char*)
        void doStuff(void*)
        char* getStuff(int)

cdef class MyClass:

    # the public-modifier will make the attribute public for cython,
    # not for python. Maybe you need to access the internal C++ object from
    # outside of the class. If not, you better declare it as private by just
    # leaving out the `private` modifier.
    # ---- EDIT ------
    # Sorry, this statement is wrong. The `private` modifier would make it available to Python,
    # so the following line would cause an error es the Pointer to MyCppClass
    # couldn't be converted to a Python object.
    #>> cdef public MyCppClass* cobj
    # correct is:
    cdef MyCppClass* obj

    def __init__(self, int some_var, char* some_string):
        self.cobj = new MyCppClass(some_var, some_string)
        if self.cobj == NULL:
            raise MemoryError('Not enough memory.')

    def __del__(self):
        del self.cobj

    property some_var:
        def __get__(self):
            return self.cobj.some_var
        def __set__(self, int var):
            self.cobj.some_var = var

需要注意的是,new这个关键字只有在设置了--cplus选项时才可用,否则你需要通过<stdlib.h>里的malloc来分配内存,并且要用extern来声明。

另外,你不需要解引用指针(也就是用->)来调用方法。Cython会跟踪对象的类型,并自动应用合适的方式。

.pxd文件是用来分开声明和实现的,或者避免命名空间冲突。想象一下,如果你想把你的Python包装器命名为和C++类一样,只需在你的.pxd文件中写入extern声明,然后在.pyx文件中使用cimport引入这个.pxd文件。

cimport my_pxd
cdef my_pxd.SomeExternedType obj

需要注意的是,你不能在.pxd文件中写实现代码。

9

经过很多尝试、错误和一些抓狂的时刻,我终于搞定了这个问题。不过,首先我得把我的C++代码改写成C,这对我来说主要就是把所有的std::string变量换成char*,并且要注意一些长度的问题。

完成后,我得到了我的.h和.c文件。我想把C代码中的一个函数在Python中使用。结果发现,Cython可以帮你把C文件编译成扩展模块,并且一次性链接所有需要的库,所以我从我的setup.py开始,最后看起来是这样的:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules=[
  Extension("myext",
    ["myext.pyx", "../stuff.c"],
    libraries=["ssl", "crypto"]
  )
]

setup(
  name = "myext",
  cmdclass = {"build_ext": build_ext},
  ext_modules = ext_modules
)

如你所见,Extension的第二个参数就是列出所有需要编译的文件,Cython会根据文件的扩展名来判断怎么编译。libraries数组告诉Cython编译器需要链接哪些库(在这个例子中,我是在封装一些加密的东西,而这些在现有的Python库中无法直接模拟)。

为了在.pyx文件中使用我的C函数,你需要在.pxd文件中写一个小的包装器。我的myext.pxd文件如下:

cdef extern from "../stuff.h":
    char* myfunc(char* arg1, char* arg2, char* arg3)

然后在.pyx文件中,你使用cimport声明来导入这个函数,这样就可以像使用其他Python函数一样使用它了:

cimport myext

def my_python_func(arg1, arg2, arg3):
    result = myext.myfunc(arg1, arg2, arg3)
    return result

当你在Mac上构建这个时,你会得到一个.so文件,可以在Python中导入并运行.pyx中的函数。可能还有更好、更正确的方法来实现这一切,但这些都是经验的积累,而这是我第一次成功搞定的。我很想知道我可能哪里做错了。

更新:

在进一步使用Cython后,我发现把它和C++结合起来也非常简单,只要你知道自己在做什么。让C++的string可用,只需在你的pyx/pyd文件中写from libcpp.string cimport string。声明C++类也同样简单:

cdef extern from "MyCPPClass.h":
    cdef cppclass MyCPPClass:
        int foo;
        string bar;

当然,你基本上需要用Python的格式重新声明一下你的类的.h定义,但为了能访问你已经写好的C++函数,这点小代价是值得的。

撰写回答