聊聊Python ctypes 模塊(轉載)

https://zhuanlan.zhihu.com/p/20152309?columnSlug=python-devhtml

 

做者:Jerry Jho
連接:https://zhuanlan.zhihu.com/p/20152309
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

摘要:模塊ctypes是Python內建的用於調用動態連接庫函數的功能模塊,必定程度上能夠用於Python與其餘語言的混合編程。因爲編寫動態連接庫,使用C/C++是最多見的方式,故ctypes最經常使用於Python與C/C++混合編程之中。python

=================================================================編程

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是同一個原理。windows

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

ctypes 有如下優勢:函數

  • Python內建,不須要單獨安裝
  • 能夠直接調用二進制的動態連接庫
  • 在Python一側,不須要了解Python內部的工做方式
  • 在C/C++一側,也不須要了解Python內部的工做方式
  • 對基本類型的相互映射有良好的支持

ctypes 有如下缺點:測試

  • 平臺兼容性差
  • 不可以直接調用動態連接庫中未經導出的函數或變量
  • 對C++的支持差

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

2. 一個簡單的例子操作系統

做爲Python文檔的一部分,ctypes 提供了完善的文檔。但沒有Windows API編程經驗的初學者讀ctypes文檔仍然會暈頭轉向。這裏舉一個小例子,盡力避開Windows API以及POSIX自己的複雜性,讀者只須要了解C語言便可。

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

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

//great_module.c #include <nmmintrin.h> #ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif DLL_EXPORT int great_function(unsigned int n) { return _mm_popcnt_u32(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

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

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

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

from ctypes import * great_module = cdll.LoadLibrary('./great_module.dll') print great_module.great_function(13) 

整數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%\System32\msvcrt.dll,在Mac OS X中,它位於 /usr/lib/libc.dylib,在Linux中,通常位於 /usr/lib/libc.so.6。

下面一段代碼能夠在三大平臺上運行:

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

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

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

因此,第一個參數咱們用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() int_val = c_int() double_val = c_double() str_format.value = "Hello %d %f" int_val.value = 15 double_val.value = 2.3 clib.printf(str_format,int_val,double_val) 

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

from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) s1 = c_char_p('a') s2 = c_char_p('b') s3 = clib.strcat(s1,s2) print s1.value #ab 
最後,當 ctypes 能夠判斷類型對應關係的時候,能夠直接將Python類型賦予C函數。ctypes 會進行 隱式類型轉換。例如:
s1 = c_char_p('a') s3 = clib.strcat(s1,'b') # 等價於 s3 = clib.strcat(s1,c_char_p('b')) print s1.value #ab 

可是,當 ctypes 沒法肯定類型對應的時候,會觸發異常。

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

異常:

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

4. 高級類型映射:數組

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

爲了方便咱們用great_module,增長一個函數 array_get

//great_module.c #ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif DLL_EXPORT int array_get(int a[], int index) { return a[index]; } 

下面咱們在Python裏產生數組類型。ctypes 類型重載了操做符 *,所以產生數組類型很容易:

from ctypes import *
great_module = cdll.LoadLibrary('./great_module.dll')

type_int_array_10 = c_int * 10

my_array = type_int_array_10()
my_array[2] = c_int(5)
print great_module.array_get(my_array,2)

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 * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) s3 = clib.strcat('a','b') print s3 # an int value like 5444948 clib.strcat.restype = c_char_p s4 = clib.strcat('c','d') print s4 # cd 

定義一個「高維數組」的方法相似。之因此加了引號,是由於C語言裏並無真正的高維數組,ctype也同樣——都是利用數組的數組實現的。

from ctypes import * type_int_array_10 = c_int * 10 type_int_array_10_10 = type_int_array_10 * 10 my_array = type_int_array_10_10() my_array[1][2] = 3 

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

ctypes 和C同樣區分指針類型和指針變量。複習這兩個概念:C語言裏,int *是指針類型。用它聲明的變量就叫指針變量。指針變量能夠被賦予某個變量的地址。

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

type_p_int = POINTER(c_int) v = c_int(4) p_int = type_p_int(v) print p_int[0] print p_int.contents 

其中,type_p_int是一個類型,這個類型是指向int的指針類型。只有將指針類型例化以後才能獲得指針變量。在例化爲指針變量的同時將其指向變量v。這段代碼在C語言裏至關於

typedef c_int * type_p_int; int v = 4; type_p_int p = &v; printf("%d",p[0]); printf("%d",*p); 

固然,因爲Python是依靠綁定傳遞類型的語言,能夠直接使用 ctypes 提供的pointer()獲得一個變量的指針變量

from ctypes import * type_p_int = POINTER(c_int) v = c_int(4) p_int = type_p_int(v) print type(p_int) print p_int[0] print p_int.contents #------- p_int = pointer(v) print type(p_int) print p_int[0] print p_int.contents 

"#-------" 以前和以後輸出的內容是同樣的。

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,
            int (*compar)(const void*,const void*));

第三個參數即爲函數指針做爲回調函數,用於給出元素之間大小的判斷方法。咱們這裏使用整數做爲判斷類型。那麼qsort的函數原型能夠理解爲:

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

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

int compar(const int*,const int*) 

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

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

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

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

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

最後,用剛纔獲得的函數指針類型CMPFUNC,以Python回調函數的函數名做爲構造函數的參數,就獲得了能夠用於C函數的函數指針變量:

p_c_cmp_func = CMPFUNC(py_cmp_func) 

完整的代碼以下:

from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def py_cmp_func(a, b): print type(a) print "py_cmp_func", a[0], b[0] return a[0] - b[0] type_array_5 = c_int * 5 ia = type_array_5(5, 1, 7, 33, 99) clib.qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func)) 

注意到,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 * from ctypes import wintypes WNDENUMPROC = WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) user32 = windll.LoadLibrary('user32.dll') def EnumWindowsProc(hwnd, lParam): length = user32.GetWindowTextLengthW(hwnd) + 1 buffer = create_unicode_buffer(length) user32.GetWindowTextW(hwnd, buffer, length) print buffer.value return True user32.EnumWindows(WNDENUMPROC(EnumWindowsProc), 0) 

7. 其餘

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

相關文章
相關標籤/搜索