使用 C/C++ 扩展 Python
Python与C/C++互操作有很多方案:Python C API, swig, sip, ctypes, cpython, cffi, boost.python等。这里选择了最原始的Python C API方式。
一、开发前准备
1.Python对象
大多数Python对象在Python解析器中都为PyObject,在C代码中只能声明PyObject*类型的python对象,然后使用该对象对应的初始化函数初始化。如PyTuple_New,PyList_New,PyDict_New,Py_BuildValue等。
例如构建一个{‘a':{‘b':['123','34']}}对象
PyObject* obj = PyDict_New(); PyObject* b = PyDict_New(); PyObject* c = PyList_New(2); PyList_SetItem(c, 0, Py_BuildValue("s", "123")); PyList_SetItem(c, 1, Py_BuildValue("s", "34")); PyDict_SetItem(a, "b", c); PyDict_SetItem(obj, "a", a);
Python对象问题这里有一些文档:
http://docs.python.org/2/c-api/intro.html#objects-types-and-reference-counts
http://docs.python.org/2/c-api/dict.html
http://docs.python.org/2/c-api/list.html
2.Python内存管理
Python对象管理采用引用技术模型,内部有一些复杂的循环引用等处理措施。主要有 Py_INCREF() / Py_DECREF()两个宏负责处理。具体文档可以看这里http://docs.python.org/2/c-api/intro.html#reference-counts
例如上一点申请的对象obj如果需要释放怎么办?不可以直接free/delete,直接Py_DECREF(obj),然后obj = NULL即可,否则会报错。
3.线程安全
Python由于历史比较悠久,作者在开发的时候可能并没有考虑到多线程这个东西,因为Python的内存管理并不是线程安全的。在后来后来版本中为了处理这个线程安全问题引入了GIL即global interpreter lock。这是一个粗粒度的锁,执行Python ByteCode之前都会取得这个锁。以至于Python的多线程比较鸡肋,GIL也就成了性能瓶颈。这个问题很多地方都有讨论,我之前有一篇文章专门对这个问题进行了说明,感兴趣的同学请去这里http://in.sdo.com/?p=1623。
有人会问为什么不设计更细粒度的锁?实际上有人已经进行了尝试,但是为了不增加实现的复杂性也就一直没有加到CPython中。其他版本的python如IronPython等对这个问题已经做了改善。
实际开发时有两种情况需要关心:
1).释放锁
这种情景只要在进行IO或CPU繁重的计算时,暂时释放GIL使得其他线程的代码可以执行。
2).取得锁
主要出现在C回调Python代码
参考文档:
http://docs.python.org/2/c-api/init.html#thread-state-and-the-global-interpreter-lock
二、开发扩展
有了上面的知识我们开始进行实际的开发。
1.导出函数
写好C API函数之后我们需要导出,写一个函数描述表即可,如下面的EchoMethods,一定要以NULL结尾。
PyObject* echo(PyObject* self, PyObject* args) { char* input = NULL; if(!PyArg_ParseTuple(args, "s", &input)) { printf("parse arg errorn"); return NULL; } int count = 0; do { printf("%sn", input); count++; }while(count < 100); return Py_BuildValue("i", 0); } static PyMethodDef EchoMethods[] = { {"echo", (PyCFunction)echo, METH_VARARGS}, {NULL, NULL} };
2.导出对象
除了上面提到的使用复杂的PyObject操作语法封装一个Python对象返回之外还有其他途径,如直接导出C的Struct到Python。这里不详谈,需要的可以查相关资料。
3.初始化模块
模块初始化调用Py_InitModule,传入模块名和模块的方法描述表即可。如果初始化失败会返回error可以做相应处理。
PyMODINIT_FUNC initecho() { Py_InitModule("echo", EchoMethods); }
三、编译与使用
1.如何编译、分发、使用
上面这些代码当然会用到python-devel库。编译的时候使用GCC直接编译成一般的so,就可以直接在python里面调用了。Python会自己选择如何加载这个so。
g++ -c echo.c -I /usr/include/python2.7/include/python2.7 -fPIC g++ -shared echo.o -o echo.so
上面已经提到了,实际上把自己编译好的so放在PYTHONPATH路径中的任意一个下面都可以直接调用了。
2.更便捷的方式
上面的编译方式可以自己写一个Makefile处理起来更灵活,实际上Python有一个更方便的处理方式。使用distutils包,编译安装一步到位,这也是easy_install等工具使用的方式。
上面这个简单使用distutils处理起来像这样:
from distutils.core import setup, Extension echomodule = Extension("echo", sources = ["echo.c"]) setup(name = "echo", version = "1.0", description = "test", author = "dudu" ext_modules = [echomodule])
Extension对象定义一个扩展的源文件、需要用到的第三方库、头文件、特殊的编译选项等等,而setup则定义安装的规则及扩展的一些属性。
使用的时候执行下面两个命令就可以了。
python setup.py build sudo python setup.py install
这部分可以参考http://docs.python.org/2/distutils/apiref.html
文章是写完了。特别推荐需要开发许多接口的人去看看开头提到的swig/sip等等,这些项目只需要编写简单的规则,就可以为c/c++中的方法生成wrapper。这里只所以有采用c api是因为需求简单,需要暴露给python的总共也没几个函数。
作者:麦田守望
就职于盛大创新院,主要从事搜索引擎研发等工作。熟悉C/C++,Python,Node.JS
来自:http://www.the520.cn/2014/02/27/python_c_api_extension.htm