使用C/C++ 擴展Python

前期的網頁抽取算法使用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']}}對象

?
1
2
3
4
5
6
7
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結尾。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PyObject* echo(PyObject* self, PyObject* args)
{
        char* input = NULL;
        if(!PyArg_ParseTuple(args, "s", &input))
        {
                printf("parse arg error\n");
                return NULL;
        }
 
        int count = 0;
        do
        {
                printf("%s\n", 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可以做相應處理。

?
1
2
3
4
PyMODINIT_FUNC initecho()
{
        Py_InitModule("echo", EchoMethods);
}

三、編譯與使用

1.如何編譯、分發、使用

上面這些代碼當然會用到python-devel庫。編譯的時候使用GCC直接編譯成一般的so,就可以直接在python裏面調用了。Python會自己選擇如何加載這個so。

?
1
2
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處理起來像這樣:

?
1
2
3
4
5
6
7
8
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則定義安裝的規則及擴展的一些屬性。

使用的時候執行下面兩個命令就可以了。

?
1
2
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的總共也沒幾個函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章