1. 程式人生 > >Python與C/C++互操作

Python與C/C++互操作

增加 包裝 eap c程序 得到 二進制 說明 數據 and

Python調用C/C++

Python調用C/C++的方法可以分為兩類:

  1. 手寫擴展模塊:除了被調用的C/C++函數外,一般還需要編寫包裹函數、導出表、導出函數、編譯腳本等代碼。

  2. 使用封裝庫的接口:比如官方的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++互操作