Python與C/C++互操作
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 cdll
libc = 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.py
from distutils.core import setup, Extension
MOD = ‘exfun‘
setup(name=MOD, ext_modules=[Extension(MOD, sources=[‘exfun.c‘])])
python3 setup.py build
python3 setup.py install
在Python中調用。
import exfun
print(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 cdll
import exfun
import timeit
def 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.78s
print(timeit.timeit("libc.fac(20)", setup="from __main__ import libc")) # 0.39s
print(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來代替動態鏈接庫形式的插件,這樣可以方便地根據需求的變化改寫腳本代碼,提高靈活性。
多語言程序分析工具
就像上面 實現與性能 一節中看到的,多語言的使用為腳本語言帶來了巨大的性能提升,然而同時也提高了編程復雜度,潛藏內存泄漏、懸空引用等一系列問題,也增加了系統性能分析的難度。下面列舉一些可能有幫助的工具。
Intel Pin: 使用動態二進制插樁的程序分析工具。
Pungi: 靜態分析Python的C擴展接口代碼的引用計數錯誤。(未開源,不適用於C++擴展)
CPyChecker: 在擴展模塊中檢查一系列錯誤的gcc插件。
Intel SEAPI (ITT API): 生成和控制應用程序執行過程中的跟蹤數據集合。
Python與C/C++互操作