使用C/C++擴展Python

使用C/C++擴展Python
翻譯:
gashero
如果你會用C,實現Python嵌入模塊很簡單。利用擴展模塊可做很多Python不方便做的事情,他們可以直接調用C庫和系統調用。
爲了支持擴展,Python API定義了一系列函數、宏和變量,提供了對Python運行時系統的訪問支持。Python的C API由C源碼組成,幷包含  “Python.h” 頭文件。
編寫擴展模塊與你的系統相關,下面會詳解。
目錄

1   一個簡單的例子

下面的例子創建一個叫做 “spam” 的擴展模塊,調用C庫函數 system()  。這個函數輸入一個NULL結尾的字符串並返回整數,可供Python調用方式如下:
>>> import spam
>>> status=spam.system("ls -l")
一個C擴展模塊的文件名可以直接是 模塊名.c 或者是 模塊名module.c  。第一行應該導入頭文件:
#include 
這會導入Python API。
Warning
因爲Python含有一些預處理定義,所以你必須在所有非標準頭文件導入之前導入Python.h 。
Python.h中所有用戶可見的符號都有 Py 或 PY 的前綴,除非定義在標準頭文件中。爲了方便
“Python.h”
也包含了一些常用的標準頭文件,包括,,,。如果你的系統沒有後面的頭文件,則會直接定義函數 malloc() 、 free() 和 realloc() 。
下面添加C代碼到擴展模塊,當調用 “spam.system(string)” 時會做出響應:
static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=system(command);
    return Py_BuildValue("i",sts);
}


調用方的Python只有一個命令參數字符串傳遞到C函數。C函數總是有兩個參數,按照慣例分別叫做 self 和  args 。
self 參數僅用於用C實現內置方法而不是函數。本例中, self  總是爲NULL,因爲我們定義的是個函數,不是方法。這一切都是相同的,所以解釋器也就不需要刻意區分兩種不同的C函數。
args
參數是一個指向Python的tuple對象的指針,包含參數。每個tuple子項對應一個調用參數。這些參數也全都是Python對象,所以需要先轉換
成C值。函數 PyArg_ParseTuple() 檢查參數類型並轉換成C值。它使用模板字符串檢測需要的參數類型。
PyArg_ParseTuple()  正常返回非零,並已經按照提供的地址存入了各個變量值。如果出錯(零)則應該讓函數返回NULL以通知解釋器出錯。

2   關於錯誤和異常

一個常見慣例是,函數發生錯誤時,應該設置一個異常環境並返回錯誤值(NULL)。異常存儲在解釋器靜態全局變量中,如果爲NULL,則沒有發生異
常。異常的第一個參數也需要保存在靜態全局變量中,也就是raise的第二個參數。第三個變量包含棧回溯信息。這三個變量等同於Python變量
sys.exc_type 、 sys.exc_value 、 sys.exc_traceback 。這對找到錯誤是很必要的。
Python API中定義了一些函數來設置這些變量。
最常用的就是 PyErr_SetString() 。參數是異常對象和C字符串。異常對象一般由像  PyExc_ZeroDivisionError 這樣的對象來預定義。C字符串指明異常原因,並最終存儲在異常的第一個參數裏面。
另一個有用的函數是 PyErr_SetFromErrno() ,僅接受一個異常對象,異常描述包含在全局變量 errno
中。最通用的函數還是 PyErr_SetObject() ,包含兩個參數,分別爲異常對象和異常描述。你不需要使用 Py_INCREF()
來增加傳遞到其他函數的參數對象的引用計數。
你可以通過 PyErr_Occurred()  獲知當前異常,返回當前異常對象,如果確實沒有則爲NULL。一般來說,你在調用函數時不需要調用 PyErr_Occurred()  檢查是否發生了異常,你可以直接檢查返回值。
如果調用更下層函數時出錯了,那麼本函數返回NULL表示錯誤,並且整個調用棧中只要有一處調用 PyErr_*()  函數設置異常就可以。一般來說,首先發現錯誤的函數應該設置異常。一旦這個錯誤到達了Python解釋器的主循環,則會中斷當前執行代碼並追究異常。
有一種情況下,模塊可能依靠其他 PyErr_*()  函數給出更加詳細的錯誤信息,並且是正確的。但是按照一般規則,這並不重要,很多操作都會因爲種種原因而掛掉。
想要忽略這些函數設置的異常,異常情況必須明確的使用 PyErr_Clear()  來清除。只有在C代碼想要自己處理異常而不是傳給解釋器時才這麼做。
每次失敗的 malloc()  調用必須拋出一個異常,直接調用 malloc() 或 realloc() 的地方要調用  PyErr_NoMemory() 並返回錯誤。所有創建對象的函數都已經實現了這個異常的拋出,所以這是每個分配內存都要做的。
還要注意的是 PyArg_ParseTuple()  系列函數的異常,返回一個整數狀態碼是有效的,0是成功,-1是失敗,有如Unix系統調用。
最後,小心垃圾情理,也就是 Py_XDECREF() 和 Py_DECREF()  的調用,會返回的異常。
選擇拋出哪個異常完全是你的個人愛好了。有一系列的C對象代表了內置Python異常,例如 PyExc_ZeroDivisionError
,你可以直接使用。當然,你可能選擇更合適的異常,不過別使用 PyExc_TypeError 告知文件打開失敗(有個更合適的
PyExc_IOError )。如果參數列表有誤, PyArg_ParseTuple() 通常會拋出 PyExc_TypeError
。如果參數值域有誤, PyExc_ValueError 更合適一些。
你也可以爲你的模塊定義一個唯一的新異常。需要在文件前部聲明一個靜態對象變量,如:
static PyObject* SpamError;
然後在模塊初始化函數(initspam())裏面初始化它,並省卻了處理:
PyMODINIT_FUNC
initspam(void) {
    PyObject* m;
    m=Py_InitModule("spam",SpamMethods);
    if (m==NULL)
        return NULL;
    SpamError=PyErr_NewException("spam.error",NULL,NULL);
    Py_INCREF(SpamError);
    PyModule_AddObject(m,"error",SpamError);
}

注意實際的Python異常名字是 spam.error 。 PyErr_NewException()  函數使用Exception爲基類創建一個類(除非是使用另外一個類替代NULL)。
同樣注意的是創建類保存了SpamError的一個引用,這是有意的。爲了防止被垃圾回收掉,否則SpamError隨時會成爲野指針。
一會討論 PyMODINIT_FUNC 作爲函數返回類型的用法。

3   回到例子

回到前面的例子,你應該明白下面的代碼:
if (!PyArg_ParseTuple(args,"s",&command))
    return NULL;
就是爲了報告解釋器一個異常。如果執行正常則變量會拷貝到本地,後面的變量都應該以指針的方式提供,以方便設置變量。本例中的command會被聲明爲  “const char* command” 。
下一個語句使用UNIX系統函數system(),傳遞給他的參數是剛纔從 PyArg_ParseTuple() 取出的:
sts=system(command);
我們的 spam.system() 函數必須返回一個PY對象,這可以通過  Py_BuildValue() 來完成,其形式與 PyArg_ParseTuple()  很像,獲取格式字符串和C值,並返回新的Python對象:
return Py_BuildValue("i",sts);
在這種情況下,會返回一個整數對象,這個對象會在Python堆裏面管理。
如果你的C函數沒有有用的返回值,則必須返回None。你可以用 Py_RETUN_NONE 宏來完成:
Py_INCREF(Py_None);
return Py_None;
Py_None 是一個C名字指定Python對象None。這是一個真正的PY對象,而不是NULL指針。

4   模塊方法表和初始化函數

把函數聲明爲可以被Python調用,需要先定義一個方法表:
static PyMethodDef SpamMethods[]= {
    ...
    {"system",spam_system,METH_VARARGS,
    "Execute a shell command."},
    ...
    {NULL,NULL,0,NULL}    /*必須的結束符*/
};

注意第三個參數 METH_VARARGS ,這個標誌指定會使用C的調用慣例。可選值有  METH_VARARGS 、 METH_VARARGS | METH_KEYWORDS 。值0代表使用  PyArg_ParseTuple() 的陳舊變量。
如果單獨使用 METH_VARARGS ,函數會等待Python傳來tuple格式的參數,並最終使用  PyArg_ParseTuple() 進行解析。
METH_KEYWORDS 值表示接受關鍵字參數。這種情況下C函數需要接受第三個 PyObject*  對象,表示字典參數,使用 PyArg_ParseTupleAndKeywords() 來解析出參數。
方法表必須傳遞給模塊初始化函數。初始化函數函數名規則爲 initname() ,其中 name  爲模塊名。並且不能定義爲文件中的static函數:
PyMODINIT_FUNC
initspam(void) {
    (void) Py_InitModule("spam",SpamMethods);
}
注意 PyMODINIT_FUNC 聲明瞭void爲返回類型,還有就是平臺相關的一些定義,如C++的就要定義成  extern “C” 。
Python程序首次導入這個模塊時就會調用initspam()函數。他調用 Py_InitModule()
來創建一個模塊對象,同時這個模塊對象會插入到 sys.modules 字典中的 “spam” 鍵下面。然後是插入方法表中的內置函數到
“spam” 鍵下面。 Py_InitModule()
返回一個指針指向剛創建的模塊對象。他是有可能發生嚴重錯誤的,也有可能在無法正確初始化時返回NULL。
當嵌入Python時, initspam() 函數不會自動被調用,除非在入口處的  _PyImport_Inittab 表。最簡單的初始化方法是在 Py_Initialize() 之後靜態調用  initspam() 函數:
int
main(int argc, char* argv[]) {
    Py_SetProgramName(argv[0]);
    Py_Initialize();
    initspam();
    //...
}
在Python發行版的 Demo/embed/demo.c 中有可以參考的源碼。
Note
從 sys.modules
中移除模塊入口,或者在多解釋器環境中導入編譯模塊,會導致一些擴展模塊出錯。擴展模塊作者應該特別注意初始化內部數據結構。同時要注意
reload() 函數可能會被用在擴展模塊身上,並調用模塊初始化函數,但是對動態狀如對象(動態鏈接庫),卻不會重新載入。
更多關於模塊的現實的例子包含在Python源碼包的Modules/xxmodule.c中。這些文件可以用作你的代碼模板,或者學習。腳本  modulator.py  包含在源碼發行版或Windows安裝中,提供了一個簡單的GUI,用來聲明需要實現的函數和對象,並且可以生成供填入的模板。腳本在 Tools/modulator/  目錄。查看README以瞭解用法。

5   編譯和連接

如果使用動態載入,細節依賴於系統,查看關於構建擴展模塊部分,和關於在Windows下構建擴展的細節。
如果你無法使用動態載入,或者希望模塊成爲Python的永久組成部分,就必須改變配置並重新構建解釋器。幸運的是,這對UNIX來說很簡單,只要
把你的代碼(例如spammodule.c)放在 Modules/ Python源碼目錄下,然後增加一行到文件
Modules/Setup.local 來描述你的文件即可:
spam spammodule.o
然後重新構建解釋器,使用make。你也可以在 Modules/ 子目錄使用make,但是你接下來首先要重建Makefile文件,使用 make Makefile 命令。這對你改變 Setup 文件來說很重要。
如果你的模塊需要其他擴展模塊連接,則需要在配置文件後面加入,如:
spam spammodule.o -lX11

6   在C中調用Python函數

迄今爲止,我們一直把注意力集中於讓Python調用C函數,其實反過來也很有用,就是用C調用Python函數。這在回調函數中尤其有用。如果一個C接口使用回調,那麼就要實現這個回調機制。
幸運的是,Python解釋器是比較方便回調的,並給標準Python函數提供了標準接口。這裏就不再詳述解析Python代碼作爲輸入的方式,如果有興趣可以參考  Python/pythonmain.c 中的  -c 命令代碼。
調用Python函數,首先Python程序要傳遞Python函數對象。當調用這個函數時,用全局變量保存Python函數對象的指針,還要調用  Py_INCREF() 來增加引用計數,當然不用全局變量也沒什麼關係。例如如下:
static PyObject* my_callback=NULL;
static PyObject*
my_set_callback(PyObject* dummy, PyObject* args) {
    PyObject* result=NULL;
    PyObject* temp;
    if (PyArg_ParseTuple(args,"O:set_callback",&temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError,"parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);
        Py_XINCREF(my_callback);
        my_callback=temp;
        Py_INCREF(Py_None);
        result=Py_None;
    }
    return result;
}

這個函數必須使用 METH_VARARGS 標誌註冊到解釋器。宏 Py_XINCREF() 和  Py_XDECREF() 增加和減少對象的引用計數。
然後,就要調用函數了,使用 PyEval_CallObject()
。這個函數有兩個參數,都是指向Python對象:Python函數和參數列表。參數列表必須總是tuple對象,如果沒有參數則要傳遞空的tuple。
使用 Py_BuildValue() 時,在圓括號中的參數會構造成tuple,無論有沒有參數,如:
int arg;
PyObject* arglist;
PyObject* result;
//...
arg=123;
//...
arglist=Py_BuildValue("(i)",arg);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);
PyEval_CallObject() //返回一個Python對象指針表示返回值。  PyEval_CallObject() 是 引用計數無關  的,有如例子中,參數列表對象使用完成後就立即減少引用計數了。`PyEval_CallObject() //返回一個Python對象指針表示返回值。  PyEval_CallObject() 是 引用計數無關  的,有如例子中,參數列表對象使用完成後就立即減少引用計數了。
PyEval_CallObject()

的返回值總是新的,新建對象或者是對已有對象增加引用計數。所以你必須獲取這個對象指針,在使用後減少其引用計數,即便是對返回值沒有興趣也要這麼做。但

是在減少這個引用計數之前,你必須先檢查返回的指針是否爲NULL。如果是NULL,則表示出現了異常並中止了。如果沒有處理則會向上傳遞並最終顯示調用
棧,當然,你最好還是處理好異常。如果你對異常沒有興趣,可以用 PyErr_Clear() 清除異常,例如:
if (result==NULL)
    return NULL;  /*向上傳遞異常*/
//使用result
Py_DECREF(result);
依賴於具體的回調函數,你還要提供一個參數列表到 PyEval_CallObject()
。在某些情況下參數列表是由Python程序提供的,通過接口再傳到回調函數。這樣就可以不改變形式直接傳遞。另外一些時候你要構造一個新的tuple來
傳遞參數。最簡單的方法就是 Py_BuildValue() 函數構造tuple。例如,你要傳遞一個事件對象時可以用:
PyObject* arglist;
//...
arglist=Py_BuildValue("(l)",eventcode);
result=PyEval_CallObject(my_callback,arglist);
Py_DECREF(arglist);
if (result==NULL)
    return NULL;  /*一個錯誤*/
/*使用返回值*/
Py_DECREF(result);

注意 Py_DECREF(arglist) 所在處會立即調用,在錯誤檢查之前。當然還要注意一些常規的錯誤,比如  Py_BuildValue() 可能會遭遇內存不足等等。

7   解析傳給擴展模塊函數的參數

函數 PyArg_ParseTuple() 聲明如下:
int PyArg_ParseTuple(PyObject* arg, char* format, ...);
參數 arg 必須是一個tuple對象,包含傳遞過來的參數, format  參數必須是格式化字符串,語法解釋見 “Python C/API” 的5.5節。剩餘參數是各個變量的地址,類型要與格式化字符串對應。
注意 PyArg_ParseTuple()  會檢測他需要的Python參數類型,卻無法檢測傳遞給他的C變量地址,如果這裏出錯了,可能會在內存中隨機寫入東西,小心。
任何Python對象的引用,在調用者這裏都是 借用的引用 ,而不增加引用計數。
一些例子:
int ok;
int i,j;
long k,l;
const char* s;
int size;
ok=PyArg_ParseTuple(args,"");
/* python call: f() */
ok=PyArg_ParseTuple(args,"s",&s);
/* python call: f('whoops!') */
ok=PyArg_ParseTuple(args,"lls",&k,&l,&s);
/* python call: f(1,2,'three') */
ok=PyArg_ParseTuple(args,"(ii)s#",&i,&j,&s,&size);
/* python call: f((1,2),'three') */
{
    const char* file;
    const char* mode="r";
    int bufsize=0;
    ok=PyArg_ParseTuple(args,"s|si",&file,&mode,&bufsize);
    /* python call:
        f('spam')
        f('spam','w')
        f('spam','wb',100000)
    */
}
{
    int left,top,right,bottom,h,v;
    ok=PyArg_ParseTuple(args,"((ii)(ii))(ii)",
        &left,&top,&right,&bottom,&h,&v);
    /* python call: f(((0,0),(400,300)),(10,10)) */
}
{
    Py_complex c;
    ok=PyArg_ParseTuple(args,"D:myfunction",&c);
    /* python call: myfunction(1+2j) */
}

8   解析傳給擴展模塊函數的關鍵字參數

函數 PyArg_ParseTupleAndKeywords() 聲明如下:
int PyArg_ParseTupleAndKeywords(PyObject* arg, PyObject* kwdict, char* format, char* kwlist[],...);
參數arg和format定義同 PyArg_ParseTuple() 。參數 kwdict
是關鍵字字典,用於接受運行時傳來的關鍵字參數。參數 kwlist
是一個NULL結尾的字符串,定義了可以接受的參數名,並從左到右與format中各個變量對應。如果執行成功
PyArg_ParseTupleAndKeywords() 會返回true,否則返回false並拋出異常。
Note
嵌套的tuple在使用關鍵字參數時無法生效,不在kwlist中的關鍵字參數會導致  TypeError 異常。
如下是使用關鍵字參數的例子模塊,作者是 Geoff Philbrick (
[email protected]
):
#include "Python.h"
static PyObject*
keywdarg_parrot(PyObject* self, PyObject* args, PyObject* keywds) {
    int voltage;
    char* state="a stiff";
    char* action="voom";
    char* type="Norwegian Blue";
    static char* kwlist[]={"voltage","state","action","type",NULL};
    if (!PyArg_ParseTupleAndKeywords(args,keywds,"i|sss",kwlist,
            &voltage,&state,&action,&type))
        return NULL;
    printf("-- This parrot wouldn't %s if you put %i Volts through it.n",action,voltage);
    printf("-- Lovely plumage, the %s -- It's %s!n",type,state);
    Py_INCREF(Py_None);
    return Py_None;
}
static PyMethodDef keywdary_methods[]= {
    /*注意PyCFunction,這對需要關鍵字參數的函數很必要*/
    {"parrot",(PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,"Print a lovely skit to standard output."},
    {NULL,NULL,0,NULL}
};
void
initkeywdarg(void) {
    Py_InitModule("keywdarg",keywdarg_methods);
}

9   構造任意值

這個函數聲明與 PyArg_ParseTuple() 很相似,如下:
PyObject* Py_BuildValue(char* format, ...);
接受一個格式字符串,與 PyArg_ParseTuple()  相同,但是參數必須是原變量的地址指針。最終返回一個Python對象適合於返回給Python代碼。
一個與 PyArg_ParseTuple() 的不同是,後面可能需要的要求返回一個tuple,比如用於傳遞給其他Python函數以參數。
Py_BuildValue()
並不總是生成tuple,在多於1個參數時會生成tuple,而如果沒有參數則返回None,一個參數則直接返回該參數的對象。如果要求強制生成一個長度
爲空的tuple,或包含一個元素的tuple,需要在格式字符串中加上括號。
例如:
代碼
返回值
Py_BuildValue(”")
None
Py_BuildValue(”i”,123)
123
Py_BuildValue(”iii”,123,456,789)
(123,456,789)
Py_BuildValue(”s”,”hello”)
‘hello’
Py_BuildValue(”ss”,”hello”,”world”)
(’hello’, ‘world’)
Py_BuildValue(”s#”,”hello”,4)
‘hell’
Py_BuildValue(”()”)
()
Py_BuildValue(”(i)”,123)
(123,)
Py_BuildValue(”(ii)”,123,456)
(123,456)
Py_BuildValue(”(i,i)”,123,456)
(123,456)
Py_BuildValue(”[i,i]”,123,456)
[123,456]
Py_BuildValue(”{s:i,s:i}”,’a',1,’b',2)
{’a':1,’b':2}
Py_BuildValue(”((ii)(ii))(ii)”,1,2,3,4,5,6)
(((1,2),(3,4)),(5,6))

10   引用計數

在C/C++語言中,程序員負責動態分配和回收堆(heap)當中的內存。這意味着,我們在C中編程時必須面對這個問題。
每個由 malloc() 分配的內存塊,最終都要由 free() 扔到可用內存池裏面去。而調用 free()
的時機非常重要,如果一個內存塊忘了 free() 則是內存泄漏,程序結束前將無法重新使用。而如果對同一內存塊 free()
了以後,另外一個指針再次訪問,則叫做野指針。這同樣會導致嚴重的問題。
內存泄露往往發生在一些並不常見的程序流程上面,比如一個函數申請了資源以後,卻提前返回了,返回之前沒有做清理工作。人們經常忘記釋放資源,尤其
對於後加新加的代碼,而且會長時間都無法發現。這些函數往往並不經常調用,而且現在大多數機器都有龐大的虛擬內存,所以內存泄漏往往在長時間運行的進程,
或經常被調用的函數中才容易發現。所以最好有個好習慣加上代碼約定來儘量避免內存泄露。
Python往往包含大量的內存分配和釋放,同樣需要避免內存泄漏和野指針。他選擇的方法就是 引用計數  。其原理比較簡單:每個對象都包含一個計數器,計數器的增減與引用的增減直接相關,當引用計數爲0時,表示對象已經沒有存在的意義了,就可以刪除了。
一個叫法是 自動垃圾回收 ,引用計數是一種垃圾回收方法,用戶必須要手動調用 free()
函數。優點是可以提高內存使用率,缺點是C語言至今也沒有一個可移植的自動垃圾回收器。引用計數卻可以很好的移植,有如C當中的 malloc() 和
free() 一樣。也許某一天會出現C語言餓自動垃圾回收器,不過在此之前我們還得用引用計數。
Python使用傳統的引用計數實現,不過他包含一個循環引用探測器。這允許應用不需要擔心的直接或間接的創建循環引用,而這實際上是引用計數實現
的自動垃圾回收的致命缺點。循環引用指對象經過幾層引用後回到自己,導致了其引用計數總是不爲0。傳統的引用計數實現無法解決循環引用的問題,儘管已經沒
有其他外部引用了。
循環引用探測器可以檢測出垃圾回收中的循環並釋放其中的對象。只要Python對象有 __del__()  方法,Python就可以通過 gc module 模塊來自動暴露出循環引用。gc模塊還提供 collect()  函數來運行循環引用探測器,可以在配置文件或運行時禁用循環應用探測器。
循環引用探測器作爲一個備選選項,默認是打開的,可以在構建時使用 –without-cycle-gc 選項加到  configure 上來配置,或者移除 pyconfig.h 文件中的 WITH_CYCLE_GC  宏定義。在循環引用探測器禁用後,gc模塊將不可用。

10.1   Python中的引用計數

有兩個宏 Py_INCREF(x) 和 Py_DECREF(x) 用於增減引用計數。  Py_DECREF() 同時會在引用計數爲0時釋放對象資源。爲了靈活性,他並不是直接調用 free()  而是調用對象所在類型的析構函數。
一個大問題是何時調用 Py_INCREF(x) 和 Py_DECREF(x)  。首先介紹一些術語。沒有任何人都不會 擁有  一個對象,只能擁有其引用。對一個對象的引用計數定義了引用數量。擁有的引用,在不再需要時負責調用 Py_DECREF()  來減少引用計數。傳遞引用計數有三種方式:傳遞、存儲和調用 Py_DECREF() 。忘記減少擁有的引用計數會導致內存泄漏。
同樣重要的一個概念是 借用 一個對象,借用的對象不能調用 Py_DECREF()  來減少引用計數。借用者在不需要借用時,不保留其引用就可以了。應該避免擁有者釋放對象之後仍然訪問對象,也就是野指針。
借用的優點是你無需管理引用計數,缺點是可能被野指針搞的頭暈。借用導致的野指針問題常發生在看起來無比正確,但是事實上已經被釋放的對象。
借用的引用也可以用 Py_INCREF()  來改造成擁有的引用。這對引用的對象本身沒什麼影響,但是擁有引用的程序有責任在適當的時候釋放這個擁有。

10.2   擁有規則

一個對象的引用進出一個函數時,其引用計數也應該同時改變。
大多數函數會返回一個對對象擁有的引用。而且幾乎所有的函數其實都會創建一個對象,例如 PyInt_FromLong() 和
Py_BuildValue() ,傳遞一個擁有的引用給接受者。即便不是剛創建的,你也需要接受一個新的擁有引用。一般來說,
PyInt_FromLong() 會維護一個常用值緩存,並且返回緩存項的引用。
很多函數提取一些對象的子對象並傳遞擁有引用,例如 PyObject_GetAttrString() 。另外,小心一些函數,包括:
PyTuple_GetItem() 、 PyList_GetItem() 、 PyDict_GetItem() 和
PyDict_GetItemString() ,他們返回的都是借用的引用。
函數 PyImport_AddModule() 也是返回借用的引用,儘管他實際上創建了對象,只不過其擁有的引用實際存儲在了  sys.modules 中。
當你傳遞一個對象的引用到另外一個函數時,一般來說,函數是借用你的引用,如果他確實需要存儲,則會使用 Py_INCREF()
來變爲擁有引用。這個規則有兩種可能的異常: PyTuple_SetItem() 和 PyList_SetItem()
,這兩個函數獲取傳遞給他的擁有引用,即便是他們執行出錯了。不過 PyDict_SetItem() 卻不是接收擁有的引用。
當一個C函數被py調用時,使用對參數的借用。調用者擁有參數對象的擁有引用。所以,借用的引用的壽命是函數返回。只有當這類參數必須存儲時,纔會使用  Py_INCREF() 變爲擁有的引用。
從C函數返回的對象引用必須是擁有的引用,這時的擁有者是調用者。

10.3   危險的薄冰

有些使用借用的情況會出現問題。這是對解釋器的盲目理解所導致的,因爲擁有者往往提前釋放了引用。
首先而最重要的情況是使用 Py_DECREF() 來釋放一個本來是借用的對象,比如列表中的元素:
void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0); /* BUG! */
}

這個函數首先借用了 list[0] ,然後把 list[1]  替換爲值0,最後打印借用的引用。看起來正確麼,不是!
我們來跟蹤一下 PyList_SetItem()
的控制流,列表擁有所有元素的引用,所以當項目1被替換時,他就釋放了原始項目1。而原始項目1是一個用戶定義類的實例,假設這個類定義包含
__del__() 方法。如果這個類的實例引用計數爲1,處理過程會調用 __del__() 方法。
因爲使用python編寫,所以 __del__() 中可以用任何python代碼來完成釋放工作。替換元素的過程會執行 del list[0] ,即減掉了對象的最後一個引用,然後就可以釋放內存了。
知道問題後,解決方案就出來了:臨時增加引用計數。正確的版本如下:
void
no_bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_INCREF(item);
    PyList_SetItem(list,1,PyInt_FromLong(0L));
    PyObject_Print(item,stdout,0);
    Py_DECREF(item);
}

這是一個真實的故事,舊版本的Python中多處包含這個問題,讓guido花費大量時間研究 __del__()  爲什麼失敗了。
第二種情況的問題出現在多線程中的借用引用。一般來說,python中的多線程之間並不能互相影響對方,因爲存在一個GIL。不過,這可能使用宏
Py_BEGIN_ALLOW_THREADS 來臨時釋放鎖,最後通過宏 Py_END_ALLOW_THREADS
來再申請鎖,這在IO調用時很常見,允許其他線程使用處理器而不是等待IO結束。很明顯,下面的代碼與前面的問題相同:
void
bug(PyObject* list) {
    PyObject* item=PyList_GetItem(list,0);
    Py_BEGIN_ALLOW_THREADS
    //一些IO阻塞調用
    Py_END_ALLOW_THREADS
    PyObject_Print(item,stdout,0); /*BUG*/
}

10.4   NULL指針

一般來說,函數接受的參數並不希望你傳遞一個NULL指針進來,這會出錯的。函數的返回對象引用返回NULL則代表發生了異常。這是Python的機制,畢竟,一個函數如果執行出錯了,那麼也沒有必要多解釋了,浪費時間。(注:彪悍的異常也不需要解釋)
最好的測試NULL的方法就是在代碼裏面,一個指針如果收到了NULL,例如 malloc()  或其他函數,則表示發生了異常。
宏 Py_INCREF() 和 Py_DECREF() 並不檢查NULL指針,不過還好,  Py_XINCREF() 和 Py_XDECREF() 會檢查。
檢查特定類型的宏,形如 Pytype_Check() 也不檢查NULL指針,因爲這個檢查是多餘的。
C函數的調用機制確保傳遞的參數列表(也就是args參數)用不爲NULL,事實上,它總是一個tuple。
而把NULL扔到Python用戶那裏可就是一個非常嚴重的錯誤了。

11   使用C++編寫擴展

有時候需要用C++編寫Python擴展模塊。不過有一些嚴格的限制。如果Python解釋器的主函數是使用C編譯器編譯和連接的,那麼全局和靜態
對象的構造函數將無法使用。而主函數使用C++編譯器時則不會有這個問題。被Python調用的函數,特別是模塊初始化函數,必須聲明爲 extern “C” 。沒有必要在Python頭文件中使用 extern “C” 因爲在使用C++編譯器時會自動加上 __cplusplus  這個定義,而一般的C++編譯器一般都會設置這個符號。

12   提供給其他模塊以C  API

很多模塊只是提供給Python使用的函數和新類型,但是偶爾也有可能被其他擴展模塊所調用。例如一個模塊實現了 “collection”
類型,可以像list一樣工作而沒有順序。有如標準Python中的list類型一樣,提供的C接口可以讓擴展模塊創建和管理list,這個新的類型也需
要有C函數以供其他擴展模塊直接管理。
初看這個功能可能以爲很簡單:只要寫這些函數就行了(不需要聲明爲靜態),提供適當的頭文件,並註釋C的API。當然,如果所有的擴展模塊都是靜態
鏈接到Python解釋器的話,這當然可以正常工作。但是當其他擴展模塊是動態鏈接庫時,定義在一個模塊中的符號,可能對另外一個模塊來說並不是可見的。
而這個可見性又是依賴操作系統實現的,一些操作系統對Python解釋器使用全局命名空間和所有的擴展模塊(例如Windows),也有些系統則需要明確
的聲明模塊的導出符號表(AIX就是個例子),或者提供一個不同策略的選擇(大多數的Unices)。即便這些符號是全局可見的,擁有函數的模塊,也可能
尚未載入。
爲了可移植性,不要奢望任何符號會對外可見。這意味着模塊中所有的符號都聲明爲 static  ,除了模塊的初始化函數以外,這也是爲了避免各個擴展模塊之間的符號名稱衝突。這也意味着必須以其他方式導出擴展模塊的符號。
Python提供了一種特殊的機制,以便在擴展模塊間傳遞C級別的信息(指針): CObject
。一個CObject是一個Python的數據類型,存儲了任意類型指針(void*)。CObject可以只通過C
API來創建和存取,但是卻可以像其他Python對象那樣來傳遞。在特別的情況下,他們可以被賦予一個擴展模塊命名空間內的名字。其他擴展模塊隨後可以
導入這個模塊,獲取這個名字的值,然後得到CObject中保存的指針。
通過CObject有很多種方式導出擴展模塊的C API。每個名字都可以得到他自己的CObject,或者可以把所有的導出C  API放在一個CObject指定的數組中來發布。所以可以有很多種方法導出C API。
如下的示例代碼展示了把大部分的重負載任務交給擴展模塊,作爲一個很普通的擴展模塊的例子。他保存了所有的C
API的指針到一個數組中,而這個數組的指針存儲在CObject中。對應的頭文件提供了一個宏以管理導入模塊和獲取C
API的指針,客戶端模塊只需要在存取C API之前執行這個宏就可以了。
這個導出模塊是修改自1.1節的spam模塊。函數 spam.system() 並不是直接調用C庫的函數 system() ,而是調用
PySpam_System() ,提供了更加複雜的功能。這個函數 PySpam_System() 同樣導出供其他擴展模塊使用。
函數 PySpam_System() 是一個純C函數,聲明爲static如下:
static int
PySpam_System(const char* command) {
    return system(command);
}
函數 spam_system() 做了細小的修改:
static PyObject*
spam_system(PyObject* self, PyObject* args) {
    const char* command;
    int sts;
    if (!PyArg_ParseTuple(args,"s",&command))
        return NULL;
    sts=PySpam_System(command);
    return Py_BuildValue("i",sts);
}
在模塊的頭部加上如下行:
#include "Python.h"
另外兩行需要添加的是:
#define SPAM_MODULE
#include "spammodule.h"
這個宏定義是告訴頭文件需要作爲導出模塊,而不是客戶端模塊。最終模塊的初始化函數必須管理初始化C API指針數組的初始化:
PyMODINIT_FUNC
initspam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;
    m = Py_InitModule("spam", SpamMethods);
    if (m == NULL)
        return;
    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
    /* Create a CObject containing the API pointer array's address */
    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);
    if (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
}


注意 PySpam_API 聲明爲static,否則 initspam()  函數執行之後,指針數組就消失了。
大部分的工作還是在頭文件 spammodule.h 中,如下:
#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System 
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 and set exception on error, 0 on success. */
static int
import_spam(void)
{
    PyObject *module = PyImport_ImportModule("spam");
    if (module != NULL) {
        PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API");
        if (c_api_object == NULL)
            return -1;
        if (PyCObject_Check(c_api_object))
            PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object);
        Py_DECREF(c_api_object);
    }
    return 0;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */


想要調用 PySpam_System() 的客戶端模塊必須在初始化函數中調用  import_spam() 以初始化導出擴展模塊:
PyMODINIT_FUNC
initclient(void) {
    PyObject* m;
    m=Py_InitModule("client",ClientMethods);
    if (m==NULL)
        return;
    if (import_spam()
這樣做的缺點是 spammodule.h 有點複雜。不過這種結構卻可以方便的用於其他導出函數,所以學着用一次也就好了。
最後需要提及的是CObject提供的一些附加函數,用於CObject指定的內存塊的分配和釋放。詳細信息可以參考Python的C 
 API參考手冊的CObject一節,和CObject的實現,參考文件 Include/cobject.h 和 Objects/cobject.c 。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章