標籤:return extension tuple red end 擴充 amp 初始 應用
Python調用C/C++
Python調用C/C++的方法可以分為兩類:
手寫擴充模組:除了被調用的C/C++函數外,一般還需要編寫包裹函數、匯出表、匯出函數、編譯指令碼等代碼。
使用封裝庫的介面:比如官方的ctypes,還有第三方的如CFFI、Boost、SWIG、pybind11等。
最終在Python中都是通過載入動態連結程式庫的方式實現調用,手寫模組的方式更為複雜,但也更為高效。在實際工程中,一般對需要調用C/C++的函數(主要出於效能考慮)先通過ctypes實現,再有針對性地使用手寫模組的方式改寫。
ctypes
ctypes[文檔]是Python官方的一個提供C相容資料類型的外部函數庫,通過動態連結或共用庫的方式調用。
從原理上來說[文章]:ctypes直接調用二進位的動態連結程式庫(平台相容性差),在Windows下最終調用的是Windows API中的LoadLibrary函數和GetProcAddress函數,在UNIX平台下最終調用的是Posix標準中的dlopen和dlsym函數。ctypes實現了一系列的類型轉換方法,Python資料類型被封裝或直接推算為C類型,即手寫模組中的PyObject * <-> C types的轉換由ctypes內部完成。
手寫擴充模組
Python/C API Reference Manul
包裹函數:主要實現Python和C的參數轉換,以及對C函數的調用。
匯出表:告訴Python模組名,其中的被調函數名,以及對應的包裹函數和參數說明。
匯出函數:按照匯出表完成模組初始化。函數名必須以PyInit_為首碼。
Python和C之間的類型轉碼如下表所示。
| Format Code | Python Type | C Type |
| :-: | :-: | :-: |
| s | str | char * |
| z | str/None | char /NULL |
| i | int | int |
| l | long | long |
| c | str | char |
| d | float | double |
| D | complex | Py_Complex |
| O | (any) | PyObject * |
| S | str | PyStringObject |
相比於ctypes,手寫擴充模組複雜得多,同時還可能帶來一些問題,如包裹函數中可能需要free()操作(防止記憶體流失)、對象引用計數的宏(Py_INCREF(), Py_DECREF(), Py_XINCREEF(), Py_XDECREF())、多線程的宏(Py_BEGIN_ALLOW_THREADS, Py_END_ALLOW_THREADS)等,因此對程式員要求較高。
實現與效能首先給出一個ctypes的例子。
編寫一個簡單的C程式fac.c實現階乘功能。
int fac(int n){ if (n < 2) return 1; return n * fac(n - 1);}
編譯產生動態連結程式庫。
gcc fac.c -fPIC -shared -o fac.so
在Python中調用。
from ctypes import cdlllibc = cdll.LoadLibrary(‘./fac.so‘)print(libc.fac(3)) # output 3! = 6
然後給出同樣功能的手寫擴充模組實現。
首先使用樣板(即介面)包裹上述階乘程式,使得應用程式代碼可以和Python解譯器進行互動。注意:Python2和Python3的介面略有不同,本文針對Python3。
int fac(int n){ if (n < 2) return 1; return n * fac(n - 1);}#include <Python.h>// 包裹函數static PyObject *ExFac(PyObject *self, PyObject *args){ int n; // 參數轉換 // 根據指定格式解析並將結果放入指標變數,返回0表示解析失敗 if (!PyArg_ParseTuple(args, "i", &n)) return NULL; // 把C資料轉換為Python對象並返回 return (PyObject *)Py_BuildValue("i", fac(n));}static PyMethodDef ExfunMethods[] = { // 表示參數以tuple形式傳入 {"fac", ExFac, METH_VARARGS}, {NULL, NULL},};// 匯出表static struct PyModuleDef ExfunMethod = { PyModuleDef_HEAD_INIT, "exfun", NULL, -1, ExfunMethods};// 匯出函數void PyInit_exfun(){ PyModule_Create(&ExfunMethod);}
把該模組編譯到Python中。
# setup.pyfrom distutils.core import setup, ExtensionMOD = ‘exfun‘setup(name=MOD, ext_modules=[Extension(MOD, sources=[‘exfun.c‘])])
python3 setup.py buildpython3 setup.py install
在Python中調用。
import exfunprint(exfun.fac(3)) # output 6
在Python中匯入並調用exfun.fac()後,包裹函數ExFac()被調用,接受一個Python的整型參數,並轉化為C的整型,然後調用C的fac()函數,得到一個整型傳回值,再轉為Python的整型作為整個函數調用的結果返回。
最後比較原生Python與上述兩種C調用的效能表現。
Python提供timeit模組來測量小程式碼片段的執行時間。以計算20的階乘為例,timeit預設執行1,000,000次。
from ctypes import cdllimport exfunimport timeitdef fac(n): if n < 2: return 1 return n * fac(n - 1)libc = cdll.LoadLibrary(‘./fac.so‘)print(timeit.timeit("fac(20)", setup="from __main__ import fac")) # 2.78sprint(timeit.timeit("libc.fac(20)", setup="from __main__ import libc")) # 0.39sprint(timeit.timeit("exfun.fac(20)", setup="from __main__ import exfun")) # 0.16s
需要注意的是,由於Python3中int對長整型的支援,計算20!時原生Python得到的是正確的結果,而調用C的都溢出了。
其他方法
如CFFI,
Boost,
SWIG,
pybind11。
C/C++調用Python
C/C++調用Python一般是為了利用指令碼開發的靈活性,類似於遊戲開發中Lua和C++的結合。在C++應用中,我們可以用一組外掛程式來實現一些具有統一介面的功能,一般外掛程式都使用動態連結程式庫實現。如果外掛程式的變化比較頻繁,我們就可以使用Python來代替動態連結程式庫形式的外掛程式,這樣可以方便地根據需求的變化改寫指令碼代碼,提高靈活性。
多語言程式分析工具
就像上面 實現與效能 一節中看到的,多語言的使用為指令碼語言帶來了巨大的效能提升,然而同時也提高了編程複雜度,潛藏記憶體流失、懸Null 參考等一系列問題,也增加了系統效能分析的難度。下面列舉一些可能有協助的工具。
Intel Pin: 使用動態二進位插樁的程式分析工具。
Pungi: 靜態分析Python的C擴充介面代碼的引用計數錯誤。(未開源,不適用於C++擴充)
CPyChecker: 在擴充模組中檢查一系列錯誤的gcc外掛程式。
Intel SEAPI (ITT API): 產生和控制應用程式執行過程中的跟蹤資料集合。
Python與C/C++互操作