使用 C/C++ 扩展 Python

jopen 10年前
前期的网页抽取算法使用C++开发,为了提升代码复用,减少维护成本,项目中决定封装成Python扩展方便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