標籤:

聊聊Python ctypes 模塊

摘要:模塊ctypes是Python內建的用於調用動態鏈接庫函數的功能模塊,一定程度上可以用於Python與其他語言的混合編程。由於編寫動態鏈接庫,使用C/C++是最常見的方式,故ctypes最常用於Python與C/C++混合編程之中。

=================================================================

1. ctypes 的原理以及優缺點

從ctypes的文檔中可以推斷,在各個平台上均使用了對應平台動態載入動態鏈接庫的方法,並通過一套類型映射的方式將Python與二進位動態鏈接庫相連接。通過閱讀ctypes本身的代碼也可以印證這個推斷(/Modules/_ctypes/_ctypes.c和/Modules/_ctypes/callproc.c)。在Windows平台下,最終調用的是Windows API中LoadLibrary函數和GetProcAddress函數,在Linux和Mac OS X平台下,最終調用的是Posix標準中的dlopen和dlsym函數。ctypes 實現了一系列的類型轉換方法,Python的數據類型會包裝或直接推算為C類型,作為函數的調用參數;函數的返回值也經過一系列的包裝成為Python類型。也就是說,PyObject* <-> C types的轉換是由ctypes內部完成的,這和SWIG是同一個原理。

從ctypes的實現原理不難看出:

ctypes 有以下優點:

  • Python內建,不需要單獨安裝
  • 可以直接調用二進位的動態鏈接庫

  • 在Python一側,不需要了解Python內部的工作方式
  • 在C/C++一側,也不需要了解Python內部的工作方式
  • 對基本類型的相互映射有良好的支持

ctypes 有以下缺點:

  • 平台兼容性差
  • 不能夠直接調用動態鏈接庫中未經導出的函數或變數
  • 對C++的支持差

就個人的經驗來看,ctypes 適合於「中輕量級」的Python C/C++混合編程。特別是遇到第三方庫提供動態鏈接庫和調用文檔,且沒有編譯器或編譯器並不互相兼容的場合下,使用ctypes特別方便。值得注意的是,對於某種需求,在Python本身就可以實現的情況下(例如獲取系統時間、讀寫文件等),應該優先使用Python自身的功能而不要使用操作系統提供的API介面,否則你的程序會喪失跨平台的特性。

2. 一個簡單的例子

作為Python文檔的一部分,ctypes 提供了完善的文檔。但沒有Windows API編程經驗的初學者讀ctypes文檔仍然會暈頭轉向。這裡舉一個小例子,儘力避開Windows API以及POSIX本身的複雜性,讀者只需要了解C語言即可。

在嘗試本節例子之前,依然要搭建Python擴展編程環境。見 搭建Python擴展開發環境 - 蛇之魅惑 - 知乎專欄

首先我們寫一個C語言的小程序,然後把它編譯成動態鏈接庫。

//great_module.cn#include <nmmintrin.h>nn#ifdef _MSC_VERn #define DLL_EXPORT __declspec( dllexport ) n#elsen #define DLL_EXPORTn#endifnnDLL_EXPORT int great_function(unsigned int n) {n return _mm_popcnt_u32(n);n}n

這個源文件中只有一個函數 great_function,它會調用Intel SSE4.2指令集的POPCNT指令(封裝在_mm_popcnt_u32中),即計算一個無符號整數的二進位表示中「1」的個數。如果你的電腦是2010年前購買的,那麼很可能不支持SSE4.2指令集,你只需要把return這一行改為 return n+1;即可,同樣能夠說明問題。

調用_mm_popcnt_u32需要包含Intel 指令集頭文件nmmintrin.h,它雖然不是標準庫的一部分,但是所有主流編譯器都支持。

中間還有一坨#ifdef...#else...#endif,這個是給MSVC準備的。因為在MSVC下,動態鏈接庫導出的函數必須加 __declspec( dllexport ) 進行修飾。而gcc(Linux和Mac OS X的默認編譯器)下,所有函數默認均導出。

接下來把它編譯為動態鏈接庫。Windows下動態鏈接庫的擴展名是dll,Linux下是so,Mac OS X下是dylib。這裡為了方便起見,一律將擴展名設定為dll。

Windows MSVC 下編譯命令:(啟動Visual Studio命令提示)

cl /LD great_module.c /o great_module.dll n

Windows GCC、Linux、Mac OS X下編譯命令相同:

gcc -fPIC -shared -msse4.2 great_module.c -o great_module.dlln

寫一個Python程序測試它,這個Python程序是跨平台的:

from ctypes import *ngreat_module = cdll.LoadLibrary(./great_module.dll)nprint great_module.great_function(13)n

整數13是二進位的1101,所以應該輸出3

3. 類型映射:基本類型

對於數字和字元串等基本類型。ctypes 採用」中間類型「的方式在Python和C之間搭建橋樑。對於C類型Tc,均有ctypes類型Tm,將其轉換為Python類型Tp。具體地說,例如某動態鏈接庫中的函數要求參數具有C類型Tc,那麼在Python ctypes 調用它的時候,就給予對應的ctypes類型Tm。Tm的值可以通過構造函數的方式傳遞對應的Python類型Tp。或者,使用它的可修改成員 Tm.value。

Tm(ctypes type)、Tc(C type)、Tp (Python type) 之對應關係見下表。

上面一段話比較繞。下面舉個例子。

大家熟知的printf函數位於C標準庫中。在C代碼中調用printf是標準化的,但是,C標準庫的實現不是標準化的。在Windows中,printf 函數位於%SystemRoot%System32msvcrt.dll,在Mac OS X中,它位於 /usr/lib/libc.dylib,在Linux中,一般位於 /usr/lib/libc.so.6。

下面一段代碼可以在三大平台上運行:

from ctypes import *nfrom platform import *nncdll_names = {n Darwin : libc.dylib,n Linux : libc.so.6,n Windows: msvcrt.dlln }nnclib = cdll.LoadLibrary(cdll_names[system()])nclib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3))n

我們只關注最後一行。printf的原型是

int printf (const char * format,...)n

所以,第一個參數我們用c_char_p創建一個C字元串,並以構造函數的方式用一個Python字元串初始化它。其後,我們給予printf一個int型和一個double型的變數,相應的,我們用c_int和c_double創建對應的C類型變數,並以構造函數的方式初始化它們。

如果不用構造函數,還可以用value成員。以下代碼與 clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)) 等價:

str_format = c_char_p()nint_val = c_int()ndouble_val = c_double()nnstr_format.value = "Hello %d %f"nint_val.value = 15ndouble_val.value = 2.3nclib.printf(str_format,int_val,double_val)n

一些C庫函數接受指針並修改指針所指向的值。這種情況下相當於數據從C函數流回Python。仍然使用value成員獲取值。

from ctypes import *nfrom platform import *nncdll_names = {n Darwin : libc.dylib,n Linux : libc.so.6,n Windows: msvcrt.dlln }nnclib = cdll.LoadLibrary(cdll_names[system()])ns1 = c_char_p(a)ns2 = c_char_p(b)ns3 = clib.strcat(s1,s2)nprint s1.value #abn

最後,當 ctypes 可以判斷類型對應關係的時候,可以直接將Python類型賦予C函數。ctypes 會進行隱式類型轉換。例如:

s1 = c_char_p(a)ns3 = clib.strcat(s1,b) # 等價於 s3 = clib.strcat(s1,c_char_p(b))nprint s1.value #abn

但是,當 ctypes 無法確定類型對應的時候,會觸發異常。

clib.printf(c_char_p("Hello %d %f"),15,2.3)n

異常:

Traceback (most recent call last):n File "test_printf.py", line 12, in <module>n clib.printf(c_char_p("Hello %d %f"),15,2.3)nctypes.ArgumentError: argument 3: <type exceptions.TypeError>: Dont know how to convert parameter 3n

4. 高級類型映射:數組

在C語言中,char 是一種類型,char [100]是另外一種類型。ctypes 也是一樣。使用數組需要預先生成需要的數組類型。

為了方便我們用great_module,增加一個函數 array_get

//great_module.cn#ifdef _MSC_VERn #define DLL_EXPORT __declspec( dllexport ) n#elsen #define DLL_EXPORTn#endif nDLL_EXPORT int array_get(int a[], int index) {n return a[index];n}n

下面我們在Python里產生數組類型。ctypes 類型重載了操作符 *,因此產生數組類型很容易:

from ctypes import *ngreat_module = cdll.LoadLibrary(./great_module.dll)nntype_int_array_10 = c_int * 10nnmy_array = type_int_array_10()nmy_array[2] = c_int(5)nprint great_module.array_get(my_array,2)n

type_int_array_10 即為創建的數組類型,如果想得到數組變數,則需要例化這個類型,即my_array。my_array的每一個成員的類型應該是 c_int,這裡將它索引為2的成員賦予值 c_int(5)。當然由於隱式轉換的存在,這裡寫 my_array[2] = 5也完全沒有問題。

至於函數返回值的類型,ctypes 規定,總是假設返回值為int。對於array_get而言,碰巧函數返回值也是int,所以具體的數值能被正確的取到。

如果動態鏈接庫中的C函數返回值不是int,需要在調用函數之前顯式的告訴ctypes返回值的類型。例如:

from ctypes import *nfrom platform import *nncdll_names = {n Darwin : libc.dylib,n Linux : libc.so.6,n Windows: msvcrt.dlln }nnclib = cdll.LoadLibrary(cdll_names[system()])ns3 = clib.strcat(a,b)nprint s3 # an int value like 5444948nclib.strcat.restype = c_char_pns4 = clib.strcat(c,d)nprint s4 # cdn

定義一個「高維數組」的方法類似。之所以加了引號,是因為C語言里並沒有真正的高維數組,ctype也一樣——都是利用數組的數組實現的。

from ctypes import *ntype_int_array_10 = c_int * 10ntype_int_array_10_10 = type_int_array_10 * 10nmy_array = type_int_array_10_10()nmy_array[1][2] = 3n

5. 高級類型映射:簡單類型指針

ctypes 和C一樣區分指針類型和指針變數。複習這兩個概念:C語言里,int *是指針類型。用它聲明的變數就叫指針變數。指針變數可以被賦予某個變數的地址。

在ctypes中,指針類型用 POINTER(ctypes_type) 創建。例如創建一個類似於C語言的int *:

type_p_int = POINTER(c_int)nv = c_int(4)np_int = type_p_int(v)nprint p_int[0]nprint p_int.contentsn

其中,type_p_int是一個類型,這個類型是指向int的指針類型。只有將指針類型例化之後才能得到指針變數。在例化為指針變數的同時將其指向變數v。這段代碼在C語言里相當於

typedef int * type_p_int;nint v = 4;ntype_p_int p = &v;nprintf("%d",p[0]);nprintf("%d",*p);n

當然,由於Python是依靠綁定傳遞類型的語言,可以直接使用 ctypes 提供的pointer()得到一個變數的指針變數

from ctypes import *nntype_p_int = POINTER(c_int)nv = c_int(4)np_int = type_p_int(v)nprint type(p_int)nprint p_int[0]nprint p_int.contentsn#-------np_int = pointer(v)nprint type(p_int)nprint p_int[0]nprint p_int.contentsn

"#-------" 之前和之後輸出的內容是一樣的。

6. 高級類型映射:函數指針

函數指針並沒有什麼特別之處。如果一個動態鏈接庫里的某個C函數需要函數指針,那麼可以遵循以下的步驟將一個Python函數包裝成函數指針:

  1. 查看文檔,將C函數指針的原型利用ctypes的CFUNCTYPE包裝成ctypes函數指針類型。
  2. 利用剛才得到的函數指針類型之構造函數,賦予其Python函數名,即得到函數指針變數。

我們這裡舉兩個例子。

第一個例子來源於ctypes官方文檔。它調用的是C標準庫中的qsort函數。

我們先觀察qsort的文檔:

qsort - C++ Reference

它的函數原型是

void qsort (void* base, size_t num, size_t size,n int (*compar)(const void*,const void*));n

第三個參數即為函數指針作為回調函數,用於給出元素之間大小的判斷方法。我們這裡使用整數作為判斷類型。那麼qsort的函數原型可以理解為:

void qsort (int* base, size_t num, size_t size,n int (*compar)(const int*,const int*));n

其中,回調函數的原型為:

int compar(const int*,const int*)n

使用CFUNCTYPE創建ctypes的函數指針類型:

CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))n

CFUNCTYPE的第一個參數是函數的返回值,函數的其他參數緊隨其後。

接下來用Python寫回調函數的實現:

def py_cmp_func(a, b):n print type(a)n print "py_cmp_func", a[0], b[0]n return a[0] - b[0]n

最後,用剛才得到的函數指針類型CMPFUNC,以Python回調函數的函數名作為構造函數的參數,就得到了可以用於C函數的函數指針變數:

p_c_cmp_func = CMPFUNC(py_cmp_func)n

完整的代碼如下:

from ctypes import *nfrom platform import *nncdll_names = {n Darwin : libc.dylib,n Linux : libc.so.6,n Windows: msvcrt.dlln }nnclib = cdll.LoadLibrary(cdll_names[system()])nnCMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))nndef py_cmp_func(a, b):n print type(a)n print "py_cmp_func", a[0], b[0]n return a[0] - b[0]nntype_array_5 = c_int * 5nia = type_array_5(5, 1, 7, 33, 99)nclib.qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func)) n

注意到,Python函數得到的參數a和b的類型都是 POINTER(c_int) (顯示為<class __main__.LP_c_int>),對指針變數解引用的方法是之前提到的[0]或者.contents。我們這裡應用了ctypes的隱式類型轉換,所以a[0]和b[0]可以當成Python的int類型使用。

有趣的是,這段代碼在*nix(Linux、Mac OS X)下調用和在Windows下調用,比較的次數是不一樣的。Windows似乎更費事。

第二個例子比較實用,但只能在Windows下運行。

我們要利用的是Windows API EnumWindows枚舉系統所有窗口的句柄,再根據窗口的句柄列出各個窗口的標題。

EnumWindows的文檔見EnumWindows function (Windows)

調用Windows API有特殊之處。由於Windows API函數不使用標準C的調用約定(微軟一貫的尿性)。故在LoadLibrary時不能夠使用cdll.LoadLibrary而使用windll.LoadLibrary。在聲明函數指針類型的時候,也不能用CFUNCTYPE而是用WINFUNCTYPE。關於調用約定的問題參見x86 calling conventions

Windows API有很多內建類型,ctypes也對應地提供了支持。代碼如下:

from ctypes import *nfrom ctypes import wintypesnnWNDENUMPROC = WINFUNCTYPE(wintypes.BOOL,n wintypes.HWND,n wintypes.LPARAM)nuser32 = windll.LoadLibrary(user32.dll)nndef EnumWindowsProc(hwnd, lParam):n length = user32.GetWindowTextLengthW(hwnd) + 1n buffer = create_unicode_buffer(length)n user32.GetWindowTextW(hwnd, buffer, length)n print buffer.valuen return Truennuser32.EnumWindows(WNDENUMPROC(EnumWindowsProc), 0)n

7. 其他

ctypes 還對C語言中的結構體、聯合體等提供支持。這部分代碼比較繁瑣,可參見ctypes的文檔 docs.python.org/2/libra

推薦閱讀:

如何讓自己的 python 代碼更有逼格?
python有大量機器學習庫,但是不能結合hadoop,該如何實現大規模的機器學習?
2017 夢醒時分

TAG:Python |