如何用Cython封装一个C++类?
我有一个C++的类,它由一个.ccp文件和一个.h文件组成。这个类可以编译(我可以写一个主方法来成功使用它)。我想知道怎么用Cython把这个类包装起来,让它在Python中也能用。
我看过文档,但有些地方不太明白。文档里提到要生成cpp文件,但我尝试按照文档操作时,我已经存在的cpp文件就被覆盖了……
我在pyx文件里应该放些什么呢?有人告诉我需要放类的定义,但具体要放多少呢?只放公共方法就可以吗?
我需要一个.pxd文件吗?我不太明白这个文件什么时候需要,什么时候又不需要。
我试着在#python的IRC频道问这些问题,但没有人回答我。
4 个回答
Cython 主要是用来进行 C 语言开发的。如果你想把 C++ 和 Python 结合起来,我推荐你使用 Boost.Python。他们的文档非常好,可以帮助你很快上手。
虽然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文件中写实现代码。
经过很多尝试、错误和一些抓狂的时刻,我终于搞定了这个问题。不过,首先我得把我的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++函数,这点小代价是值得的。