用 C 編寫 Python 的延伸模組 (Python calls C)

「藉助於擁有基於標準函式庫的大量工具、能夠使用低階語言如C和可以作為其他函式庫介面的C++,Python已成為一種強大的應用於其他語言與工具之間的膠水語言。」 – 維基百科
所以讓我們來看看 Python 如何呼叫以 C 語言寫好的程式,見識一下何謂膠水語言(Glue Language)。
以下說明的是 Python 內建的作法,所謂的內建指的是 Python.h 通常安裝完 Python 都會存在於 /usr/lib/python/ 之中。另一種作法是透過 Boost.Python 的 C++ 函式庫。

首先,此範例程式的功用:執行 Shell command ,並且做簡單的加法運算(1+2=3),然後回傳。
我們就先來看一下要編譯成 Python 模組的 C 原始碼吧!

include static PyObject *spam_system(PyObject *self, PyObject *args) { const char command; int sts; int a; int b; if (!PyArg_ParseTuple(args, "(i, i)s", &a, &b, &command)) return NULL; system(command); int sum = a + b; return Py_BuildValue("i", sum); } static PyMethodDef SpamMethods[] = { {"c_system", spam_system, METH_VARARGS, "Execute a shell command."}, {NULL, NULL, 0, NULL} / Sentinel */ }; PyMODINIT_FUNC initspam(void) { (void) Py_InitModule("spam", SpamMethods); }

由上往下看下來,spam_system 很明顯就是我們自定義的函式,不過有幾點必須特別注意一下:

  • 函數的回傳型態必須是 static PyObject * ,C 與 Python 之間都必須透過此型態的物件來溝通。
  • 輸入參數是固定的,一定會有 PyObject *self 以及 PyObject *args。在這個例子我們不會使用到 self,且其值為 NULL(只會在實作built-in method的時候才不為空)。
  • PyArg_ParseTuple 這個函式負責的就是分析處理傳入的參數,也就是型態為 Tuple 的物件 (被args所指)。
  • 回傳值需透過 Py_BuildValue 轉回 PyObject* 傳遞,做的事情與 PyArg_ParseTuple 恰好相反。

接下來的 static PyMethodDef SpamMethods[] 定義了一個「Method Table」,接下去看之前,我們必須先瞭解 PyMethodDef 這個物件的意義,才能進一步的瞭解其中存放的參數作用:

typedef struct PyMethodDef { char ml_name; / method name / PyCFunction ml_meth; / implementation function / int ml_flags; / flags */ char ml_doc; / docstring */ } PyMethodDef;

知道了其定義之後,來看看各參數代表什麼意思吧!

  • “c_system":代表說被 python 引用之後的 method 名稱
  • spam_system:你所寫的 C 函數名稱
  • METH-VARARGS:定義了Calling convention,也就是規範 spam_system 的參數型態、回傳值型態、以及解析函式的使用。
  • “Execute a shell command.":此延伸模組的描述,描述可以透過 help(spam) 於 Python 中呼叫

最後的 PyMODINIT_FUNC initspam(void) 負責了初始化的任務,當此模組在 Python 中被引用的當下,此函數就會被呼叫,也就是間接的呼叫了 Py_InitModule 這個初始化函數,其中:

  • “spam":代表了此模組的名稱
  • SpamMethods:Method table name

寫好了 C 程式之後,我們就需要透過簡單的 Python script 把它編譯成函式庫:

from distutils.core import setup, Extension module1 = Extension('spam', sources = ['spammodule.c']) setup (name = 'spam', version = '1.0', description = 'This is a demo package', ext_modules = [module1])

這個設定檔應該淺顯易懂,就不贅述了。

最後來講解整個流程:

  1. 寫好 spammodule.c 以及 setup.py 之後,透過 “python setup.py build" 編譯函式庫
  2. “sudo python setup.py install" 安裝函式庫,這樣 python 才可以順利找到我們新增的這個模組
  3. import spam; 這時候會呼叫 initspam 做初始化的動作,初始化過程參考 Method Table 的設定
  4. spam.c_system(1, 2, ‘ls -l’);  先透過 Tuple 把括號內的值存起來,並把指向此 Tuple 的 PyObject 指標傳送到 C 那端
  5. 透過 PyArg_ParseTuple 把 Tuple 內的資料轉成 C 的形態儲存,在此例中,’ls -al’ 被以 const char *儲存
  6. 進行運算
  7. 透過 PyArg_ParseTuple 將運算結果轉為 PyObject * 回傳給 Python

其中比較有趣的地方應該是參數傳遞的部份,如果我們只傳遞一個參數,自然是比較直覺,但是如果我們要傳遞超過一個以上並且非字串的參數該怎麼呢?來看個例子:

ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);

其中的 “(ii)s#" 代表的是 Format string,從這裡我們可以清楚 i 代表了整數,而 s# 代表了字串及其長度,所以後面就是相對應用來存放的參數。

在 Python 中相對應的函式呼叫可能長得像: module.function((1, 2), ‘hello’)

沒想到會打了這麼長一篇,不過其實原理不會很難,並且使用起來相當的自然,與 Python module 無異,不愧為膠水語言啊XD