python內置模塊之ctype


ctypes --- Python 的外部函數庫


ctypes 是 Python 的外部函數庫。它提供了與 C 兼容的數據類型,並允許調用 DLL 或共享庫中的函數。可使用該模塊以純 Python 形式對這些庫進行封裝。

ctypes 教程

注意:在本教程中的示例代碼使用 doctest 進行過測試,保證其正確運行。由於有些代碼在 Linux,Windows 或 Mac OS X 下的表現不同,這些代碼會在 doctest 中包含相關的指令注解。

注意:部分示例代碼引用了 ctypes c_int 類型。在 sizeof(long) == sizeof(int) 的平台上此類型是 c_long 的一個別名。所以,在程序輸出 c_long 而不是你期望的 c_int 時不必感到迷惑 --- 它們實際上是同一種類型。

操作導入的動態鏈接庫中的函數

通過操作dll對象的屬性來操作這些函數。

>>>
>>> from ctypes import * >>> libc.printf <_FuncPtr object at 0x...> >>> print(windll.kernel32.GetModuleHandleA) <_FuncPtr object at 0x...> >>> print(windll.kernel32.MyOwnFunction) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "ctypes.py", line 239, in __getattr__ func = _StdcallFuncPtr(name, self) AttributeError: function 'MyOwnFunction' not found >>> 

注意:Win32 系統的動態庫,比如 kernel32 和 user32,通常會同時導出同一個函數的 ANSI 版本和 UNICODE 版本。UNICODE 版本通常會在名字最后以 W 結尾,而 ANSI 版本的則以 A 結尾。 win32的 GetModuleHandle 函數會根據一個模塊名返回一個 模塊句柄,該函數暨同時包含這樣的兩個版本的原型函數,並通過宏 UNICODE 是否定義,來決定宏 GetModuleHandle 導出的是哪個具體函數。

/* ANSI version */ HMODULE GetModuleHandleA(LPCSTR lpModuleName); /* UNICODE version */ HMODULE GetModuleHandleW(LPCWSTR lpModuleName); 

windll 不會通過這樣的魔法手段來幫你決定選擇哪一種函數,你必須顯式的調用 GetModuleHandleA 或 GetModuleHandleW,並分別使用字節對象或字符串對象作參數。

有時候,dlls的導出的函數名不符合 Python 的標識符規范,比如 "??2@YAPAXI@Z"。此時,你必須使用 getattr() 方法來獲得該函數。

>>>
>>> getattr(cdll.msvcrt, "??2@YAPAXI@Z") <_FuncPtr object at 0x...> >>> 

Windows 下,有些 dll 導出的函數沒有函數名,而是通過其順序號調用。對此類函數,你也可以通過 dll 對象的數值索引來操作這些函數。

>>>
>>> cdll.kernel32[1] <_FuncPtr object at 0x...> >>> cdll.kernel32[0] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "ctypes.py", line 310, in __getitem__ func = _StdcallFuncPtr(name, self) AttributeError: function ordinal 0 not found >>> 

調用函數

你可以貌似是調用其它 Python 函數那樣直接調用這些函數。在這個例子中,我們調用了 time() 函數,該函數返回一個系統時間戳(從 Unix 時間起點到現在的秒數),而``GetModuleHandleA()`` 函數返回一個 win32 模塊句柄。

此函數中調用的兩個函數都使用了空指針(用 None 作為空指針):

>>>
>>> print(libc.time(None)) 1150640792 >>> print(hex(windll.kernel32.GetModuleHandleA(None))) 0x1d000000 >>> 

如果你用 cdecl 調用方式調用 stdcall 約定的函數,則會甩出一個異常 ValueError。反之亦然。

>>>
>>> cdll.kernel32.GetModuleHandleA(None) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: Procedure probably called with not enough arguments (4 bytes missing) >>> >>> windll.msvcrt.printf(b"spam") Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: Procedure probably called with too many arguments (4 bytes in excess) >>> 

你必須閱讀這些庫的頭文件或說明文檔來確定它們的正確的調用協議。

在 Windows 中,ctypes 使用 win32 結構化異常處理來防止由於在調用函數時使用非法參數導致的程序崩潰。

>>>
>>> windll.kernel32.GetModuleHandleA(32) Traceback (most recent call last): File "<stdin>", line 1, in <module> OSError: exception: access violation reading 0x00000020 >>> 

然而,總有許多辦法,通過調用 ctypes 使得 Python 程序崩潰。因此,你必須小心使用。 faulthandler 模塊可以用於幫助診斷程序崩潰的原因。(比如由於錯誤的C庫函數調用導致的段錯誤)。

None,整型,字節對象和(UNICODE)字符串是僅有的可以直接作為函數參數使用的四種Python本地數據類型。None` 作為C的空指針 (NULL),字節和字符串類型作為一個指向其保存數據的內存塊指針 (char* 或 wchar_t*)。Python 的整型則作為平台默認的C的 int 類型,他們的數值被截斷以適應C類型的整型長度。

在我們開始調用函數前,我們必須先了解作為函數參數的 ctypes 數據類型。

基礎數據類型

ctypes 定義了一些和C兼容的基本數據類型:

ctypes 類型

C 類型

Python 類型

c_bool

_Bool

bool (1)

c_char

char

單字符字節串對象

c_wchar

wchar_t

單字符字符串

c_byte

char

int

c_ubyte

unsigned char

int

c_short

short

int

c_ushort

unsigned short

int

c_int

int

int

c_uint

unsigned int

int

c_long

long

int

c_ulong

unsigned long

int

c_longlong

__int64 或 long long

int

c_ulonglong

unsigned __int64 或 unsigned long long

int

c_size_t

size_t

int

c_ssize_t

ssize_t 或 Py_ssize_t

int

c_float

float

float

c_double

double

float

c_longdouble

long double

float

c_char_p

char* (以 NUL 結尾)

字節串對象或 None

c_wchar_p

wchar_t* (以 NUL 結尾)

字符串或 None

c_void_p

void*

int 或 None

  1. 構造函數接受任何具有真值的對象。

所有這些類型都可以通過使用正確類型和值的可選初始值調用它們來創建:

>>>
>>> c_int() c_long(0) >>> c_wchar_p("Hello, World") c_wchar_p(140018365411392) >>> c_ushort(-3) c_ushort(65533) >>> 

由於這些類型是可變的,它們的值也可以在以后更改:

>>>
>>> i = c_int(42) >>> print(i) c_long(42) >>> print(i.value) 42 >>> i.value = -99 >>> print(i.value) -99 >>> 

當給指針類型的對象 c_char_pc_wchar_p 和 c_void_p 等賦值時,將改變它們所指向的 內存地址,而 不是 它們所指向的內存區域的 內容 (這是理所當然的,因為 Python 的 bytes 對象是不可變的):

>>>
>>> s = "Hello, World" >>> c_s = c_wchar_p(s) >>> print(c_s) c_wchar_p(139966785747344) >>> print(c_s.value) Hello World >>> c_s.value = "Hi, there" >>> print(c_s) # the memory location has changed c_wchar_p(139966783348904) >>> print(c_s.value) Hi, there >>> print(s) # first object is unchanged Hello, World >>> 

但你要注意不能將它們傳遞給會改變指針所指內存的函數。如果你需要可改變的內存塊,ctypes 提供了 create_string_buffer() 函數,它提供多種方式創建這種內存塊。當前的內存塊內容可以通過 raw 屬性存取,如果你希望將它作為NUL結束的字符串,請使用 value 屬性:

>>>
>>> from ctypes import * >>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes >>> print(sizeof(p), repr(p.raw)) 3 b'\x00\x00\x00' >>> p = create_string_buffer(b"Hello") # create a buffer containing a NUL terminated string >>> print(sizeof(p), repr(p.raw)) 6 b'Hello\x00' >>> print(repr(p.value)) b'Hello' >>> p = create_string_buffer(b"Hello", 10) # create a 10 byte buffer >>> print(sizeof(p), repr(p.raw)) 10 b'Hello\x00\x00\x00\x00\x00' >>> p.value = b"Hi" >>> print(sizeof(p), repr(p.raw)) 10 b'Hi\x00lo\x00\x00\x00\x00\x00' >>> 

create_string_buffer() 函數替代以前的ctypes版本中的 c_buffer() 函數 (仍然可當作別名使用)和 c_string() 函數。create_unicode_buffer() 函數創建包含 unicode 字符的可變內存塊,與之對應的C語言類型是 wchar_t

調用函數,繼續

注意 printf 將打印到真正標准輸出設備,而*不是* sys.stdout,因此這些實例只能在控制台提示符下工作,而不能在 IDLE 或 PythonWin 中運行。

>>>
>>> printf = libc.printf >>> printf(b"Hello, %s\n", b"World!") Hello, World! 14 >>> printf(b"Hello, %S\n", "World!") Hello, World! 14 >>> printf(b"%d bottles of beer\n", 42) 42 bottles of beer 19 >>> printf(b"%f bottles of beer\n", 42.5) Traceback (most recent call last): File "<stdin>", line 1, in <module> ArgumentError: argument 2: exceptions.TypeError: Don't know how to convert parameter 2 >>> 

正如前面所提到過的,除了整數、字符串以及字節串之外,所有的 Python 類型都必須使用它們對應的 ctypes 類型包裝,才能夠被正確地轉換為所需的C語言類型。

>>>
>>> printf(b"An int %d, a double %f\n", 1234, c_double(3.14)) An int 1234, a double 3.140000 31 >>> 

使用自定義的數據類型調用函數

你也可以通過自定義 ctypes 參數轉換方式來允許自定義類型作為參數。 ctypes 會尋找 _as_parameter_ 屬性並使用它作為函數參數。當然,它必須是數字、字符串或者二進制字符串:

>>>
>>> class Bottles: ... def __init__(self, number): ... self._as_parameter_ = number ... >>> bottles = Bottles(42) >>> printf(b"%d bottles of beer\n", bottles) 42 bottles of beer 19 >>> 

如果你不想把實例的數據存儲到 _as_parameter_ 屬性。可以通過定義 property 函數計算出這個屬性。

指定必選參數的類型(函數原型)

可以通過設置 argtypes 屬性的方法指定從 DLL 中導出函數的必選參數類型。

argtypes 必須是一個 C 數據類型的序列 (這里的 printf 可能不是個好例子,因為它是變長參數,而且每個參數的類型依賴於格式化字符串,不過嘗試這個功能也很方便):

>>>
>>> printf.argtypes = [c_char_p, c_char_p, c_int, c_double] >>> printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2) String 'Hi', Int 10, Double 2.200000 37 >>> 

指定數據類型可以防止不合理的參數傳遞(就像 C 函數的原型),並且會自動嘗試將參數轉換為需要的類型:

>>>
>>> printf(b"%d %d %d", 1, 2, 3) Traceback (most recent call last): File "<stdin>", line 1, in <module> ArgumentError: argument 2: exceptions.TypeError: wrong type >>> printf(b"%s %d %f\n", b"X", 2, 3) X 2 3.000000 13 >>> 

如果你想通過自定義類型傳遞參數給函數,必須實現 from_param() 類方法,才能夠將此自定義類型用於 argtypes 序列。from_param() 類方法接受一個 Python 對象作為函數輸入,它應該進行類型檢查或者其他必要的操作以保證接收到的對象是合法的,然后返回這個對象,或者它的 _as_parameter_ 屬性,或者其他你想要傳遞給 C 函數的參數。這里也一樣,返回的結果必須是整型、字符串、二進制字符串、 ctypes 類型,或者一個具有 _as_parameter_ 屬性的對象。

返回類型

默認情況下都會假定函數返回 C int 類型。 其他返回類型可以通過設置函數對象的 restype 屬性來指定。

這是個更高級的例子,它調用了 strchr 函數,這個函數接收一個字符串指針以及一個字符作為參數,返回另一個字符串指針。

>>>
>>> strchr = libc.strchr >>> strchr(b"abcdef", ord("d")) 8059983 >>> strchr.restype = c_char_p # c_char_p is a pointer to a string >>> strchr(b"abcdef", ord("d")) b'def' >>> print(strchr(b"abcdef", ord("x"))) None >>> 

如果希望避免上述的 ord("x") 調用,可以設置 argtypes 屬性,第二個參數就會將單字符的 Python 二進制字符對象轉換為 C 字符:

>>>
>>> strchr.restype = c_char_p >>> strchr.argtypes = [c_char_p, c_char] >>> strchr(b"abcdef", b"d") 'def' >>> strchr(b"abcdef", b"def") Traceback (most recent call last): File "<stdin>", line 1, in <module> ArgumentError: argument 2: exceptions.TypeError: one character string expected >>> print(strchr(b"abcdef", b"x")) None >>> strchr(b"abcdef", b"d") 'def' >>> 

如果外部函數返回了一個整數,你也可以使用要給可調用的 Python 對象(比如函數或者類)作為 restype 屬性的值。將會以 C 函數返回的 整數 對象作為參數調用這個可調用對象,執行后的結果作為最終函數返回值。這在錯誤返回值校驗和自動拋出異常等方面比較有用。

>>>
>>> GetModuleHandle = windll.kernel32.GetModuleHandleA >>> def ValidHandle(value): ... if value == 0: ... raise WinError() ... return value ... >>> >>> GetModuleHandle.restype = ValidHandle >>> GetModuleHandle(None) 486539264 >>> GetModuleHandle("something silly") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in ValidHandle OSError: [Errno 126] The specified module could not be found. >>> 

WinError 函數可以調用 Windows 的 FormatMessage() API 獲取錯誤碼的字符串說明,然后 返回 一個異常。 WinError 接收一個可選的錯誤碼作為參數,如果沒有的話,它將調用 GetLastError() 獲取錯誤碼。

請注意,使用 errcheck 屬性可以實現更強大的錯誤檢查手段;詳情請見參考手冊。

傳遞指針(或以引用方式傳遞形參)

有時候 C 函數接口可能由於要往某個地址寫入值,或者數據太大不適合作為值傳遞,從而希望接收一個 指針 作為數據參數類型。這和 傳遞參數引用 類似。

ctypes 暴露了 byref() 函數用於通過引用傳遞參數,使用 pointer() 函數也能達到同樣的效果,只不過 pointer() 需要更多步驟,因為它要先構造一個真實指針對象。所以在 Python 代碼本身不需要使用這個指針對象的情況下,使用 byref() 效率更高。

>>>
>>> i = c_int() >>> f = c_float() >>> s = create_string_buffer(b'\000' * 32) >>> print(i.value, f.value, repr(s.value)) 0 0.0 b'' >>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s", ... byref(i), byref(f), s) 3 >>> print(i.value, f.value, repr(s.value)) 1 3.1400001049 b'Hello' >>> 

結構體和聯合

結構體和聯合必須繼承自 ctypes 模塊中的 Structure 和 Union 。子類必須定義 _fields_ 屬性。 _fields_ 是一個二元組列表,二元組中包含 field name 和 field type 。

type 字段必須是一個 ctypes 類型,比如 c_int,或者其他 ctypes 類型: 結構體、聯合、數組、指針。

這是一個簡單的 POINT 結構體,它包含名稱為 x 和 y 的兩個變量,還展示了如何通過構造函數初始化結構體。

>>>
>>> from ctypes import * >>> class POINT(Structure): ... _fields_ = [("x", c_int), ... ("y", c_int)] ... >>> point = POINT(10, 20) >>> print(point.x, point.y) 10 20 >>> point = POINT(y=5) >>> print(point.x, point.y) 0 5 >>> POINT(1, 2, 3) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: too many initializers >>> 

當然,你可以構造更復雜的結構體。一個結構體可以通過設置 type 字段包含其他結構體或者自身。

這是以一個 RECT 結構體,他包含了兩個 POINT ,分別叫 upperleft 和 lowerright:

>>>
>>> class RECT(Structure): ... _fields_ = [("upperleft", POINT), ... ("lowerright", POINT)] ... >>> rc = RECT(point) >>> print(rc.upperleft.x, rc.upperleft.y) 0 5 >>> print(rc.lowerright.x, rc.lowerright.y) 0 0 >>> 

嵌套結構體可以通過幾種方式構造初始化:

>>>
>>> r = RECT(POINT(1, 2), POINT(3, 4)) >>> r = RECT((1, 2), (3, 4)) 

可以通過  獲取字段 descriptor ,它能提供很多有用的調試信息。

>>>
>>> print(POINT.x) <Field type=c_long, ofs=0, size=4> >>> print(POINT.y) <Field type=c_long, ofs=4, size=4> >>> 

警告

 

ctypes 不支持帶位域的結構體、聯合以值的方式傳給函數。這可能在 32 位 x86 平台上可以正常工作,但是對於一般情況,這種行為是未定義的。帶位域的結構體、聯合應該總是通過指針傳遞給函數。

結構體/聯合字段對齊及字節順序

默認情況下,結構體和聯合的字段與 C 的字節對齊是一樣的。也可以在定義子類的時候指定類的 _pack_ 屬性來覆蓋這種行為。 它必須設置為一個正整數,表示字段的最大對齊字節。這和 MSVC 中的 #pragma pack(n) 功能一樣。

ctypes 中的結構體和聯合使用的是本地字節序。要使用非本地字節序,可以使用 BigEndianStructureLittleEndianStructureBigEndianUnion, and LittleEndianUnion 作為基類。這些類不能包含指針字段。

結構體和聯合中的位域

結構體和聯合中是可以包含位域字段的。位域只能用於整型字段,位長度通過 _fields_ 中的第三個參數指定:

>>>
>>> class Int(Structure): ... _fields_ = [("first_16", c_int, 16), ... ("second_16", c_int, 16)] ... >>> print(Int.first_16) <Field type=c_long, ofs=0:0, bits=16> >>> print(Int.second_16) <Field type=c_long, ofs=0:16, bits=16> >>> 

數組

數組是一個序列,包含指定個數元素,且必須類型相同。

創建數組類型的推薦方式是使用一個類型乘以一個正數:

TenPointsArrayType = POINT * 10 

下面是一個構造的數據案例,結構體中包含了4個 POINT 和一些其他東西。

>>>
>>> from ctypes import * >>> class POINT(Structure): ... _fields_ = ("x", c_int), ("y", c_int) ... >>> class MyStruct(Structure): ... _fields_ = [("a", c_int), ... ("b", c_float), ... ("point_array", POINT * 4)] >>> >>> print(len(MyStruct().point_array)) 4 >>> 

和平常一樣,通過調用它創建實例:

arr = TenPointsArrayType() for pt in arr: print(pt.x, pt.y) 

以上代碼會打印幾行 0 ,因為數組內容被初始化為 0.

也能通過指定正確類型的數據來初始化:

>>>
>>> from ctypes import * >>> TenIntegers = c_int * 10 >>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) >>> print(ii) <c_long_Array_10 object at 0x...> >>> for i in ii: print(i, end=" ") ... 1 2 3 4 5 6 7 8 9 10 >>> 

指針

可以將 ctypes 類型數據傳入 pointer() 函數創建指針:

>>>
>>> from ctypes import * >>> i = c_int(42) >>> pi = pointer(i) >>> 

指針實例擁有 contents 屬性,它返回指針指向的真實對象,如上面的 i 對象:

>>>
>>> pi.contents c_long(42) >>> 

注意 ctypes 並沒有 OOR (返回原始對象), 每次訪問這個屬性時都會構造返回一個新的相同對象:

>>>
>>> pi.contents is i False >>> pi.contents is pi.contents False >>> 

將這個指針的 contents 屬性賦值為另一個 c_int 實例將會導致該指針指向該實例的內存地址:

>>>
>>> i = c_int(99) >>> pi.contents = i >>> pi.contents c_long(99) >>> 

指針對象也可以通過整數下標進行訪問:

>>>
>>> pi[0] 99 >>> 

通過整數下標賦值可以改變指針所指向的真實內容:

>>>
>>> print(i) c_long(99) >>> pi[0] = 22 >>> print(i) c_long(22) >>> 

使用 0 以外的索引也是合法的,但是你必須確保知道自己為什么這么做,就像 C 語言中: 你可以訪問或者修改任意內存內容。 通常只會在函數接收指針是才會使用這種特性,而且你 知道 這個指針指向的是一個數組而不是單個值。

內部細節, pointer() 函數不只是創建了一個指針實例,它首先創建了一個指針 類型 。這是通過調用 POINTER() 函數實現的,它接收 ctypes 類型為參數,返回一個新的類型:

>>>
>>> PI = POINTER(c_int) >>> PI <class 'ctypes.LP_c_long'> >>> PI(42) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: expected c_long instead of int >>> PI(c_int(42)) <ctypes.LP_c_long object at 0x...> >>> 

無參調用指針類型可以創建一個 NULL 指針。 NULL 指針的布爾值是 False

>>>
>>> null_ptr = POINTER(c_int)() >>> print(bool(null_ptr)) False >>> 

解引用指針的時候, ctypes 會幫你檢測是否指針為 NULL (但是解引用無效的 非 NULL 指針仍會導致 Python 崩潰):

>>>
>>> null_ptr[0] Traceback (most recent call last): .... ValueError: NULL pointer access >>> >>> null_ptr[0] = 1234 Traceback (most recent call last): .... ValueError: NULL pointer access >>> 

類型轉換

通常情況下, ctypes 具有嚴格的類型檢查。這代表着, 如果在函數 argtypes 中或者結構體定義成員中有 POINTER(c_int) 類型,只有相同類型的實例才會被接受。 也有一些例外。比如,你可以傳遞兼容的數組實例給指針類型。所以,對於 POINTER(c_int) ,ctypes 也可以接受 c_int 類型的數組:

>>>
>>> class Bar(Structure): ... _fields_ = [("count", c_int), ("values", POINTER(c_int))] ... >>> bar = Bar() >>> bar.values = (c_int * 3)(1, 2, 3) >>> bar.count = 3 >>> for i in range(bar.count): ... print(bar.values[i]) ... 1 2 3 >>> 

另外,如果一個函數 argtypes 列表中的參數顯式的定義為指針類型(如 POINTER(c_int) ),指針所指向的 類型 (這個例子中是 c_int )也可以傳遞給函數。ctypes 會自動調用對應的 byref() 轉換。

可以給指針內容賦值為 None 將其設置為 Null

>>>
>>> bar.values = None >>> 

有時候你擁有一個不兼容的類型。 在 C 中,你可以將一個類型強制轉換為另一個。 ctypes 中的 a cast() 函數提供了相同的功能。 上面的結構體 Bar 的 value 字段接收 POINTER(c_int) 指針或者 c_int 數組,但是不能接受其他類型的實例:

>>>
>>> bar.values = (c_byte * 4)() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: incompatible types, c_byte_Array_4 instance instead of LP_c_long instance >>> 

這種情況下, 需要手動使用 cast() 函數。

cast() 函數可以將一個指針實例強制轉換為另一種 ctypes 類型。 cast() 接收兩個參數,一個 ctypes 指針對象或者可以被轉換為指針的其他類型對象,和一個 ctypes 指針類型。 返回第二個類型的一個實例,該返回實例和第一個參數指向同一片內存空間:

>>>
>>> a = (c_byte * 4)() >>> cast(a, POINTER(c_int)) <ctypes.LP_c_long object at ...> >>> 

所以 cast() 可以用來給結構體 Bar 的 values 字段賦值:

>>>
>>> bar = Bar() >>> bar.values = cast((c_byte * 4)(), POINTER(c_int)) >>> print(bar.values[0]) 0 >>> 

不完整類型

不完整類型 即還沒有定義成員的結構體、聯合或者數組。在 C 中,它們通常用於前置聲明,然后在后面定義:

struct cell; /* forward declaration */ struct cell { char *name; struct cell *next; }; 

直接翻譯成 ctypes 的代碼如下,但是這行不通:

>>>
>>> class cell(Structure): ... _fields_ = [("name", c_char_p), ... ("next", POINTER(cell))] ... Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in cell NameError: name 'cell' is not defined >>> 

因為新的 cell  在 class 語句結束之前還沒有完成定義。在 ctypes 中,我們可以先定義 cell 類,在 class 語句結束之后再設置 _fields_ 屬性:

>>>
>>> from ctypes import * >>> class cell(Structure): ... pass ... >>> cell._fields_ = [("name", c_char_p), ... ("next", POINTER(cell))] >>> 

讓我們試試。我們定義兩個 cell 實例,讓它們互相指向對方,然后通過指針鏈式訪問幾次:

>>>
>>> c1 = cell() >>> c1.name = b"foo" >>> c2 = cell() >>> c2.name = b"bar" >>> c1.next = pointer(c2) >>> c2.next = pointer(c1) >>> p = c1 >>> for i in range(8): ... print(p.name, end=" ") ... p = p.next[0] ... foo bar foo bar foo bar foo bar >>> 

回調函數

ctypes 允許創建一個指向 Python 可調用對象的 C 函數。它們有時候被稱為 回調函數 。

首先,你必須為回調函數創建一個類,這個類知道調用約定,包括返回值類型以及函數接收的參數類型及個數。

CFUNCTYPE() 工廠函數使用 cdecl 調用約定創建回調函數類型。在 Windows 上, WINFUNCTYPE() 工廠函數使用 stdcall 調用約定為回調函數創建類型。

這些工廠函數的第一個參數是返回值類型,回調函數的參數類型作為剩余參數。

這里展示一個使用 C 標准庫函數 qsort() 的例子,它使用一個回調函數對數據進行排序。 qsort() 將用來給整數數組排序:

>>>
>>> IntArray5 = c_int * 5 >>> ia = IntArray5(5, 1, 7, 33, 99) >>> qsort = libc.qsort >>> qsort.restype = None >>> 

qsort() 必須接收的參數,一個指向待排序數據的指針,元素個數,每個元素的大小,以及一個指向排序函數的指針,即回調函數。然后回調函數接收兩個元素的指針,如果第一個元素小於第二個,則返回一個負整數,如果相等則返回0,否則返回一個正整數。

所以,我們的回調函數要接收兩個整數指針,返回一個整數。首先我們創建回調函數的 類型

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

首先,這是一個簡單的回調,它會顯示傳入的值:

>>>
>>> def py_cmp_func(a, b): ... print("py_cmp_func", a[0], b[0]) ... return 0 ... >>> cmp_func = CMPFUNC(py_cmp_func) >>> 

結果:

>>>
>>> qsort(ia, len(ia), sizeof(c_int), cmp_func) py_cmp_func 5 1 py_cmp_func 33 99 py_cmp_func 7 33 py_cmp_func 5 7 py_cmp_func 1 7 >>> 

現在我們可以比較兩個元素並返回有用的結果了:

>>>
>>> def py_cmp_func(a, b): ... print("py_cmp_func", a[0], b[0]) ... return a[0] - b[0] ... >>> >>> qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func)) py_cmp_func 5 1 py_cmp_func 33 99 py_cmp_func 7 33 py_cmp_func 1 7 py_cmp_func 5 7 >>> 

我們可以輕易地驗證,現在數組是有序的了:

>>>
>>> for i in ia: print(i, end=" ") ... 1 5 7 33 99 >>> 

這些工廠函數可以當作裝飾器工廠,所以可以這樣寫:

>>>
>>> @CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) ... def py_cmp_func(a, b): ... print("py_cmp_func", a[0], b[0]) ... return a[0] - b[0] ... >>> qsort(ia, len(ia), sizeof(c_int), py_cmp_func) py_cmp_func 5 1 py_cmp_func 33 99 py_cmp_func 7 33 py_cmp_func 1 7 py_cmp_func 5 7 >>> 

注解

 

請確保你維持的 CFUNCTYPE() 對象的引用周期與它們在 C 代碼中的使用期一樣長。 ctypes 不會確保這一點,如果不這樣做,它們可能會被垃圾回收,導致程序在執行回調函數時發生崩潰。

注意,如果回調函數在Python之外的另外一個線程使用(比如,外部代碼調用這個回調函數), ctypes 會在每一次調用上創建一個虛擬 Python 線程。這個行為在大多數情況下是合理的,但也意味着如果有數據使用 threading.local 方式存儲,將無法訪問,就算它們是在同一個 C 線程中調用的 。

訪問 dll 的導出變量

一些動態鏈接庫不僅僅導出函數,也會導出變量。一個例子就是 Python 庫本身的 Py_OptimizeFlag ,根據啟動選項 -O 、 -OO 的不同,它是值可能為 0、1、2 的整型。

ctypes 可以通過 in_dll() 類方法訪問這類變量 。 pythonapi 是用於訪問 Python C 接口的預定義符號:

>>>
>>> opt_flag = c_int.in_dll(pythonapi, "Py_OptimizeFlag") >>> print(opt_flag) c_long(0) >>> 

如果解釋器使用 -O 選項啟動,這個例子會打印 c_long(1) , 如果使用 -OO 啟動,則會打印 c_long(2) 。

一個擴展例子, 同時也展示了使用指針訪問 Python 導出的 PyImport_FrozenModules 指針對象。

對文檔中這個值的解釋說明

該指針被初始化為指向 struct _frozen 數組,以 NULL 或者 0 作為結束標記。當一個凍結模塊被導入,首先要在這個表中搜索。第三方庫可以以此來提供動態創建的凍結模塊集合。

這足以證明修改這個指針是很有用的。為了讓實例大小不至於太長,這里只展示如何使用 ctypes 讀取這個表:

>>>
>>> from ctypes import * >>> >>> class struct_frozen(Structure): ... _fields_ = [("name", c_char_p), ... ("code", POINTER(c_ubyte)), ... ("size", c_int)] ... >>> 

我們定義了 struct _frozen  數據類型,接着就可以獲取這張表的指針了:

>>>
>>> FrozenTable = POINTER(struct_frozen) >>> table = FrozenTable.in_dll(pythonapi, "PyImport_FrozenModules") >>> 

由於 table 是指向 struct_frozen 數組的 指針 ,我們可以遍歷它,只不過需要自己判斷循環是否結束,因為指針本身並不包含長度。它早晚會因為訪問到野指針或者什么的把自己搞崩潰,所以我們最好在遇到 NULL 后就讓它退出循環:

>>>
>>> for item in table: ... if item.name is None: ... break ... print(item.name.decode("ascii"), item.size) ... _frozen_importlib 31764 _frozen_importlib_external 41499 __hello__ 161 __phello__ -161 __phello__.spam 161 >>> 

Python 的凍結模塊和凍結包(由負 size 成員表示)並不是廣為人知的事情,它們僅僅用於實驗。例如,可以使用 import __hello__ 嘗試一下這個功能。

意外

ctypes 也有自己的邊界,有時候會發生一些意想不到的事情。

比如下面的例子:

>>>
>>> from ctypes import * >>> class POINT(Structure): ... _fields_ = ("x", c_int), ("y", c_int) ... >>> class RECT(Structure): ... _fields_ = ("a", POINT), ("b", POINT) ... >>> p1 = POINT(1, 2) >>> p2 = POINT(3, 4) >>> rc = RECT(p1, p2) >>> print(rc.a.x, rc.a.y, rc.b.x, rc.b.y) 1 2 3 4 >>> # now swap the two points >>> rc.a, rc.b = rc.b, rc.a >>> print(rc.a.x, rc.a.y, rc.b.x, rc.b.y) 3 4 3 4 >>> 

嗯。我們預想應該打印 2 。但是為什么呢? 這是 rc.a, rc.b rc.b, rc.a 這行代碼展開后的步驟:

>>>
>>> temp0, temp1 = rc.b, rc.a >>> rc.a = temp0 >>> rc.b = temp1 >>> 

注意 temp0 和 temp1 對象始終引用了對象 rc 的內容。然后執行 rc.a temp0 會把 temp0 的內容拷貝到 rc 的空間。這也改變了 temp1 的內容。最終導致賦值語句 rc.b temp1 沒有產生預想的效果。

記住,訪問被包含在結構體、聯合、數組中的對象並不會將其 復制 出來,而是得到了一個代理對象,它是對根對象的內部內容的一層包裝。

下面是另一個可能和預期有偏差的例子:

>>>
>>> s = c_char_p() >>> s.value = b"abc def ghi" >>> s.value b'abc def ghi' >>> s.value is s.value False >>> 

注解

 

使用 c_char_p  實例化的對象只能將其值設置為 bytes 或者整數。

為什么這里打印了 False ? ctypes 實例是一些內存塊加上一些用於訪問這些內存塊的 descriptor 組成。將 Python 對象存儲在內存塊並不會存儲對象本身,而是存儲了對象的 內容 。每次訪問對象的內容都會構造一個新的 Python 對象。

變長數據類型

ctypes 對變長數組和結構體提供了一些支持 。

The resize() function can be used to resize the memory buffer of an existing ctypes object. The function takes the object as first argument, and the requested size in bytes as the second argument. The memory block cannot be made smaller than the natural memory block specified by the objects type, a ValueError is raised if this is tried:

>>>
>>> short_array = (c_short * 4)() >>> print(sizeof(short_array)) 8 >>> resize(short_array, 4) Traceback (most recent call last): ... ValueError: minimum size is 8 >>> resize(short_array, 32) >>> sizeof(short_array) 32 >>> sizeof(type(short_array)) 8 >>> 

這非常好,但是要怎么訪問數組中額外的元素呢?因為數組類型已經定義包含4個元素,導致我們訪問新增元素時會產生以下錯誤:

>>>
>>> short_array[:] [0, 0, 0, 0] >>> short_array[7] Traceback (most recent call last): ... IndexError: invalid index >>> 

使用 ctypes 訪問變長數據類型的一個可行方法是利用 Python 的動態特性,根據具體情況,在知道這個數據的大小后,(重新)指定這個數據的類型。

ctypes 參考手冊

尋找動態鏈接庫

在編譯型語言中,動態鏈接庫會在編譯、鏈接或者程序運行時訪問。

The purpose of the find_library() function is to locate a library in a way similar to what the compiler or runtime loader does (on platforms with several versions of a shared library the most recent should be loaded), while the ctypes library loaders act like when a program is run, and call the runtime loader directly.

ctypes.util 模塊提供了一個函數,可以幫助確定需要加載的庫。

ctypes.util. find_library (name)

嘗試尋找一個庫然后返回其路徑名, name 是庫名稱, 且去除了 lib 等前綴和 .so 、 .dylib 、版本號等后綴(這是 posix 連接器 -l 選項使用的格式)。如果沒有找到對應的庫,則返回 None 。

確切的功能取決於系統。

在 Linux 上, find_library() 會嘗試運行外部程序(/sbin/ldconfiggccobjdump 以及 ld) 來尋找庫文件。返回庫文件的文件名。

在 3.6 版更改: 在Linux 上,如果其他方式找不到的話,會使用環境變量 LD_LIBRARY_PATH 搜索動態鏈接庫。

這是一些例子:

>>>
>>> from ctypes.util import find_library >>> find_library("m") 'libm.so.6' >>> find_library("c") 'libc.so.6' >>> find_library("bz2") 'libbz2.so.1.0' >>> 

在 OS X 上, find_library() 會嘗試幾種預定義的命名方案和路徑來查找庫,如果成功,則返回完整的路徑名:

>>>
>>> from ctypes.util import find_library >>> find_library("c") '/usr/lib/libc.dylib' >>> find_library("m") '/usr/lib/libm.dylib' >>> find_library("bz2") '/usr/lib/libbz2.dylib' >>> find_library("AGL") '/System/Library/Frameworks/AGL.framework/AGL' >>> 

在 Windows 上, find_library() 在系統路徑中搜索,然后返回全路徑,但是如果沒有預定義的命名方案, find_library("c") 調用會返回 None

使用 ctypes 包裝動態鏈接庫,更好的方式 可能 是在開發的時候就確定名稱,然后硬編碼到包裝模塊中去,而不是在運行時使用 find_library() 尋找庫。

加載動態鏈接庫

有很多方式可以將動態鏈接庫加載到 Python 進程。其中之一是實例化以下類的其中一個:

class  ctypes. CDLL (namemode=DEFAULT_MODEhandle=Noneuse_errno=Falseuse_last_error=Falsewinmode=0)

此類的實例即已加載的動態鏈接庫。庫中的函數使用標准 C 調用約定,並假定返回 int 。

在 Windows 上創建 CDLL 實例可能會失敗,即使 DLL 名稱確實存在。 當某個被加載 DLL 所依賴的 DLL 未找到時,將引發 OSError 錯誤並附帶消息 "[WinError 126] The specified module could not be found". 此錯誤消息不包含缺失 DLL 的名稱,因為 Windows API 並不會返回此類信息,這使得此錯誤難以診斷。 要解決此錯誤並確定是哪一個 DLL 未找到,你需要找出所依賴的 DLL 列表並使用 Windows 調試與跟蹤工具確定是哪一個未找到。

參見

 

Microsoft DUMPBIN 工具 -- 一個用於查找 DLL 依賴的工具。

class  ctypes. OleDLL (namemode=DEFAULT_MODEhandle=Noneuse_errno=Falseuse_last_error=Falsewinmode=0)

僅 Windows : 此類的實例即加載好的動態鏈接庫,其中的函數使用 stdcall 調用約定,並且假定返回 windows 指定的 HRESULT 返回碼。 HRESULT 的值包含的信息說明函數調用成功還是失敗,以及額外錯誤碼。 如果返回值表示失敗,會自動拋出 OSError 異常。

在 3.3 版更改: 以前是引發 WindowsError

class  ctypes. WinDLL (namemode=DEFAULT_MODEhandle=Noneuse_errno=Falseuse_last_error=Falsewinmode=0)

僅 Windows: 此類的實例即加載好的動態鏈接庫,其中的函數使用 stdcall 調用約定,並假定默認返回 int 。

在 Windows CE 上,只能使用 stdcall 調用約定,為了方便, WinDLL 和 OleDLL 在這個平台上都使用標准調用約定。

調用動態庫導出的函數之前,Python會釋放 global interpreter lock ,並在調用后重新獲取。

class  ctypes. PyDLL (namemode=DEFAULT_MODEhandle=None)

這個類實例的行為與 CDLL 類似,只不過 不會 在調用函數的時候釋放 GIL 鎖,且調用結束后會檢查 Python 錯誤碼。 如果錯誤碼被設置,會拋出一個 Python 異常。

所以,它只在直接調用 Python C 接口函數的時候有用。

通過使用至少一個參數(共享庫的路徑名)調用它們,可以實例化所有這些類。也可以傳入一個已加載的動態鏈接庫作為 handler 參數,其他情況會調用系統底層的 dlopen 或 LoadLibrary 函數將庫加載到進程,並獲取其句柄。

mode 可以指定庫加載方式。詳情請參見 dlopen(3) 手冊頁。 在 Windows 上, 會忽略 mode ,在 posix 系統上, 總是會加上 RTLD_NOW ,且無法配置。

use_errno 參數如果設置為 true,可以啟用ctypes的機制,通過一種安全的方法獲取系統的 errno 錯誤碼。 ctypes 維護了一個線程局部變量,它是系統 errno 的一份拷貝;如果調用了使用 use_errno=True 創建的外部函數, errno 的值會與 ctypes 自己拷貝的那一份進行交換,函數執行完后立即再交換一次。

The function ctypes.get_errno() returns the value of the ctypes private copy, and the function ctypes.set_errno() changes the ctypes private copy to a new value and returns the former value.

use_last_error 參數如果設置為 true,可以在 Windows 上啟用相同的策略,它是通過 Windows API 函數 GetLastError()  和 SetLastError() 管理的。 ctypes.get_last_error() 和 ctypes.set_last_error() 可用於獲取和設置 ctypes 自己維護的 windows 錯誤碼拷貝。

winmode 參數用於在 Windows 平台上指定庫的加載方式( 因為 mode 會被忽略)。他接受任何與 Win32 API 的 LoadLibraryEx 的標志兼容的值作為參數。省略時,默認設置使用最安全的DLL加載的標志,以避免DLL劫持等問題。傳入 DLL 的全路徑是保證正確加載庫及其依賴最安全的方法。

在 3.8 版更改: 增加了 winmode 參數。

ctypes. RTLD_GLOBAL

用於 mode 參數的標識值。在此標識不可用的系統上,它被定義為整數0。

ctypes. RTLD_LOCAL

Flag to use as mode parameter. On platforms where this is not available, it is the same as RTLD_GLOBAL.

ctypes. DEFAULT_MODE

加載動態鏈接庫的默認模式。在 OSX 10.3 上,它是 RTLD_GLOBAL ,其余系統上是 RTLD_LOCAL 。

這些類的實例沒有共用方法。動態鏈接庫的導出函數可以通過屬性或者索引的方式訪問。注意,通過屬性的方式訪問會緩存這個函數,因而每次訪問它時返回的都是同一個對象。另一方面,通過索引訪問,每次都會返回一個新的對象:

>>>
>>> from ctypes import CDLL >>> libc = CDLL("libc.so.6") # On Linux >>> libc.time == libc.time True >>> libc['time'] == libc['time'] False 

還有下面這些屬性可用,他們的名稱以下划線開頭,以避免和導出函數重名:

PyDLL. _handle

用於訪問庫的系統句柄。

PyDLL. _name

傳入構造函數的庫名稱。

共享庫也可以通用使用一個預制對象來加載,這種對象是 LibraryLoader 類的實例,具體做法或是通過調用 LoadLibrary() 方法,或是通過將庫作為加載器實例的屬性來提取。

class  ctypes. LibraryLoader (dlltype)

加載共享庫的類。 dlltype 應當為 CDLLPyDLLWinDLL 或 OleDLL 類型之一。

__getattr__() 具有特殊的行為:它允許通過將一個共享庫作為庫加載器實例的屬性進行訪問來加載它。 加載結果將被緩存,因此重復的屬性訪問每次都會返回相同的庫。

LoadLibrary (name)

加載一個共享庫到進程中並將其返回。 此方法總是返回一個新的庫實例。

可用的預制庫加載器有如下這些:

ctypes. cdll

創建 CDLL 實例。

ctypes. windll

僅限 Windows:創建 WinDLL 實例.

ctypes. oledll

僅限 Windows:創建 OleDLL 實例。

ctypes. pydll

創建 PyDLL 實例。

要直接訪問 C Python api,可以使用一個現成的 Python 共享庫對象:

ctypes. pythonapi

一個 PyDLL 的實例,它將 Python C API 函數作為屬性公開。 請注意所有這些函數都應返回 C int,當然這也不是絕對的,因此你必須分配正確的 restype 屬性以使用這些函數。

引發一個 審計事件 ctypes.dlopen,附帶參數 name

引發一個審計事件 ctypes.dlsym,附帶參數 libraryname

引發一個審計事件 ctypes.dlsym/handle,附帶參數 handlename

外部函數

正如之前小節的說明,外部函數可作為被加載共享庫的屬性來訪問。 用此方式創建的函數對象默認接受任意數量的參數,接受任意 ctypes 數據實例作為參數,並且返回庫加載器所指定的默認結果類型。 它們是一個私有類的實例:

class  ctypes. _FuncPtr

C 可調用外部函數的基類。

外部函數的實例也是兼容 C 的數據類型;它們代表 C 函數指針。

此行為可通過對外部函數對象的特殊屬性賦值來自定義。

restype

賦值為一個 ctypes 類型來指定外部函數的結果類型。 使用 None 表示 void,即不返回任何結果的函數。

賦值為一個不為 ctypes 類型的可調用 Python 對象也是可以的,在此情況下函數應返回 C int,該可調用對象將附帶此整數被調用,以允許進一步的處理或錯誤檢測。 這種用法已被棄用,為了更靈活的后續處理或錯誤檢測請使用一個 ctypes 數據類型作為 restype 並將 errcheck 屬性賦值為一個可調用對象。

argtypes

賦值為一個 ctypes 類型的元組來指定函數所接受的參數類型。 使用 stdcall 調用規范的函數只能附帶與此元組長度相同數量的參數進行調用;使用 C 調用規范的函數還可接受額外的未指明參數。

當外部函數被調用時,每個實際參數都會被傳給 argtypes 元組中條目的 from_param() 類方法,此方法允許將實際參數適配為此外部函數所接受的對象。 例如,argtypes 元組中的 c_char_p 條目將使用 ctypes 約定規則把作為參數傳入的字符串轉換為字節串對象。

新增:現在可以將不是 ctypes 類型的條目放入 argtypes,但每個條目都必須具有 from_param() 方法用於返回可作為參數的值(整數、字符串、ctypes 實例)。 這樣就允許定義可將自定義對象適配為函數形參的適配器。

errcheck

將一個 Python 函數或其他可調用對象賦值給此屬性。 該可調用對象將附帶三個及以上的參數被調用。

callable (resultfuncarguments)

result 是外部函數返回的結果,由 restype 屬性指明。

func 是外部函數對象本身,這樣就允許重新使用相同的可調用對象來對多個函數進行檢查或后續處理。

arguments 是一個包含最初傳遞給函數調用的形參的元組,這樣就允許對所用參數的行為進行特別處理。

此函數所返回的對象將會由外部函數調用返回,但它還可以在外部函數調用失敗時檢查結果並引發異常。

exception  ctypes. ArgumentError

此異常會在外部函數無法對某個傳入參數執行轉換時被引發。

引發一個審計事件 ctypes.seh_exception 並附帶參數 code

引發一個審計事件 ctypes.call_function,附帶參數 func_pointerarguments

函數原型

外部函數也可通過實例化函數原型來創建。 函數原型類似於 C 中的函數原型;它們在不定義具體實現的情況下描述了一個函數(返回類型、參數類型、調用約定)。 工廠函數必須使用函數所需要的結果類型和參數類型來調用,並可被用作裝飾器工廠函數,在此情況下可以通過 @wrapper 語法應用於函數。 請參閱 回調函數 了解有關示例。

ctypes. CFUNCTYPE (restype*argtypesuse_errno=Falseuse_last_error=False)

返回的函數原型會創建使用標准 C 調用約定的函數。 該函數在調用過程中將釋放 GIL。 如果 use_errno 設為真值,則在調用之前和之后系統 errno 變量的 ctypes 私有副本會與真正的 errno 值進行交換;use_last_error 會為 Windows 錯誤碼執行同樣的操作。

ctypes. WINFUNCTYPE (restype*argtypesuse_errno=Falseuse_last_error=False)

僅限 Windows only:返回的函數原型會創建使用 stdcall 調用約定的函數,但在 Windows CE 上 WINFUNCTYPE() 則會與 CFUNCTYPE() 相同。 該函數在調用過程中將釋放 GIL。 use_errno 和 use_last_error 具有與前面相同的含義。

ctypes. PYFUNCTYPE (restype*argtypes)

返回的函數原型會創建使用 Python 調用約定的函數。 該函數在調用過程中將 不會 釋放 GIL。

這些工廠函數所創建的函數原型可通過不同的方式來實例化,具體取決於調用中的類型與數量:

prototype (address)

在指定地址上返回一個外部函數,地址值必須為整數。

prototype (callable)

基於 Python callable 創建一個 C 可調用函數(回調函數)。

prototype (func_spec[, paramflags])

返回由一個共享庫導出的外部函數。 func_spec 必須為一個 2 元組 (name_or_ordinal, library)。 第一項是字符串形式的所導出函數名稱,或小整數形式的所導出函數序號。 第二項是該共享庫實例。

prototype (vtbl_indexname[, paramflags[, iid]])

返回將調用一個 COM 方法的外部函數。 vtbl_index 虛擬函數表中的索引。 name 是 COM 方法的名稱。 iid 是可選的指向接口標識符的指針,它被用於擴展的錯誤報告。

COM 方法使用特殊的調用約定:除了在 argtypes 元組中指定的形參,它們還要求一個指向 COM 接口的指針作為第一個參數。

可選的 paramflags 形參會創建相比上述特性具有更多功能的外部函數包裝器。

paramflags 必須為一個與 argtypes 長度相同的元組。

此元組中的每一項都包含有關形參的更多信息,它必須為包含一個、兩個或更多條目的元組。

第一項是包含形參指令旗標組合的整數。

1

指定函數的一個輸入形參。

2

輸出形參。 外部函數會填入一個值。

4

默認為整數零值的輸入形參。

可選的第二項是字符串形式的形參名稱。 如果指定此項,則可以使用該形參名稱來調用外部函數。

可選的第三項是該形參的默認值。

這個例子演示了如何包裝 Windows 的 MessageBoxW 函數以使其支持默認形參和已命名參數。 相應 windows 頭文件的 C 聲明是這樣的:

WINUSERAPI int WINAPI MessageBoxW( HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType); 

這是使用 ctypes 的包裝:

>>>
>>> from ctypes import c_int, WINFUNCTYPE, windll >>> from ctypes.wintypes import HWND, LPCWSTR, UINT >>> prototype = WINFUNCTYPE(c_int, HWND, LPCWSTR, LPCWSTR, UINT) >>> paramflags = (1, "hwnd", 0), (1, "text", "Hi"), (1, "caption", "Hello from ctypes"), (1, "flags", 0) >>> MessageBox = prototype(("MessageBoxW", windll.user32), paramflags) 

現在 MessageBox 外部函數可以通過以下方式來調用:

>>>
>>> MessageBox() >>> MessageBox(text="Spam, spam, spam") >>> MessageBox(flags=2, text="foo bar") 

第二個例子演示了輸出形參。 這個 win32 GetWindowRect 函數通過將指定窗口的維度拷貝至調用者必須提供的 RECT 結構體來提取這些值。 這是相應的 C 聲明:

WINUSERAPI BOOL WINAPI GetWindowRect( HWND hWnd, LPRECT lpRect); 

這是使用 ctypes 的包裝:

>>>
>>> from ctypes import POINTER, WINFUNCTYPE, windll, WinError >>> from ctypes.wintypes import BOOL, HWND, RECT >>> prototype = WINFUNCTYPE(BOOL, HWND, POINTER(RECT)) >>> paramflags = (1, "hwnd"), (2, "lprect") >>> GetWindowRect = prototype(("GetWindowRect", windll.user32), paramflags) >>> 

帶有輸出形參的函數如果輸出形參存在單一值則會自動返回該值,或是當輸出形參存在多個值時返回包含這些值的元組,因此當 GetWindowRect 被調用時現在將返回一個 RECT 實例。

輸出形參可以與 errcheck 協議相結合以執行進一步的輸出處理與錯誤檢查。 Win32 GetWindowRect API 函數返回一個 BOOL 來表示成功或失敗,因此此函數可執行錯誤檢查,並在 API 調用失敗時引發異常:

>>>
>>> def errcheck(result, func, args): ... if not result: ... raise WinError() ... return args ... >>> GetWindowRect.errcheck = errcheck >>> 

如果 errcheck 不加更改地返回它所接收的參數元組,則 ctypes 會繼續對輸出形參執行常規處理。 如果你希望返回一個窗口坐標的元組而非 RECT 實例,你可以從函數中提取這些字段並返回它們,常規處理將不會再執行:

>>>
>>> def errcheck(result, func, args): ... if not result: ... raise WinError() ... rc = args[1] ... return rc.left, rc.top, rc.bottom, rc.right ... >>> GetWindowRect.errcheck = errcheck >>> 

工具函數

ctypes. addressof (obj)

以整數形式返回內存緩沖區地址。 obj 必須為一個 ctypes 類型的實例。

引發一個 審計事件 ctypes.addressof,附帶參數 obj

ctypes. alignment (obj_or_type)

返回一個 ctypes 類型的對齊要求。 obj_or_type 必須為一個 ctypes 類型或實例。

ctypes. byref (obj[, offset])

返回指向 obj 的輕量指針,該對象必須為一個 ctypes 類型的實例。 offset 默認值為零,且必須為一個將被添加到內部指針值的整數。

byref(obj, offset) 對應於這段 C 代碼:

(((char *)&obj) + offset) 

返回的對象只能被用作外部函數調用形參。 它的行為類似於 pointer(obj),但構造起來要快很多。

ctypes. cast (objtype)

此函數類似於 C 的強制轉換運算符。 它返回一個 type 的新實例,該實例指向與 obj 相同的內存塊。 type 必須為指針類型,而 obj 必須為可以被作為指針來解讀的對象。

ctypes. create_string_buffer (init_or_sizesize=None)

此函數會創建一個可變的字符緩沖區。 返回的對象是一個 c_char 的 ctypes 數組。

init_or_size 必須是一個指明數組大小的整數,或者是一個將被用來初始化數組條目的字節串對象。

如果將一個字節串對象指定為第一個參數,則將使緩沖區大小比其長度多一項以便數組的最后一項為一個 NUL 終結符。 可以傳入一個整數作為第二個參數以允許在不使用字節串長度的情況下指定數組大小。

引發一個 審計事件 ctypes.create_string_buffer,附帶參數 initsize

ctypes. create_unicode_buffer (init_or_sizesize=None)

此函數會創建一個可變的 unicode 字符緩沖區。 返回的對象是一個 c_wchar 的 ctypes 數組。

init_or_size 必須是一個指明數組大小的整數,或者是一個將被用來初始化數組條目的字符串。

如果將一個字符串指定為第一個參數,則將使緩沖區大小比其長度多一項以便數組的最后一項為一個 NUL 終結符。 可以傳入一個整數作為第二個參數以允許在不使用字符串長度的情況下指定數組大小。

引發一個 審計事件 ctypes.create_unicode_buffer,附帶參數 initsize

ctypes. DllCanUnloadNow ()

僅限 Windows:此函數是一個允許使用 ctypes 實現進程內 COM 服務的鈎子。 它將由 _ctypes 擴展 dll 所導出的 DllCanUnloadNow 函數來調用。

ctypes. DllGetClassObject ()

僅限 Windows:此函數是一個允許使用 ctypes 實現進程內 COM 服務的鈎子。 它將由 _ctypes 擴展 dll 所導出的 DllGetClassObject 函數來調用。

ctypes.util. find_library (name)

嘗試尋找一個庫並返回路徑名稱。 name 是庫名稱並且不帶任何前綴如 lib 以及后綴如 .so.dylib 或版本號(形式與 posix 鏈接器選項 -l 所用的一致)。 如果找不到庫,則返回 None

確切的功能取決於系統。

ctypes.util. find_msvcrt ()

僅限 Windows:返回 Python 以及擴展模塊所使用的 VC 運行時庫的文件名。 如果無法確定庫名稱,則返回 None

如果你需要通過調用 free(void *) 來釋放內存,例如某個擴展模塊所分配的內存,重要的一點是你應當使用分配內存的庫中的函數。

ctypes. FormatError ([code])

僅限 Windows:返回錯誤碼 code 的文本描述。 如果未指定錯誤碼,則會通過調用 Windows api 函數 GetLastError 來獲得最新的錯誤碼。

ctypes. GetLastError ()

僅限 Windows:返回 Windows 在調用線程中設置的最新錯誤碼。 此函數會直接調用 Windows GetLastError() 函數,它並不返回錯誤碼的 ctypes 私有副本。

ctypes. get_errno ()

返回調用線程中系統 errno 變量的 ctypes 私有副本的當前值。

引發一個 審計事件 ctypes.get_errno,不附帶任何參數。

ctypes. get_last_error ()

僅限 Windows:返回調用線程中系統 LastError 變量的 ctypes 私有副本的當前值。

引發一個 審計事件 ctypes.get_last_error,不附帶任何參數。

ctypes. memmove (dstsrccount)

與標准 C memmove 庫函數相同:將 count 個字節從 src 拷貝到 dst。 dst 和 src 必須為整數或可被轉換為指針的 ctypes 實例。

ctypes. memset (dstccount)

與標准 C memset 庫函數相同:將位於地址 dst 的內存塊用 count 個字節的 c 值填充。 dst 必須為指定地址的整數或 ctypes 實例。

ctypes. POINTER (type)

這個工廠函數創建並返回一個新的 ctypes 指針類型。 指針類型會被緩存並在內部重用,因此重復調用此函數耗費不大。 type 必須為 ctypes 類型。

ctypes. pointer (obj)

此函數會創建一個新的指向 obj 的指針實例。 返回的對象類型為 POINTER(type(obj))

注意:如果你只是想向外部函數調用傳遞一個對象指針,你應當使用更為快速的 byref(obj)

ctypes. resize (objsize)

此函數可改變 obj 的內部內存緩沖區大小,其參數必須為 ctypes 類型的實例。 沒有可能將緩沖區設為小於對象類型的本機大小值,該值由 sizeof(type(obj)) 給出,但將緩沖區加大則是可能的。

ctypes. set_errno (value)

設置調用線程中系統 errno 變量的 ctypes 私有副本的當前值為 value 並返回原來的值。

引發一個 審計事件 ctypes.set_errno 附帶參數 errno

ctypes. set_last_error (value)

僅限 Windows:設置調用線程中系統 LastError 變量的 ctypes 私有副本的當前值為 value 並返回原來的值。

引發一個 審計事件 ctypes.set_last_error,附帶參數 error

ctypes. sizeof (obj_or_type)

返回 ctypes 類型或實例的內存緩沖區以字節表示的大小。 其功能與 C sizeof 運算符相同。

ctypes. string_at (addresssize=- 1)

此函數返回從內存地址 address 開始的以字節串表示的 C 字符串。 如果指定了 size,則將其用作長度,否則將假定字符串以零值結尾。

引發一個 審計事件 ctypes.string_at,附帶參數 addresssize

ctypes. WinError (code=Nonedescr=None)

僅限 Windows:此函數可能是 ctypes 中名字起得最差的函數。 它會創建一個 OSError 的實例。 如果未指定 code,則會調用 GetLastError 來確定錯誤碼。 如果未指定 descr,則會調用 FormatError() 來獲取錯誤的文本描述。

在 3.3 版更改: 以前是會創建一個 WindowsError 的實例。

ctypes. wstring_at (addresssize=- 1)

此函數返回從內存地址 address 開始的以字符串表示的寬字節字符串。 如果指定了 size,則將其用作字符串中的字符數量,否則將假定字符串以零值結尾。

引發一個 審計事件 ctypes.wstring_at,附帶參數 addresssize

數據類型

class  ctypes. _CData

這個非公有類是所有 ctypes 數據類型的共同基類。 另外,所有 ctypes 類型的實例都包含一個存放 C 兼容數據的內存塊;該內存塊的地址可由 addressof() 輔助函數返回。 還有一個實例變量被公開為 _objects;此變量包含其他在內存塊包含指針的情況下需要保持存活的 Python 對象。

ctypes 數據類型的通用方法,它們都是類方法(嚴謹地說,它們是 metaclass 的方法):

from_buffer (source[, offset])

此方法返回一個共享 source 對象緩沖區的 ctypes 實例。 source 對象必須支持可寫緩沖區接口。 可選的 offset 形參指定以字節表示的源緩沖區內偏移量;默認值為零。 如果源緩沖區不夠大則會引發 ValueError

引發一個 審計事件 ctypes.cdata/buffer 附帶參數 pointersizeoffset

from_buffer_copy (source[, offset])

此方法創建一個 ctypes 實例,從 source 對象緩沖區拷貝緩沖區,該對象必須是可讀的。 可選的 offset 形參指定以字節表示的源緩沖區內偏移量;默認值為零。 如果源緩沖區不夠大則會引發 ValueError

引發一個 審計事件 ctypes.cdata/buffer 附帶參數 pointersizeoffset

from_address (address)

此方法會使用 address 所指定的內存返回一個 ctypes 類型的實例,該參數必須為一個整數。

引發一個 審計事件 ctypes.cdata,附帶參數 address

from_param (obj)

此方法會將 obj 適配為一個 ctypes 類型。 它調用時會在當該類型存在於外部函數的 argtypes 元組時傳入外部函數調用所使用的實際對象;它必須返回一個可被用作函數調用參數的對象。

所有 ctypes 數據類型都帶有這個類方法的默認實現,它通常會返回 obj,如果該對象是此類型的實例的話。 某些類型也能接受其他對象。

in_dll (libraryname)

此方法返回一個由共享庫導出的 ctypes 類型。 name 為導出數據的符號名稱,library 為所加載的共享庫。

ctypes 數據類型的通用實例變量:

_b_base_

有時 ctypes 數據實例並不擁有它們所包含的內存塊,它們只是共享了某個基對象的部分內存塊。 _b_base_ 只讀成員是擁有內存塊的根 ctypes 對象。

_b_needsfree_

這個只讀變量在 ctypes 數據實例自身已分配了內存塊時為真值,否則為假值。

_objects

這個成員或者為 None,或者為一個包含需要保持存活以使內存塊的內存保持有效的 Python 對象的字典。 這個對象只是出於調試目的而對外公開;絕對不要修改此字典的內容。

基礎數據類型

class  ctypes. _SimpleCData

這個非公有類是所有基本 ctypes 數據類型的基類。 它在這里被提及是因為它包含基本 ctypes 數據類型共有的屬性。 _SimpleCData 是 _CData 的子類,因此繼承了其方法和屬性。 非指針及不包含指針的 ctypes 數據類型現在將可以被封存。

實例擁有一個屬性:

value

這個屬性包含實例的實際值。 對於整數和指針類型,它是一個整數,對於字符類型,它是一個單字符字符串對象或字符串,對於字符指針類型,它是一個 Python 字節串對象或字符串。

當從 ctypes 實例提取 value 屬性時,通常每次會返回一個新的對象。 ctypes 並 沒有 實現原始對象返回,它總是會構造一個新的對象。 所有其他 ctypes 對象實例也同樣如此。

基本數據類型當作為外部函數調用結果被返回或者作為結構字段成員或數組項被提取時,會透明地轉換為原生 Python 類型。 換句話說,如果某個外部函數具有 c_char_p 的 restype,你將總是得到一個 Python 字節串對象,而 不是 一個 c_char_p 實例。

基本數據類型的子類並 沒有 繼續此行為。 因此,如果一個外部函數的 restype 是 c_void_p 的一個子類,你將從函數調用得到一個該子類的實例。 當然,你可以通過訪問 value 屬性來獲取指針的值。

這些是基本 ctypes 數據類型:

class  ctypes. c_byte

代表 C signed char 數據類型,並將值解讀為一個小整數。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_char

代表 C char 數據類型,並將值解讀為單個字符。 該構造器接受一個可選的字符串初始化器,字符串的長度必須恰好為一個字符。

class  ctypes. c_char_p

當指向一個以零為結束符的字符串時代表 C char* 數據類型。 對於通用字符指針來說也可能指向二進制數據,必須要使用 POINTER(c_char)。 該構造器接受一個整數地址,或者一個字節串對象。

class  ctypes. c_double

代表 C double 數據類型。 該構造器接受一個可選的浮點數初始化器。

class  ctypes. c_longdouble

代表 C long double 數據類型。 該構造器接受一個可選的浮點數初始化器。 在 sizeof(long double) == sizeof(double) 的平台上它是 c_double 的一個別名。

class  ctypes. c_float

代表 C float 數據類型。 該構造器接受一個可選的浮點數初始化器。

class  ctypes. c_int

代表 C signed int 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。 在 sizeof(int) == sizeof(long) 的平台上它是 c_long 的一個別名。

class  ctypes. c_int8

代表 C 8 位 signed int 數據類型。 通常是 c_byte 的一個別名。

class  ctypes. c_int16

代表 C 16 位 signed int 數據類型。 通常是 c_short 的一個別名。

class  ctypes. c_int32

代表 C 32 位 signed int 數據類型。 通常是 c_int 的一個別名。

class  ctypes. c_int64

代表 C 64 位 signed int 數據類型。 通常是 c_longlong 的一個別名。

class  ctypes. c_long

代表 C signed long 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_longlong

代表 C signed long long 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_short

代表 C signed short 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_size_t

代表 C size_t 數據類型。

class  ctypes. c_ssize_t

代表 C ssize_t 數據類型。

3.2 新版功能.

class  ctypes. c_ubyte

代表 C unsigned char 數據類型,它將值解讀為一個小整數。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_uint

代表 C unsigned int 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。 在 sizeof(int) == sizeof(long) 的平台上它是 c_ulong 的一個別名。

class  ctypes. c_uint8

代表 C 8 位 unsigned int 數據類型。 通常是 c_ubyte 的一個別名。

class  ctypes. c_uint16

代表 C 16 位 unsigned int 數據類型。 通常是 c_ushort 的一個別名。

class  ctypes. c_uint32

代表 C 32 位 unsigned int 數據類型。 通常是 c_uint 的一個別名。

class  ctypes. c_uint64

代表 C 64 位 unsigned int 數據類型。 通常是 c_ulonglong 的一個別名。

class  ctypes. c_ulong

代表 C unsigned long 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_ulonglong

代表 C unsigned long long 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_ushort

代表 C unsigned short 數據類型。 該構造器接受一個可選的整數初始化器;不會執行溢出檢查。

class  ctypes. c_void_p

代表 C void* 類型。 該值被表示為整數形式。 該構造器接受一個可選的整數初始化器。

class  ctypes. c_wchar

代表 C wchar_t 數據類型,並將值解讀為一單個字符的 unicode 字符串。 該構造器接受一個可選的字符串初始化器,字符串的長度必須恰好為一個字符。

class  ctypes. c_wchar_p

代表 C wchar_t* 數據類型,它必須為指向以零為結束符的寬字符串的指針。 該構造器接受一個整數地址或者一個字符串。

class  ctypes. c_bool

代表 C bool 數據類型 (更准確地說是 C99 _Bool)。 它的值可以為 True 或 False,並且該構造器接受任何具有邏輯值的對象。

class  ctypes. HRESULT

Windows 專屬:代表一個 HRESULT 值,它包含某個函數或方法調用的成功或錯誤信息。

class  ctypes. py_object

代表 C PyObject* 數據類型。 不帶參數地調用此構造器將創建一個 NULL PyObject* 指針。

ctypes.wintypes 模塊提供了其他許多 Windows 專屬的數據類型,例如 HWNDWPARAM 或 DWORD。 還定義了一些有用的結構體例如 MSG 或 RECT

結構化數據類型

class  ctypes. Union (*args**kw)

本機字節序的聯合所對應的抽象基類。

class  ctypes. BigEndianStructure (*args**kw)

大端 字節序的結構體所對應的抽象基類。

class  ctypes. LittleEndianStructure (*args**kw)

小端 字節序的結構體所對應的抽象基類。

非本機字節序的結構體不能包含指針類型字段,或任何其他包含指針類型字段的數據類型。

class  ctypes. Structure (*args**kw)

本機 字節序的結構體所對應的抽象基類。

實際的結構體和聯合類型必須通過子類化這些類型之一來創建,並且至少要定義一個 _fields_ 類變量。 ctypes 將創建 descriptor,它允許通過直接屬性訪問來讀取和寫入字段。 這些是

_fields_

一個定義結構體字段的序列。 其中的條目必須為 2 元組或 3 元組。 元組的第一項是字段名稱,第二項指明字段類型;它可以是任何 ctypes 數據類型。

對於整數類型字段例如 c_int,可以給定第三個可選項。 它必須是一個定義字段比特位寬度的小正整數。

字段名稱在一個結構體或聯合中必須唯一。 不會檢查這個唯一性,但當名稱出現重復時將只有一個字段可被訪問。

可以在定義 Structure 子類的類語句 之后 再定義 _fields_ 類變量,這將允許創建直接或間接引用其自身的數據類型:

class List(Structure): pass List._fields_ = [("pnext", POINTER(List)), ... ] 

但是,_fields_ 類變量必須在類型第一次被使用(創建實例,調用 sizeof() 等等)之前進行定義。 在此之后對 _fields_ 類變量賦值將會引發 AttributeError。

可以定義結構體類型的子類,它們會繼承基類的字段再加上在子類中定義的任何 _fields_

_pack_

一個可選的小整數,它允許覆蓋實體中結構體字段的對齊方式。 當 _fields_ 被賦值時必須已經定義了 _pack_,否則它將沒有效果。

_anonymous_

一個可選的序列,它會列出未命名(匿名)字段的名稱。 當 _fields_ 被賦值時必須已經定義了 _anonymous_,否則它將沒有效果。

在此變量中列出的字段必須為結構體或聯合類型字段。 ctypes 將在結構體類型中創建描述器以允許直接訪問嵌套字段,而無需創建對應的結構體或聯合字段。

以下是一個示例類型(Windows):

class _U(Union): _fields_ = [("lptdesc", POINTER(TYPEDESC)), ("lpadesc", POINTER(ARRAYDESC)), ("hreftype", HREFTYPE)] class TYPEDESC(Structure): _anonymous_ = ("u",) _fields_ = [("u", _U), ("vt", VARTYPE)] 

TYPEDESC 結構體描述了一個 COM 數據類型,vt 字段指明哪個聯合字段是有效的。 由於 u 字段被定義為匿名字段,現在可以直接從 TYPEDESC 實例訪問成員。 td.lptdesc 和 td.u.lptdesc 是等價的,但前者速度更快,因為它不需要創建臨時的聯合實例:

td = TYPEDESC() td.vt = VT_PTR td.lptdesc = POINTER(some_type) td.u.lptdesc = POINTER(some_type) 

可以定義結構體的子類,它們會繼承基類的字段。 如果子類定義具有單獨的 _fields_ 變量,在其中指定的字段會被添加到基類的字段中。

結構體和聯合的構造器均可接受位置和關鍵字參數。 位置參數用於按照 _fields_ 中的出現順序來初始化成員字段。 構造器中的關鍵字參數會被解讀為屬性賦值,因此它們將以相應的名稱來初始化 _fields_,或為不存在於 _fields_ 中的名稱創建新的屬性。

數組與指針

class  ctypes. Array (*args)

數組的抽象基類。

創建實際數組類型的推薦方式是通過將任意 ctypes 類型與一個正整數相乘。 作為替代方式,你也可以子類化這個類型並定義 _length_ 和 _type_ 類變量。 數組元素可使用標准的抽取和切片方式來讀寫;對於切片讀取,結果對象本身 並非 一個 Array

_length_

一個指明數組中元素數量的正整數。 超出范圍的抽取會導致 IndexError。 該值將由 len() 返回。

_type_

指明數組中每個元素的類型。

Array 子類構造器可接受位置參數,用來按順序初始化元素。

class  ctypes. _Pointer

私有對象,指針的抽象基類。

實際的指針類型是通過調用 POINTER() 並附帶其將指向的類型來創建的;這會由 pointer() 自動完成。

如果一個指針指向的是數組,則其元素可使用標准的抽取和切片方式來讀寫。 指針對象沒有長度,因此 len() 將引發 TypeError。 抽取負值將會從指針 之前 的內存中讀取(與 C 一樣),並且超出范圍的抽取將可能因非法訪問而導致崩潰(視你的運氣而定)。

_type_

指明所指向的類型。

contents

返回指針所指向的對象。 對此屬性賦值會使指針改為指向所賦值的對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM