編寫Python擴展(Extending Python with C or C++)

其實這是一篇譯文,看官方文檔的時候覺得不好對重點做標記,加上以後遺忘的時候看中文可以更快速的撿起來,所以在閱讀的過程中就直接翻譯出來記錄在此了,藉助於博客的一些編輯功能對重點做突出表現。

原文是python的官方文檔:http://docs.python.org/3/extending/extending.html

這裏有個大失誤就是看的文檔是3.4的,而我一般使用的Python都是2.6/2.7的,所以實際使用的時候得按照2.7的文檔(http://docs.python.org/2.7/extending/extending.html)來(否則會有錯誤,比如“錯誤:‘PyModuleDef_HEAD_INIT’未聲明”)。

下面是一個基於python2.6.6完整的示例:

[dongsong@localhost spam]$ vpython --version
Python 2.6.6
[dongsong@localhost spam]$ pwd
/home/dongsong/python_study/spam
[dongsong@localhost spam]$ tree -a
.
├── setup.py
└── spammodule.c

0 directories, 2 files
[dongsong@localhost spam]$ cat setup.py 
from distutils.core import setup, Extension

module1 = Extension('spam',
                                        #include_dirs = ['/usr/local/include/python2.6/'],
                                        #libraries = ['/usr/local/lib/'],
                                        sources = ['spammodule.c'])

setup(  name = 'spam',
                version = '1.0',
                description = 'call system()',
                ext_modules = [module1])

[dongsong@localhost spam]$ cat spammodule.c 
#include<Python.h>
//自定義異常
static PyObject* SpamError;
//模塊的功能函數
static PyObject* spam_system(PyObject* self, PyObject* args)
{
        const char* command;
        int sts;
        if (!PyArg_ParseTuple(args, "s", &command))
                return NULL;
        sts = system(command);
        if (sts < 0) {
                PyErr_SetString(SpamError, "System command failed");
                return NULL;
        }
        return PyLong_FromLong(sts);
}
//方法表
static PyMethodDef SpamMethods[] = {
        {"system", spam_system, METH_VARARGS,
        "Execute a shell command."},
        {NULL, NULL, 0, NULL}
};
//模塊的結構體定義 v3.4
/*
static struct PyModuleDef spammodule = {
        PyModuleDef_HEAD_INIT,
        "spam",
        spam_doc,
        -1,
        SpamMethods
};*/
//模塊初始化函數
//PyMODINIT_FUNC PyInit_spam(void) // v3.4
PyMODINIT_FUNC initspam(void)
{
        PyObject* m;
        //m = PyModule_Create(&spammodule); // v3.4
        m = Py_InitModule("spam", SpamMethods);
        if (m == NULL)
                return;
        SpamError = PyErr_NewException("spam.error",NULL,NULL);
        Py_INCREF(SpamError);
        PyModule_AddObject(m,"error",SpamError);
}
[dongsong@localhost spam]$ vpython setup.py build
running build
running build_ext
building 'spam' extension
creating build
creating build/temp.linux-x86_64-2.6
gcc -pthread -fno-strict-aliasing -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -DNDEBUG -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -I/usr/include/python2.6 -c spammodule.c -o build/temp.linux-x86_64-2.6/spammodule.o
creating build/lib.linux-x86_64-2.6
gcc -pthread -shared build/temp.linux-x86_64-2.6/spammodule.o -L/usr/lib64 -lpython2.6 -o build/lib.linux-x86_64-2.6/spam.so
t[dongsong@localhost spam]$ tree -a
.
├── build
│?? ├── lib.linux-x86_64-2.6
│?? │?? └── spam.so
│?? └── temp.linux-x86_64-2.6
│??     └── spammodule.o
├── setup.py
└── spammodule.c

3 directories, 4 files
[dongsong@localhost spam]$ vpython setup.py install
running install
running build
running build_ext
running install_lib
copying build/lib.linux-x86_64-2.6/spam.so -> /home/dongsong/venv/lib64/python2.6/site-packages
running install_egg_info
Removing /home/dongsong/venv/lib64/python2.6/site-packages/spam-1.0-py2.6.egg-info
Writing /home/dongsong/venv/lib64/python2.6/site-packages/spam-1.0-py2.6.egg-info
[dongsong@localhost spam]$ vpython
Python 2.6.6 (r266:84292, Jul 10 2013, 22:48:45) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-3)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import spam
>>> dir(spam)
['__doc__', '__file__', '__name__', '__package__', 'error', 'system']
>>> spam.system('ls -l')
總用量 12
drwxrwxr-x 4 dongsong dongsong 4096 7月  21 15:55 build
-rw-rw-r-- 1 dongsong dongsong  304 7月  21 15:55 setup.py
-rw-rw-r-- 1 dongsong dongsong 1025 7月  21 15:55 spammodule.c
0L
>>> 

1. Extending Python with C or C++

C編寫的擴展模塊源碼文件構成:

1.功能函數 spam_system

2.模塊的方法表(Method Table)SpamMethods

3.模塊的初始化函數(Initialization Function)PyInit_spam

4.模塊定義結構體(definition structure) spammodule

如果會c語言編程的話,往python中添加新的內建模塊(built-in modules)會很簡單。有兩件事情是這些擴展模塊可以做而直接用python無法做的:實現新的內建對象類型(built-in object types);調用c庫函數和系統調用。

爲了支持擴展,Python API(Application Programmers Interface)定義了一批函數、宏和變量供我們訪問Python運行時系統(Python run-time system)的大部分方面。include頭文件“Python.h”就可以把Python的API引入到源文件裏面來。

如果你的用例要調用C庫函數或者系統調用,你應該考慮使用ctypes模塊而不是編寫自定義的c代碼。ctypes不僅可以讓你的python代碼與C代碼交互,而且移植性更好。

1.1 A Simple Example

讓我們創建一個叫spam的擴展模塊,並且我們說我們要創建一個調用C庫函數system的Python接口。我們要這個函數可以從Python中像下面這樣調用:

>>>import spam
>>>status = spam.system("ls -l")

根據歷史規律(約定成俗),如果一個模塊名字是spam,那麼實現它的c源碼文件一般是spammodule.c;如果模塊名字很長,比如spammify,那麼c源碼文件可以是spammify.c
我們文件的第一行是:
#include <Python.h>
這條語句引入Python API.
注意:因爲python可能要定義一些預處理器的定義(影響一些系統的標準頭文件),所以我們應該在include任何標準頭文件之前include Python.h
Python.h中定義的所有用戶可見的符號都有個前綴Py或者PY,除了那些定義在標準頭文件裏面的。Python.h include了少量幾個標準頭文件(stdio.h, string.h, errno.h和stdlib.h),一則爲了方便,二則因爲這些標準頭文件在python解釋器中被廣泛的使用。如果這些頭文件在你的系統中不存在,它(Python.h?)會直接聲明malloc(),free()和realloc()。
下一步我們添加到我們的模塊文件中來的是C函數,這個函數會在Python語句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 PyLong_FromLong(sts);
}

這裏有一個從Python參數列表(例如“ls -l”)到C函數參數的直觀翻譯。C函數(spam_system)一般有兩個參數,按照習俗叫做self和args.

self參數,對於模塊級別(module-level)的函數,self指向模塊對象;對於一個方法,它指向對象實例。

arg參數,它是一個指向Python元組對象的指針,這個元組對象裏面是參數。元組的每個元素對應着函數調用的參數列表中的一個參數。參數是Python對象---爲了在我們的C函數裏面用它們做點事兒,我們必須要把他們轉換成C數據。Python API中的函數PyArg_ParseTuple()會檢查參數類型並把他們轉換成C數據。它使用一個模板字符串來確定參數需要的類型(也就是要存儲轉換後的數據的C變量的類型)。更多細節稍後再說。

PyArg_ParseTuple()返回true:如果所有參數類型正確並且數據存儲到了傳入的對應的變量上(變量地址已經傳進去了)。PyArg_ParseTuple()返回false(zero):如果一個無效的參數列表被傳進去。後面這種情況,會拋出一個對應的異常,所以被調用的函數會立馬返回NULL(就像我們在例子中看到的一樣)。

1.2. Intermezzo: Errors and Exceptions

Python中的一個重要約定是這樣的:當一個函數失敗的時候,Python解釋器設置一個異常條件(環境)並返回錯誤值(一般是空指針)。異常被存儲在解釋器內部的一個靜態全局變量上;如果這個變量爲空(NULL)就表明沒有異常發生。再用另一個全局變量存儲異常對應的信息(raise的第二個參數)。還有第三個變量存放導致錯誤發生的python代碼的調用棧的回溯(stack traceback)。C中的這三個變量等效於Python中sys.exc_info()的返回值。瞭解這些對於理解錯誤的傳遞非常重要。

PyErr_SetString(),最常用的設置異常的函數。參數是異常對象和一個C字符串。異常對象一般是預定義對象,比如PyExc_ZeroDivisionError. C字符串是錯誤的原因,它被轉換成一個Python的字符串對象並作爲異常對應的信息存儲起來("asociated value" of the exception)。

另一個很有用的函數是PyErr_SetFromErrno(),只需要一個異常參數,通過檢查全局變量errno構建異常對應的信息。最普遍的函數是PyErr_SetObject(),需要兩個對象參數,一個是異常另一個是對應的信息。我們不需要對傳入這些函數的對象調用Py_INCREF()

PyErr_Occurred()可以檢測以否已經有異常被設置了。它返回當前異常獨享或者NULL(如果還沒有異常發生)。一般來講,我們不需要調用這個函數來看是否有錯誤發生,因爲返回值(函數返回值)會告訴我們。

當一個函數f調用另一個函數g並檢測到後者失敗了,f應該返回一個錯誤值(一般是NULL或者-1)。f不再調用PyErr_*()之類的函數---因爲g已經調過了---f的調用者也返回一個錯誤值而不調用PyErr_*()之類的函數,以此類推---最詳細的錯誤原因已經在第一個檢測到它的函數裏面做了彙報了。當錯誤到大Python解釋器的主循環(main loop),就會終止當前正在執行的python代碼並試圖找到一個Python程序員設置異常處理(an exception handler)。

要忽略一個失敗的函數調用設置的異常,異常條件(環境)只能用顯式調用PyErr_Clear()來清除。C代碼只在它不想把錯誤傳遞給解釋器而想完全靠自己處理它(可能要嘗試其他事情或者假裝沒有錯誤發生)的時候才調用PyErr_Clear()。

每個失敗的malloc()調用必須返回一個異常---malloc()(或者realloc())的直接調用者(direct caller)必須調用PyErr_NoMemory()並返回一個失敗標示(錯誤碼)。所有的對象創建函數(比如,PyLong_FromLong())已經做過這種處理了,所以這個規則是針對那些直接調用malloc()的情況而言的。

注意,對於PyArg_ParseTuple()和類似函數(friends)的異常,返回一個整數狀態值的函數一般會在成功的時候返回一個整數或者0,在失敗的時候返回-1,跟Unix系統調用類似。

最後,當返回錯誤標識的時候要小心清理垃圾(對我們已創建的對象調用Py_XDECREF()或者Py_DECREF()).

拋出哪個異常完全由我們決定。跟所有的內建Python異常相對應的C對象都已經預先聲明好了,比如PyExc_ZeroDivisionError,這些我們可以直接使用。當然,我們應該明智的選擇異常---不要使用PyExc_TypeError來表示有文件打不開(用PyExc_IOError纔對)。如果參數列表有錯,PyArg_ParseTuple()函數一般會拋出PyExc_TypeError。如果我們有一個數值必須在一個特定範圍內或者必須符合某些條件的參數,那麼出錯時PyExc_ValueError比較合適。

我們也可以自定義一個新的異常:我們要在文件(模塊的C代碼)開頭出聲明一個靜態對象(PyObject)變量:

static PyObject* SpamError;

然後在模塊的初始化函數(PyInit_spam())中用一個異常對象初始化它。

PyMODINIT_FUNC
PyInit_spam(void)
{
	PyObject *m;
	m = PyModule_Create(&spammodule);
	if (m == NULL)
		return NULL;
	SpamError = PyErr_NewException("spam.error", NULL, NULL);
	Py_INCREF(SpamError);
	PyModule_AddObject(m, "error", SpamError);
	return m;
}
注意,這個異常對象的Python名字是spam.error.PyErr_NewException()函數可以Exception(內建異常的文檔有描述)爲基類(除非另一個非NULL的基類傳遞進去)創建一個類。

還得注意的是,SpamError變量保留了對新創建的異常類的引用;這是故意的!因爲異常可以被其他外部的代碼從這個模塊移除,保留對這個類的引用可以確保它不會被丟棄(導致SpamError變成一個懸空的指針)。如果變成了懸空的指針,拋這個異常的C代碼會導致一個core dump或者其他意想不到的問題。

我們稍後再討論把PyMODINIT_FUNC作爲函數返回類型的用法。

spam.error異常可以在我們的擴展模塊中被拋出:如下所示通過調用PyErr_SetString():

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
	const char *command;
	int sts;
	if (!PyArg_ParseTuple(args, "s", &command))
		return NULL;
	sts = system(command);
	if (sts < 0) {
		PyErr_SetString(SpamError, "System command failed");
		return NULL;
	}
	return PyLong_FromLong(sts);
}

1.3. Back to the Example

回到我們例子中的函數,我們現在可以理解下面這語句了:

if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
如果參數列表檢測到錯誤它返回NULL。否則,參數的字符串數據會被拷貝給局部變量command。這是一個指針賦值,我們不應該修改它所指向的字符串,所以在標準C裏面,變量command應該恰當的聲明爲const char *command.

下一條語句是對Unix函數system()的調用,把我們剛從PyArg_ParseTuple()獲取的字符串傳給它:

sts = system(command);
我們的spam.system()函數必須把sys的值作爲一個Python對象返回去。這個由函數PyLong_FromLong()完成:
return PyLong_FromLong(sts);
在這裏,它會返回一個整數對象(是的,即使是整數,在python中也是堆裏面的對象)。

c模塊提供的函數返回值必須是Python對象,如果C函數不返回有用的值(return void),對應的Python函數應該返回None(可由宏Py_RETURN_NONE代勞):

Py_INCREF(Py_None);
return Py_None;
Py_None是Python的特別對象None在C中的名字,它是一個真實(和空指針不一樣)的對象。
1.4. The Module’s Method Table and Initialization Function

spam_system()怎麼被Python程序調用是一定會說的。只是,首先,我們需要把它的名字和地址列進一個方法表(method table)裏:

static PyMethodDef SpamMethods[] = {
	...
	{"system",  spam_system, METH_VARARGS,
	 "Execute a shell command."},
	...
	{NULL, NULL, 0, NULL}        /* Sentinel */
};
注意,第三個參數(METH_VARARGS),這個標記告訴解釋器該函數調用按照約定會使用C函數。一般情況下這裏會用METH_VARARGS或METH_VARARGS|METH_KEYWORDS, 0表示使用了PyArg_ParseTuple()的一個過時的變體。

使用單純的METH_VARARGS,函數要求Python程序員傳遞元組參數以便通過PyArg_ParseTuple()解析;這個函數的更多的信息下面給出。

如果要傳關鍵字參數給函數就應該把METH_KEYWORDS設置上。這種情況下,C函數應該接受一個關鍵字的字典作爲第三個參數(PyObject*)。用PyArg_ParseTupleAndKeywords()來解析參數。

模塊的結構體定義

static struct PyModuleDef spammodule = {
   PyModuleDef_HEAD_INIT,
   "spam",   /* name of module */
   spam_doc, /* module documentation, may be NULL */
   -1,       /* size of per-interpreter state of the module,
				or -1 if the module keeps state in global variables. */
   SpamMethods
};

模塊的初始化函數

PyMODINIT_FUNC
PyInit_spam(void)
{
	return PyModule_Create(&spammodule);
}

模塊的結構體定義必須傳遞給模塊的初始化函數。初始化函數必須用PyInit_name()的格式命名,name就是模塊的名稱。初始化函數是模塊文件中唯一的非靜態(non-static)條目(在C的源碼中用static可以限定相關條目只可在本文件中被訪問,可見模塊的初始化函數是唯一的出口/對外接口)。

當python程序第一次import模塊spam時模塊的初始化函數PyInit_spam()會被調用。

模塊的初始化函數內部調用PyModule_Create()返回一個module對象,並根據模塊的方法表(一個由PyMethodDef結構體構成的數組)把函數對象插入到這個新建的module。初始化函數需要返回一個modeule對象給它的調用者,然後該module對象會被插入到sys.modules中。

在程序中嵌入Python解釋器時,PyInit_spam()函數不會自動被調用除非在PyImport_Inittab表中有該條目。要把spam模塊添加到初始化表,需要用PyImport_AppendInittab():

int
main(int argc, char *argv[])
{
	/* Add a built-in module, before Py_Initialize */
	PyImport_AppendInittab("spam", PyInit_spam);


	/* Pass argv[0] to the Python interpreter */
	Py_SetProgramName(argv[0]);


	/* Initialize the Python interpreter.  Required. */
	Py_Initialize();


	/* Optionally import the module; alternatively,
	   import can be deferred until the embedded script
	   imports it. */
	PyImport_ImportModule("spam");

Python源碼中Modules/xxmodule.c是一個更可靠的模塊示例,這個文件可以用來作爲一個編寫擴展模塊的模板。

1.5. Compilation and Linkage

編譯成動態加載模塊:

linux下編譯 Building C and C++ Extensions with distutils(http://docs.python.org/3/extending/building.html#building) ---> page down !

windows下編譯 Building C and C++ Extensions on Windows(http://docs.python.org/3/extending/windows.html#building-on-windows)

編譯成Python解釋器固定的部分(不要動態加載):

修改配置重新編譯解釋器
UNIX把我們的文件(比如spammodule.c)放到Python源碼的Modules/目錄下;在Modeules/Setup.local中添加一行"spam spammodule.o";在頂級目錄下運行make.如果我們的模塊需要鏈接其他額外的庫,額外的庫也需要加到前述的那一行中,如"spam spammodule.o -lX11"
1.6. Calling Python Functions from C

目前爲止我們一直專注在如何讓Python調用C函數。反過來,C代碼調用Python函數也是很有意義的。對於那些支持回調函數的庫來講,這個尤其需要。如果一個C接口使用了回調,對應的Python一般需要爲Python程序員提供一個回調機制;而實現則需要用C的回調函數調用Python的回調函數。當然,可以想到,“C代碼調用Python函數”還會有其他的用途。

幸運的是,Python解釋器很容易遞歸調用,並且有一個標準接口來調用Python函數。

調用一個Python函數是很容易的:python程序必須以某種方式給我們傳遞Python函數對象(function object)。我們需要提供一個函數(或者其他什麼接口)來做這個。當我們提供的這個函數被調用時,它把一個指向Python函數對象(對它Py_INCREF()的時候要小心)的指針保存到一個全局變量(a global variable)裏面---或者其他我們覺得合適的地方。例如,下面是一個模塊定義的示例:

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);         /* Add a reference to new callback */
		Py_XDECREF(my_callback);  /* Dispose of previous callback */
		my_callback = temp;       /* Remember new callback */
		/* Boilerplate to return "None" */
		Py_INCREF(Py_None);
		result = Py_None;
	}
	return result;
}

這個函數(my_set_callback)在註冊到解釋器時必須使用METH_VARARGS標記;這個在前面The Module's Method and Initialization Function(http://docs.python.org/3/extending/extending.html#methodtable)裏講過了。PyArg_ParseTuple()函數和它的參數在Extraction Parameters in Extension Functions(http://docs.python.org/3/extending/extending.html#parsetuple)裏有文檔。

宏Py_XINCREF()和Py_XDECREF()用於增加/減少一個對象的引用計數,它們操作空指針也是安全的(上述例子中temp不是空指針)。引用計數的文檔在這裏http://docs.python.org/3/extending/extending.html#refcounts

然後,是時候調用函數了,我們可以調用C函數PyObject_CallObject().這個函數有兩個參數,都是指向任意Python對象的指針:Python函數,和參數列表。參數列表必須是一個元組對象(a tuple object),元組的長度就是參數的個數。要無參調用Python函數,傳遞NULL或者一個空元組進去;要帶參調用,傳遞一個元組進去。Py_BuildValue(),當它的格式化字符串由"括號"和"0或其他字符(格式化符號)"組成時,該函數返回一個元組。例如:

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject()返回一個Python對象指針:這是Python函數的返回值。PyObject_CallObject()對於它的參數而言是“引用計數中性”(reference-count-neutral,我的理解是對傳入內部的對象不加也不減其引用計數)的。在上述例子中,一個新的元組被創建出來作爲參數列表,並且在調用結束後立馬用Py_DECREF()減引用計數。

PyObject_CallObject()的返回值是新的(new):要嘛是一個全新的對象,要嘛是一個已存在的、已增加了引用計數的對象。所以,除非我們要把它保存到全局變量裏面去,否則我們應該對這個結果調用Py_DECREF(),即使我們對結果值不感興趣。

然而,在減引用前,檢查一下這個返回值是否爲NULL也是很重要的。如果爲空,表明Python函數異常終止了。如果調用PyObject_CallObject()的C代碼是被Python程序調用的,那麼C代碼現在應該返回一個錯誤標示給它的Python調用方,這樣python解釋器就可以打印調用棧信息了,或者調用方python代碼可以處理異常。如果PyObject_CallObject()返回值不可能爲空或者不需要關心,那麼應該調用PyErr_Clear()清除異常。例如:

if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
Python回調函數(callback function)如果需要的話,我們可能還得提供一個參數列表給PyObject_CallObject().某些情況下,參數列表也需要由Python程序通過指定回調函數的那個接口(上述的my_set_callback(..)函數)給出。然後跟函數對象一樣被存儲和使用。其他一些情況下,我們需要構建一個新的元組作爲函數列表。最簡單的方式是調用Py_BuildValue().例如,如果我們傳遞一個不可分割的事件代碼(an integral event code),我們可以用下述代碼:
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
注意,Py_DECREF(arglist)的位置緊跟在函數調用後面、在錯誤檢查之前!還要注意的是,嚴格來講這段代碼不完整(健全):Py_BuildValue()可能耗盡內存,而這需要做檢查。

我們也可以通過PyObject_Call()以帶關鍵字參數的形式調用函數,Py_Object_Call()支持參數(位置參數)和關鍵字參數。就像上述例子一樣,我們使用Py_BuildValue()來構建字典:

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
1.7. Extracting Parameters in Extension Functions
在擴展函數裏面提取參數(位置參數)
int PyArg_ParseTuple(PyObject *arg, char *format, ...);

arg必須是一個元組對象,它包含一個參數列表,從Python傳遞給C函數。format必須是一個格式化串,語法解釋見Python/C API 參開手冊中的Parsing arguments and building values(http://docs.python.org/3/c-api/arg.html#arg-parsing).剩餘參數必須是變量地址,變量類型有格式化串決定。

注意,PyArg_ParseTuple()檢查Python參數是否有要求的類型,但不檢查後面那些參數(C變量地址)的有效性:如果我們在這裏犯錯,我們的代碼可能會崩潰或者至少會有內存被非法改寫。所以要小心。

還要注意的是,所有傳給這個函數的Python對象的引用是借出去的;不要減少他們的引用計數。(Note that any Python object references which are provided to the caller are borrowed references; do not decrement their reference count! 到底什麼意思??有歧義啊~)

一些示例如下:

#define PY_SSIZE_T_CLEAN  /* Make "s#" use Py_ssize_t rather than int. */
#include <Python.h>

int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
	/* Python call: f() */

ok = PyArg_ParseTuple(args, "s", &s); /* A string */
	/* Possible Python call: f('whoops!') */

ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
	/* Possible Python call: f(1, 2, 'three') */

ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
	/* A pair of ints and a string, whose size is also returned */
	/* Possible 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);
	/* A string, and optionally another string and an integer */
	/* Possible Python calls:
	   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);
	/* A rectangle and a point */
	/* Possible Python call:
	   f(((0, 0), (400, 300)), (10, 10)) */
}

{
	Py_complex c;
	ok = PyArg_ParseTuple(args, "D:myfunction", &c);
	/* a complex, also providing a function name for errors */
	/* Possible Python call: myfunction(1+2j) */
}

1.8. Keyword Parameters for Extension Functions

在擴展函數裏面提取關鍵字參數

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict, char *format, char *kwlist[], ...);
arg和format參數跟PyArg_ParseTuple()相同。kwdict參數一個關鍵字的字典,是從Python接收到的第三個參數(PyObject* self, Pyobject* args, PyObject* keywds)。kwlist參數是一個以NULL結尾的字符串列表,它標識關鍵字參數(參數名);這些名字跟format中的類型信息從左到右相匹配。如果調用成功,PyArg_ParseTupleAndKeywords()返回true,否則它返回false並拋出一個對應的異常。

注意,使用關鍵字參數時,嵌套的元組不能被解析。傳遞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 keywdarg_methods[] = {
	/* The cast of the function is necessary since PyCFunction values
	 * only take two PyObject* parameters, and keywdarg_parrot() takes
	 * three.
	 */
	{"parrot", (PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
	 "Print a lovely skit to standard output."},
	{NULL, NULL, 0, NULL}   /* sentinel */
};

static struct PyModuleDef keywdargmodule = {
	PyModuleDef_HEAD_INIT,
	"keywdarg",
	NULL,
	-1,
	keywdarg_methods
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
	return PyModule_Create(&keywdargmodule);
}
1.9. Building Arbitrary Values

這是跟PyArg_ParseTuple()類似(地位相當,counterpart)的函數。聲明如下:

PyObject *Py_BuildValue(char *format, ...);

該函數跟PyArg_ParseTuple()一樣會識別一堆的格式化信息,但是參數不能是指針,只能是數值。函數返回一個新的Python對象,適合於從C函數返回給Python調用方。

跟PyArg_ParseTuple()不同的一點:後者要求它的第一個參數是元組(因爲python參數列表在內部總是以元組的形式呈現),而Py_BuildValue()並不總是構建元組。只有當格式化字符串包換兩個或更多個格式化單元(format units)的時候,Py_BuildValue()纔會創建一個元組出來。如果格式化串爲空,它返回None;如果格式化串只包含一個格式化單元,它返回格式化單元描述的那個對象。要強制它返回一個大小爲0或者1的元組,需要用括號把格式化串括起來。

示例(左邊是調用,右邊是Python數據結果):

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'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}",
			  "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
			  1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))
1.10. Reference Counts

在C/C++裏面,程序員需要負責從堆動態申請和歸還內存。在C裏面,用malloc()和free()來完成。在C++裏面,用new和delete有同樣效果,下面我們的討論僅限於C。

用malloc()申請的每塊內存最終都應該調用一次free()把內存歸還給可用內存的池子裏。在正確的時機調用free()是非常重要的。如果一個內存塊的地址被遺忘了並且對應的free()沒有調用,那麼在整個程序結束之前它佔據的內存是不能被重複利用的。這就是內存泄露。另一方面,如果程序對一個內存塊調用了free()卻還是繼續使用這塊內存,那麼在使用另一個malloc()來重用這塊內存時就發生衝突了。這和引用未初始化的數據一樣不會有好結果---core dumps,錯誤的結果,詭異的崩潰。

常見的內存泄露原因是代碼寫得不嚴謹(unusual paths though the code)。舉個例子,一個函數可能申請一塊內存,做一些計算,然後釋放內存。現在函數的需求變了,可能對計算增加一個錯誤條件(an error condition)的檢測並從函數中提前返回。這裏提前返回時就很容忘記釋放申請的內存塊,尤其是當這個返回是後加到代碼裏面去的。這種泄露,一旦引入,經常會令人討厭的延續很長一段時間:錯誤退出只是所有調用中的一小部分,並且現代化的機器都有大量的虛擬內存,所有這種泄露只會在一個長時間運行且頻繁使用內存泄露函數的進程裏暴露出來。所以,從編碼習慣和策略上最小化這種錯誤以防止內存泄露的發生是非常重要的。

因爲Python大量使用malloc()和free(),Python需要一種策略來避免內存泄露和(非法)使用已釋放的內存。引用計數就是被選中的策略。原理很簡單:每個對象包含一個計數,對該對象的引用被存儲到某個地方時對象的計數增加,對該對象的引用被刪除時對象的計數減小。當計數變成0,說明最後的一個對該對象的引用也被刪除了,那麼該對象可以釋放(內存)了。

另一種策略被成爲自動垃圾收集(automatic garbage collection)。(有時,引用計數也被成爲垃圾收集策略,所以我用“自動(automatic)”來區分這兩個策略)。自動垃圾收集策略最大的優點是用戶不用顯式的調用free().(另一種宣稱的優勢是在速度或內存使用上的改善---然而,這個是沒有事實依據的)。自動垃圾收集策略相對於C的劣勢是,很難有移植性好(portable)的自動垃圾收集器,而引用計數可以方便的實現移植性(只要malloc()和free()可用就行了---這是標準C需要保證的)。可能未來某天一個C語言的、具備足夠移植性性的自動垃圾收集器會出現。但目前爲止,我們還是得靠引用計數而生。

Python使用的是傳統引用計數的實現,它還提供了環形檢測(檢測引用出現環)。這使得應用程序不用擔心創建直接或間接的環形引用;這些是單獨使用引用計數來實現垃圾收集的缺點。環形引用由一些引用他們自身(可能不是直接的)的對象構成,所以環上的每個對象有一個非零的引用計數。典型的引用計數的實現無法回收環形引用上任何對象的內存,無法回收被“環上的對象”引用的“對象”的內存,即使已經沒有其他地方引用這些對象了

環形檢測可以檢測垃圾環並回收它們,只要它們沒有實現Python的結束函數(finalizers,我理解的是析構函數)(__del__())。如果存在這些結束函數,檢測器會把環用gc模塊暴露(expose,我理解的是丟出去)出去(具體來講,是gc模塊裏面的garbage變量)。gc模塊也提供了一種運行監測器的方式(collect()函數),也提供了運行時停用檢測器的接口和功能,和配置文件效果一樣。環形檢測器被認爲是一個可選(非必需)組件;雖然它默認被引入(be included),但是在Unix平臺(包含Mac OS X)它是可以通過編譯階段加--without-cycle-gc選項到配置腳本來禁掉的。如果環形檢測器用這種方式禁掉了,gc模塊將不在可用。

1.10.1. Reference Counting in Python

兩個宏(Py_INCREF(x)和Py_DECREF(x))處理引用計數的增加和減小。Py_DECREF()會在引用計數變成零的時候釋放對象。爲了靈活性(For flexibility),它不直接調用call()函數---而是,它通過該對象的類型對象(the object's type object)的一個函數指針來做函數調用。爲了這一點(和其他一些原因),每個對象必須包含一個指向它的類型對象的指針。

現在遺留了一個大問題:什麼時候調用Py_INCREF(x)和Py_DECREF(x)?我們先來介紹幾個術語。沒人擁有(own)一個對象;你可以擁有對對象的一個引用。一個對象的引用計數就是擁有對它的引用的數量。當一個引用不再需要時,引用的擁有者要負責調用Py_DECREF()。引用的擁有關係可以傳遞。有三種方式可以丟掉一個已經擁有的引用:傳遞它,存儲它,或者調用Py_DECREF().忘記丟掉一個已擁有的引用會造成內存泄露

借用(borrow)一個對象的引用也是可以的。借用者不能調用Py_DECREF().借用者持有對象的時間不能比這個引用的擁有者(即被借者)持有對象的時間還長。在引用的擁有者已經丟棄該引用以後,借用者還繼續使用借來的引用有使用已釋放內存的風險,這個應該完全避免。

借用一個引用相對於擁有一個引用的優點是,我們不需要關心丟棄引用的事情---換句話說,用一個借來的引用當函數提前退出的時候我們沒有泄露內存的風險。借用引用的缺點是,在一個微妙的情況下,在看上去正確的代碼裏,一個借來的引用可能已經由被借者把擁有關係掉丟了,而這裏還在拿着它pia~pia~的用。

一個借來的引用可以通過調用Py_INCREF()而轉變成一個擁有的引用。這個不影響被借者的狀態---這種方式會創建一個新的被擁有的的引用,並且給予完整的擁有者權利(新的擁有者也必須像之前的擁有者一樣合理的丟棄引用)。

1.10.2. Ownership Rules

當一個對象的引用被傳進或傳出一個函數,引用的擁有關係(所有權)是否跟着傳遞必須有明確說明,且作爲函數接口規範的一部分。

概述,函數返回的引用一般是傳遞所有權(例外:PyTuple_GetItem(),PyList_GetItem(),PyDict_GetItem(),PyDict_GetItemString(),PyImport_AddModule()),傳入函數的引用一般是借用所有權(例外:PyTuple_SetItem(),PyList_SetItem())。

大部分返回對象引用的函數會傳遞所有權。特別是所有創建新對象的函數,比如PyLong_FromLong()和Py_BuildValue(),都把引用的所有權傳遞給了接收者。即使對象不是新創建的,我們還是會收到對象的一個新的引用的所有權。比如,PyLong_FromLong()維護着一個常用數據的緩存並可能返回一個引用指向一個被緩存的條目。

很多從其他對象中提取對象的函數也會傳遞引用的所有權,比如PyObject_GetAttrString().這裏情況還是不夠清晰,因爲少數幾個常見的函數存在例外:PyTuple_GetItem(),PyList_GetItem(),PyDict_GetItem(),和PyDict_GetItemString()全部從元組(tuple),列表(list)和字典(dict)中返回借用的引用

PyImport_AddModule()函數也返回借用的引用,即使實際上它返回的對象可能是它新創建的:這是有可能的,因爲一個對象的已擁有引用(an owned reference)已存儲在sys.modules.

當我們把一個對象引用傳遞進另一個函數時,一般來講,那個函數會從我們這裏借用引用---如果它需要存儲它,它會調用Py_INCREF()來把自己變成一個引用的擁有者。這條規則也有兩個重要的例外:PyTuple_SetItem()和PyList_SetItem().這些函數會拿走傳遞給他們的對象的所有權---即使他們運行失敗!(注意,PyDict_SetItem()和類似函數(friends)不拿走引用所有權---他們是正常的("normal"))

當Python調用C函數時,C函數從它的調用者那裏借來參數的引用。調用者保留對對象引用的所有權,所以借來的引用的生命期只能持續到這個函數返回。只有當這個借來的引用必須要存儲或者做傳遞時,它纔會通過調用Py_INCREF()被轉變成有擁有權的引用。

從C函數返回給Python的對象引用必須是一個有擁有權的引用---擁有權從函數內傳遞給了它的調用者。

1.10.3. Thin Ice

這裏有幾個看上去很無害但實際卻會出問題的對借用引用的使用案例。這幾種情況都不得不做解釋器的隱式調用,而這些會導致引用的擁有者丟棄引用。

第一個也是最重要的一個情況是在借用一個列表的元素的引用(a reference to a list itm)時對一個不相干的對象使用Py_DECREF()。比如:

void
bug(PyObject *list)
{
	PyObject *item = PyList_GetItem(list, 0);

	PyList_SetItem(list, 1, PyLong_FromLong(0L));
	PyObject_Print(item, stdout, 0); /* BUG! */
}

這個函數先是借來一個list[0]的引用,然後把list[1]的值用0替換掉,最後打印借來的引用。看上去沒問題,是吧?但是有問題!

讓我們對控制流跟蹤到PyList_SetItem()內部。列表擁有它所有元素的引用,所以當第一個元素(item 1)被替換時,列表必須丟棄掉原來那第一個元素。現在我們假設原來的第一個元素是一個用戶自定義類的實例(an instance of a user-defined class),然後我們再假設那個類定義了一個__del__()方法。如果這個實例有一個引用計數爲1,丟棄它會調用它的__del__()方法。

因爲它是用Python編寫的,__del__()函數會執行任意的Python代碼。那麼它有可能會做些事情讓item的引用在bug()裏面失效麼?你打賭!假設傳遞到bug()的列表是可以被那個__del__()方法訪問的,它可以執行一條一句類似於"del list[0]"的語句,並且假定這是對那個對象的最後一個引用,那麼它會釋放內存,因此item無效了。

既然你知道代碼的問題,解決辦法很簡單:臨時增加引用計數。正確版本如下:

void
no_bug(PyObject *list)
{
	PyObject *item = PyList_GetItem(list, 0);

	Py_INCREF(item);
	PyList_SetItem(list, 1, PyLong_FromLong(0L));
	PyObject_Print(item, stdout, 0);
	Py_DECREF(item);
}

這是一個真實的故事。有個老版本的Python包含各種這樣的BUG,並導致一些人花費大量的時間用C調試器來查找爲什麼他的__del__()方法會失敗...

第二個借用引用的案例是一個涉及線程的變體。正常來講,Python解釋器的多線程相互不會干擾,因爲有一個全局鎖保護Python的整個對象空間。然而,用宏Py_BEGIN_ALLOW_THREADS可以臨時釋放這個鎖,用宏Py_END_ALLOW_THREADS可以重新請求它(加鎖)。在阻塞IO調用時這是很常見的,當等待IO完整的時讓其他線程使用處理器。很明顯,下面的函數有前述例子一樣的問題:

void
bug(PyObject *list)
{
	PyObject *item = PyList_GetItem(list, 0);
	Py_BEGIN_ALLOW_THREADS
	...some blocking I/O call...
	Py_END_ALLOW_THREADS
	PyObject_Print(item, stdout, 0); /* BUG! */
}
1.10.4. NULL Pointers

一般來講,使用對象引用作爲參數的函數不希望我們給它們傳空指針(NULL pointers),如果你這麼幹就會dump core(or cause later core dumps). 一般來講,返回對象引用的函數如果返回NULL只表明有異常發生了。不要測試NULL參數的原因是,函數經常把他們接收的對象傳遞給其他函數---如果每個函數都要做空指針測試,那將會有很多的多餘的測試,代碼運行會慢很多。

只在“source”測試NULL會更好一些,“source”:當收到一個可能爲NULL的指針,比如,malloc()或者一個可能拋出異常的函數的返回值。

宏Py_INCREF()和Py_DECREF()不檢測空指針---而,他們的變體Py_XINCREF()和Py_XDECREF()會做檢測。

檢查一個特定對象的類型的宏(Pytype_Check())不檢查空指針---再說一遍,簡言之,會有很多代碼並會運行緩慢。這個宏也沒有做NULL檢測的變體。

C函數調用機制保證傳遞給C函數的參數列表(在例子中是args)永遠不爲NULL---事實上他保證args一定是一個元組。

讓空指針越獄跑到Python程序員手裏去是一個嚴重的錯誤。

1.11. Writing Extensions in C++

用C++編寫擴展模塊也是可以的。一些限制如下。如果主程序(python解釋器)使用C編譯器編譯和連接的,有構造器的全局或靜態對象無法使用。如果主程序是用C++編譯器鏈接(linked)的這就不是問題了。要被Python解釋器(尤其是模塊初始化函數)調用的函數必須用“extern "c"”聲明。python的頭文件沒必要用“extern "c" {...}”包圍---如果定義了符號__cplusplus(所有最新的C++編譯器都定義過這種符號),python頭文件就已經使用這種格式了。

1.12. Providing a C API for an Extension Module

很多擴展模塊只提供python使用的新函數和類型,但是有時候擴展模塊裏面的代碼對其他擴展模塊來講也很有用。比如,一個擴展模塊可以實現一個新的類型“collection”,它類似於list但無序。就像標準Python的list類型有一個允許擴展模塊創建和操縱list的C API一樣,這個新的collection類型也應該有一堆c函數來讓其他擴展模塊直接操作collection。

第一眼貌似很容易:只用寫函數(當然了,不要做static聲明),提供一個恰當的頭文件,和C API文檔。實際上,如果所有擴展模塊都靜態鏈接到Python解釋器的話這麼幹確實沒問題。當模塊被作爲共享庫來使用時,定義在一個模塊中的符號對另一個模塊可能就不可見了。“是否可見”的細節由操作系統決定;有些系統爲Python解釋器和所有擴展模塊(例如,windows)使用一個全局的名字空間,而其他系統在模塊連接時要求一個顯式的列表存儲引入的符號(an explicit list of imported symbols),或者要求提供一個不同策略的選擇(大多數的unix)。即使符號是全局可見的,我們要調用的函數所在的模塊也有可能還沒有加載呢!

要想有夠好的可移植性,就不應該對符號的可見性做任何假設。這意味着爲了避免和其他擴展模塊的名稱衝突,擴展模塊裏面的所有符號應該被聲明爲static,除了模塊的初始化函數。這意味着要被其他擴展模塊訪問的符號必須以一種不同的方式導出。

Python提供了一種特別的機制把C級別(C-level)的信息(指針)從一個擴展模塊傳遞到另一個:Capsules(膠囊?).一個Capsule是一個Python數據類型,它存儲(攜帶,stored)一個指針(void*).Capsules只能通過他們的C API創建和訪問,但是他們可以像任何其他Python對象一樣到處傳遞。尤其是(specially),它們可以賦值給擴展模塊的名字空間中的一個名字。其他擴展模塊可以import這個模塊,獲得這個名字對應的值,並接着通過Capsule獲得指針。

有很多方式可以使得Capsules用於導出一個擴展模塊的C API。每個函數可以有它自己的Capsule,或者所有C API指針可以存儲到一個數組,這個數組的地址通過一個Capsule公佈出去。存儲和獲取指針的各種任務可以用不同的方式在當前模塊和其他client模塊之間分發。

不論你選哪種方法,合理的命名你的Capsules是很重要的。函數PyCapsule_New()使用一個name參數(const char *);我們可以傳遞一個NULL名字進去,但我們被強烈的建議使用一個非空的名字。合理的命名Capsules在一定程度上可以保證運行時的類型安全(type-safety);如果不命名的話,一個模塊沒法告訴另一個模塊這裏的Capsule.

需要特別說明的是,用於暴露C API的Capsules應該按下面的風格(convention)給一個名字:

modulename.attributename

PyCapsule_Import()函數可以很容易的加載一個由Capsule提供的C API,但要求Capsule的名字符合這種風格。這樣(behavior)讓C API的使用者更加肯定他們加載的Capsule包含正確的C API.

下面的例子論證了把大部分複雜工作(負擔)放在出口(export)模塊上的做法,對於常用庫模塊是很合理的。它把所有C API指針(在例子中只有一個指針)存儲到一個(元素類型爲)void指針的數組裏面,這個數組將會成爲Capsule的值。跟模塊對應的頭文件給出一個宏來import模塊和獲取C API指針;client模塊只有在訪問C API之前需要調用這個宏。

要出口(export)的模塊是從spam模塊修改而來的。函數spam.system()不直接調用C庫函數system(),而是調用函數PySpam_System(),當然了,這個函數會做一些更復雜的事情(比如把“spam”添加到每個命令裏)。這個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 PyLong_FromLong(sts);
}
在模塊的開始、緊跟着下面這行:
#include "Python.h"
必須再添加兩行:
#define SPAM_MODULE
#include "spammodule.h"
#define是要告訴頭文件,它正在被include到一個出口模塊(exporting module),而不是一個client模塊。最後,模塊的初始化函數必須小心的初始化C API指針數組:
PyMODINIT_FUNC
PyInit_spam(void)
{
	PyObject *m;
	static void *PySpam_API[PySpam_API_pointers];
	PyObject *c_api_object;

	m = PyModule_Create(&spammodule);
	if (m == NULL)
		return NULL;

	/* Initialize the C API pointer array */
	PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

	/* Create a Capsule containing the API pointer array's address */
	c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

	if (c_api_object != NULL)
		PyModule_AddObject(m, "_C_API", c_api_object);
	return m;
}

注意,PySpam_API被聲明爲static;否則,當PyInit_spam()終止時指針數組會消失!

大部分的工作在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 on error, 0 on success.
 * PyCapsule_Import will set an exception if there's an error.
 */
static int
import_spam(void)
{
	PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
	return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

爲了訪問函數PySpam_System(),client模塊要做的是必須在它的初始化函數裏調用函數(或者宏)import_spam():

PyMODINIT_FUNC
PyInit_client(void)
{
	PyObject *m;

	m = PyModule_Create(&clientmodule);
	if (m == NULL)
		return NULL;
	if (import_spam() < 0)
		return NULL;
	/* additional initialization can happen here */
	return m;
}

這種方式的主要缺點是spammodule.h文件太複雜了。然而,每個要導出的函數的基本結構是一樣的,所以這個只用學一次就行了。

最後需要提醒的的是,Capsules提供了附加的功能,這個對Capsule裏面存儲的指針做內存申請和釋放時尤其有用。細節在Python/C API參考手冊的Capsules部分(http://docs.python.org/3/c-api/capsule.html#capsules)和Capsules的實現裏面(源碼Include/pycapsule.h和Object/pycapsule.c)。


3. Building C and C++ Extensions with distutils

http://docs.python.org/2/extending/building.html

從Python1.4開始,Python在Unix平臺提供一個專門的make file來爲編譯動態鏈接擴展和定製解釋器創建make file.從Python2.0開始,這個機制(Makefile.pre.in相關的東西和設置文件)不再支持了。創建定製的解釋器很少被使用,並且擴展模塊可以用distutils創建。

用distutils創建擴展模塊要求機器上已經安裝了distutils,distutils已經被包含到python2.x裏面了並且可單獨被python1.5使用。自從distutils也支持創建二進制包以後,用戶沒必要一定用編譯器和和distutils來安裝擴展了。

一個distutils包包含一個驅動腳本,setup.py.這是一個純Python文件,在大部分簡單的情況下,這個文件看上去是這樣的:

from distutils.core import setup, Extension

module1 = Extension('demo',
					sources = ['demo.c'])

setup (name = 'PackageName',
	   version = '1.0',
	   description = 'This is a demo package',
	   ext_modules = [module1])
拿着這個setup.py和一個demo.c文件,運行

python setup.py build
將會編譯demo.c,並在build目錄生成一個名字爲demo的擴展模塊。在不同的系統上,模塊文件最終會在子目錄build/lib.system中,並且可能有一個像demo.so或demo.pyd一樣的名字。

在setup.py,所有的執行是通過調用setup函數來完成的。它接受可變數量的關鍵字參數,上面的例子只用了其中的一個子集。具體來說,上述例子指定元信息(meta-infomation)來構建包,並且它指定了包的內容。一般來講,一個包會包含額外的模塊,像Python源碼模塊,文檔,子包,等等。請參閱distutils文檔(Distributing Python Modules,http://docs.python.org/3/distutils/index.html#distutils-index)學習distutils的更多功能;這裏只解釋怎麼創建擴展模塊。

一般要預先計算setup()的參數,以便更好的構造驅動腳本。在上述例子中,傳給setup()的參數ext_modules是一個擴展模塊的list,list的每個元素是一個擴展(Extension)的實例(instance)。在例子裏,那個實例定義了一個名爲demo的擴展(Extension),這個擴展是通過編譯一個單獨的源文件demo.c來生成的。

在很多情況下,創建一個擴展要比這裏說的更復雜一些,因爲可能會需要額外的預處理器定義(additional preprocessor defines)和額外的庫。這個有下面這個例子來證明:

from distutils.core import setup, Extension

module1 = Extension('demo',
					define_macros = [('MAJOR_VERSION', '1'),
									 ('MINOR_VERSION', '0')],
					include_dirs = ['/usr/local/include'],
					libraries = ['tcl83'],
					library_dirs = ['/usr/local/lib'],
					sources = ['demo.c'])

setup (name = 'PackageName',
	   version = '1.0',
	   description = 'This is a demo package',
	   author = 'Martin v. Loewis',
	   author_email = '[email protected]',
	   url = 'http://docs.python.org/extending/building',
	   long_description = '''
This is really just a demo package.
''',
	   ext_modules = [module1])

在這個例子裏,setup()調用時使用了額外的元信息(meta-information),在創建發佈包時這些額外的元信息是推薦使用的。模塊它自己也指定了預處理器定義(preprocessor defines),include目錄,library目錄.根據編譯器的不同,distutils用不同的方式把這些信息傳遞給編譯器。比如,在Unix上,這個會聲場如下的編譯命令:

gcc -DNDEBUG -g -O3 -Wall -Wstrict-prototypes -fPIC -DMAJOR_VERSION=1 -DMINOR_VERSION=0 -I/usr/local/include -I/usr/local/include/python2.2 -c demo.c -o build/temp.linux-i686-2.2/demo.o
gcc -shared build/temp.linux-i686-2.2/demo.o -L/usr/local/lib -ltcl83 -o build/lib.linux-i686-2.2/demo.so
這幾行是純粹是爲了演示;distutils的用戶應該相信distutils會做正確的調用。
3.1. Distributing your extension modules

當一個模塊被成功創建出來以後,有三種方式使用它。

最終的用戶通常想要安裝模塊,他們可以這樣做:

python setup.py install

模塊的維護者應該會生成源碼包,他們這樣幹:

python setup.py install

有些情況下,額外的文件需要被添加到源碼發佈包裏面去;這個通過一個MANIFEST.in文件來完成;細節請看distutils文檔。

如果源碼發佈包已經成功創建,模塊維護者可以創建二進制發佈包。根據不同的平臺,下面命令中的一個會被用來幹這活:

python setup.py bdist_wininst
python setup.py bdist_rpm
python setup.py bdist_dumb


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