1. 在其它應用程序嵌入 Python?
前幾章討論了如何對 Python 進(jìn)行擴展,也就是如何用 C 函數庫 擴展 Python 的功能。反過(guò)來(lái)也是可以的:將 Python 嵌入到 C/C++ 應用程序中豐富其功能。這種嵌入可以讓?xiě)贸绦蛴?Python 來(lái)實(shí)現某些功能,而不是用 C 或 C++ 。用途會(huì )有很多;比如允許用戶(hù)用 Python 編寫(xiě)一些腳本,以便定制應用程序滿(mǎn)足需求。如果某些功能用 Python 編寫(xiě)起來(lái)更為容易,那么開(kāi)發(fā)人員自己也能這么干。
Python 的嵌入類(lèi)似于擴展,但不完全相同。不同之處在于,擴展 Python 時(shí)應用程序的主程序仍然是 Python 解釋器,而嵌入 Python 時(shí)的主程序可能與 Python 完全無(wú)關(guān)——而是應用程序的某些部分偶爾會(huì )調用 Python 解釋器來(lái)運行一些 Python 代碼。
因此,若要嵌入 Python,就要提供自己的主程序。此主程序要做的事情之一就是初始化 Python 解釋器。至少得調用函數 Py_Initialize()
。還有些可選的調用可向 Python 傳遞命令行參數。之后即可從應用程序的任何地方調用解釋器了。
調用解釋器的方式有好幾種:可向 PyRun_SimpleString()
傳入一個(gè)包含 Python 語(yǔ)句的字符串,也可向 PyRun_SimpleFile()
傳入一個(gè) stdio 文件指針和一個(gè)文件名(僅在錯誤信息中起到識別作用)。還可以調用前面介紹過(guò)的底層操作來(lái)構造并使用 Python 對象。
參見(jiàn)
- Python/C API 參考手冊
本文詳細介紹了 Python 的 C 接口。這里有大量必要的信息。
1.1. 高層次的嵌入?
最簡(jiǎn)單的 Python 嵌入形式就是采用非常高層的接口。該接口的目標是只執行一段 Python 腳本,而無(wú)需與應用程序直接交互。比如以下代碼可以用來(lái)對某個(gè)文件進(jìn)行一些操作。
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
wchar_t *program = Py_DecodeLocale(argv[0], NULL);
if (program == NULL) {
fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
exit(1);
}
Py_SetProgramName(program); /* optional but recommended */
Py_Initialize();
PyRun_SimpleString("from time import time,ctime\n"
"print('Today is', ctime(time()))\n");
if (Py_FinalizeEx() < 0) {
exit(120);
}
PyMem_RawFree(program);
return 0;
}
在 Py_Initialize()
之前,應該先調用 Py_SetProgramName()
函數,以便向解釋器告知 Python運行庫的路徑。接下來(lái),Py_Initialize()
會(huì )初始化 Python 解釋器,然后執行硬編碼的 Python 腳本,打印出日期和時(shí)間。之后,調用 Py_FinalizeEx()
關(guān)閉解釋器,程序結束。在真實(shí)的程序中,可能需要從其他來(lái)源獲取 Python 腳本,或許是從文本編輯器例程、文件,或者某個(gè)數據庫。利用 PyRun_SimpleFile()
函數可以更好地從文件中獲取 Python 代碼,可省去分配內存空間和加載文件內容的麻煩。
1.2. 突破高層次嵌入的限制:概述?
高級接口能從應用程序中執行任何 Python 代碼,但至少交換數據可說(shuō)是相當麻煩的。如若需要交換數據,應使用較低級別的調用。幾乎可以實(shí)現任何功能,代價(jià)是得寫(xiě)更多的 C 代碼。
應該注意,盡管意圖不同,但擴展 Python 和嵌入 Python 的過(guò)程相當類(lèi)似。前幾章中討論的大多數主題依然有效。為了說(shuō)明這一點(diǎn),不妨來(lái)看一下從 Python 到 C 的擴展代碼到底做了什么:
將 Python 的數據轉換為 C 格式,
用轉換后的數據執行 C 程序的函數調用,
將調用返回的數據從 C 轉換為 Python 格式。
嵌入 Python 時(shí),接口代碼會(huì )這樣做:
將 C 數據轉換為 Python 格式,
用轉換后的數據執行對 Python 接口的函數調用,
將調用返回的數據從 Python 轉換為 C 格式。
可見(jiàn)只是數據轉換的步驟交換了一下順序,以順應跨語(yǔ)言的傳輸方向。唯一的區別是在兩次數據轉換之間調用的函數不同。在執行擴展時(shí),調用一個(gè) C 函數,而執行嵌入時(shí)調用的是個(gè) Python 函數。
本文不會(huì )討論如何將數據從 Python 轉換到 C 去,反之亦然。另外還假定讀者能夠正確使用引用并處理錯誤。由于這些地方與解釋器的擴展沒(méi)有區別,請參考前面的章節以獲得所需的信息。
1.3. 只做嵌入?
第一個(gè)程序的目標是執行 Python 腳本中的某個(gè)函數。就像高層次接口那樣,Python 解釋器并不會(huì )直接與應用程序進(jìn)行交互(但下一節將改變這一點(diǎn))。
要運行 Python 腳本中定義的函數,代碼如下:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
int i;
if (argc < 3) {
fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
return 1;
}
Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);
Py_DECREF(pName);
if (pModule != NULL) {
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
pArgs = PyTuple_New(argc - 3);
for (i = 0; i < argc - 3; ++i) {
pValue = PyLong_FromLong(atoi(argv[i + 3]));
if (!pValue) {
Py_DECREF(pArgs);
Py_DECREF(pModule);
fprintf(stderr, "Cannot convert argument\n");
return 1;
}
/* pValue reference stolen here: */
PyTuple_SetItem(pArgs, i, pValue);
}
pValue = PyObject_CallObject(pFunc, pArgs);
Py_DECREF(pArgs);
if (pValue != NULL) {
printf("Result of call: %ld\n", PyLong_AsLong(pValue));
Py_DECREF(pValue);
}
else {
Py_DECREF(pFunc);
Py_DECREF(pModule);
PyErr_Print();
fprintf(stderr,"Call failed\n");
return 1;
}
}
else {
if (PyErr_Occurred())
PyErr_Print();
fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
}
Py_XDECREF(pFunc);
Py_DECREF(pModule);
}
else {
PyErr_Print();
fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
return 1;
}
if (Py_FinalizeEx() < 0) {
return 120;
}
return 0;
}
上述代碼先利用 argv[1]
加載 Python 腳本,再調用 argv[2]
指定的函數。函數的整數參數是 argv
數組中的其余值。如果 編譯并鏈接 該程序(此處將最終的可執行程序稱(chēng)作 call), 并用它執行一個(gè) Python 腳本,例如:
def multiply(a,b):
print("Will compute", a, "times", b)
c = 0
for i in range(0, a):
c = c + b
return c
然后結果應該是:
$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6
盡管相對其功能而言,該程序體積相當龐大,但大部分代碼是用于 Python 和 C 之間的數據轉換,以及報告錯誤。嵌入 Python 的有趣部分從此開(kāi)始:
Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);
初始化解釋器之后,則用 PyImport_Import()
加載腳本。此函數的參數需是個(gè) Python 字符串,一個(gè)用 PyUnicode_FromString()
數據轉換函數構建的字符串。
pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */
if (pFunc && PyCallable_Check(pFunc)) {
...
}
Py_XDECREF(pFunc);
腳本一旦加載完畢,就會(huì )用 PyObject_GetAttrString()
查找屬性名稱(chēng)。如果名稱(chēng)存在,并且返回的是可調用對象,即可安全地視其為函數。然后程序繼續執行,照常構建由參數組成的元組。然后用以下方式調用 Python 函數:
pValue = PyObject_CallObject(pFunc, pArgs);
當函數返回時(shí),pValue
要么為 NULL
,要么包含對函數返回值的引用。請確保用完后釋放該引用。
1.4. 對嵌入 Python 功能進(jìn)行擴展?
到目前為止,嵌入的 Python 解釋器還不能訪(fǎng)問(wèn)應用程序本身的功能。Python API 通過(guò)擴展嵌入解釋器實(shí)現了這一點(diǎn)。 也就是說(shuō),用應用程序提供的函數對嵌入的解釋器進(jìn)行擴展。雖然聽(tīng)起來(lái)有些復雜,但也沒(méi)那么糟糕。只要暫時(shí)忘記是應用程序啟動(dòng)了 Python 解釋器。而把應用程序看作是一堆子程序,然后寫(xiě)一些膠水代碼讓 Python 訪(fǎng)問(wèn)這些子程序,就像編寫(xiě)普通的 Python 擴展程序一樣。 例如:
static int numargs=0;
/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
if(!PyArg_ParseTuple(args, ":numargs"))
return NULL;
return PyLong_FromLong(numargs);
}
static PyMethodDef EmbMethods[] = {
{"numargs", emb_numargs, METH_VARARGS,
"Return the number of arguments received by the process."},
{NULL, NULL, 0, NULL}
};
static PyModuleDef EmbModule = {
PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
NULL, NULL, NULL, NULL
};
static PyObject*
PyInit_emb(void)
{
return PyModule_Create(&EmbModule);
}
在 main()
函數之前插入上述代碼。并在調用 Py_Initialize()
之前插入以下兩條語(yǔ)句:
numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);
這兩行代碼初始化了 numargs
變量,并讓 emb.numargs()
函數能被嵌入的 Python 解釋器訪(fǎng)問(wèn)到。有了這些擴展,Python 腳本可以執行類(lèi)似以下功能:
import emb
print("Number of arguments", emb.numargs())
在真實(shí)的應用程序中,這種方法將把應用的 API 暴露給 Python 使用。
1.5. 在 C++ 中嵌入 Python?
還可以將 Python 嵌入到 C++ 程序中去;確切地說(shuō),實(shí)現方式將取決于 C++ 系統的實(shí)現細節;一般需用 C++ 編寫(xiě)主程序,并用 C++ 編譯器來(lái)編譯和鏈接程序。不需要用 C++ 重新編譯 Python 本身。
1.6. 在類(lèi) Unix 系統中編譯和鏈接?
為了將 Python 解釋器嵌入應用程序,找到正確的編譯參數傳給編譯器 (和鏈接器) 并非易事,特別是因為 Python 加載的庫模塊是以 C 動(dòng)態(tài)擴展(.so
文件)的形式實(shí)現的。
為了得到所需的編譯器和鏈接器參數,可執行 pythonX.Y-config
腳本,它是在安裝 Python 時(shí)生成的(也可能存在 python3-config
腳本)。該腳本有幾個(gè)參數,其中以下幾個(gè)參數會(huì )直接有用:
pythonX.Y-config --cflags
將給出建議的編譯參數。$ /opt/bin/python3.4-config --cflags -I/opt/include/python3.4m -I/opt/include/python3.4m -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
pythonX.Y-config --ldflags
將給出建議的鏈接參數。$ /opt/bin/python3.4-config --ldflags -L/opt/lib/python3.4/config-3.4m -lpthread -ldl -lutil -lm -lpython3.4m -Xlinker -export-dynamic
備注
為了避免多個(gè) Python 安裝版本引發(fā)混亂(特別是在系統安裝版本和自己編譯版本之間),建議用 pythonX.Y-config
指定絕對路徑,如上例所述。
如果上述方案不起作用(不能保證對所有 Unix 類(lèi)平臺都生效;歡迎提出 bug 報告),就得閱讀系統關(guān)于動(dòng)態(tài)鏈接的文檔,并檢查 Python 的 Makefile
(用 sysconfig.get_makefile_filename()
找到所在位置)和編譯參數。這時(shí) sysconfig
模塊會(huì )是個(gè)有用的工具,可用編程方式提取需組合在一起的配置值。比如:
>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'