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 時不必感到迷惑 --- 它們實際上是同一種類型。
載入動態連接庫?
ctypes 導出了 cdll 對象,在 Windows 系統中還導出了 windll 和 oledll 對象用于載入動態連接庫。
通過操作這些對象的屬性,你可以載入外部的動態鏈接庫。cdll 載入按標準的 cdecl 調用協議導出的函數,而 windll 導入的庫按 stdcall 調用協議調用其中的函數。 oledll 也按 stdcall 調用協議調用其中的函數,并假定該函數返回的是 Windows HRESULT 錯誤代碼,并當函數調用失敗時,自動根據該代碼甩出一個 OSError 異常。
在 3.3 版更改: 原來在 Windows 下甩出的異常類型 WindowsError 現在是 OSError 的一個別名。
這是一些 Windows 下的例子。注意:msvcrt 是微軟 C 標準庫,包含了大部分 C 標準函數,這些函數都是以 cdecl 調用協議進行調用的。
>>> from ctypes import *
>>> print(windll.kernel32)
<WinDLL 'kernel32', handle ... at ...>
>>> print(cdll.msvcrt)
<CDLL 'msvcrt', handle ... at ...>
>>> libc = cdll.msvcrt
>>>
Windows會自動添加通常的 .dll 文件擴展名。
注解
通過 cdll.msvcrt 調用的標準 C 函數,可能會導致調用一個過時的,與當前 Python 所不兼容的函數。因此,請盡量使用標準的 Python 函數,而不要使用 msvcrt 模塊。
在 Linux 下,必須使用 包含 文件擴展名的文件名來導入共享庫。因此不能簡單使用對象屬性的方式來導入庫。因此,你可以使用方法 LoadLibrary(),或構造 CDLL 對象來導入庫。
>>> cdll.LoadLibrary("libc.so.6")
<CDLL 'libc.so.6', handle ... at ...>
>>> libc = CDLL("libc.so.6")
>>> libc
<CDLL 'libc.so.6', handle ... at ...>
>>>
操作導入的動態鏈接庫中的函數?
通過操作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 類型 |
|---|---|---|
|
bool (1) |
|
|
單字符字節對象 |
|
|
單字符字符串 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
整型 |
|
|
浮點數 |
|
|
浮點數 |
|
|
浮點數 |
|
|
字節串對象或 |
|
|
字符串或 |
|
|
int 或 |
構造函數接受任何具有真值的對象。
所有這些類型都可以通過使用正確類型和值的可選初始值調用它們來創建:
>>> 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_p, c_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.(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 中的結構體和聯合使用的是本地字節序。要使用非本地字節序,可以使用 BigEndianStructure, LittleEndianStructure, BigEndianUnion, 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 ,因為數組內容被初始化為 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 = "foo"
>>> c2 = cell()
>>> c2.name = "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
>>>
嗯。我們預想應該打印 3 4 1 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個元素,women訪問新增元素會產生以下錯誤:
>>> 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/ldconfig, gcc, objdump 以及 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(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)? 此類的實例即已加載的動態鏈接庫。庫中的函數使用標準 C 調用約定,并假定返回
int。
-
class
ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)? 僅 Windows : 此類的實例即加載好的動態鏈接庫,其中的函數使用
stdcall調用約定,并且假定返回 windows 指定的HRESULT返回碼。HRESULT的值包含的信息說明函數調用成功還是失敗,以及額外錯誤碼。 如果返回值表示失敗,會自動拋出OSError異常。在 3.3 版更改: 以前是引發
WindowsError。
-
class
ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)? 僅 Windows: 此類的實例即加載好的動態鏈接庫,其中的函數使用
stdcall調用約定,并假定默認返回int。在 Windows CE 上,只能使用 stdcall 調用約定,為了方便,
WinDLL和OleDLL在這個平臺上都使用標準調用約定。
調用動態庫導出的函數之前,Python會釋放 global interpreter lock ,并在調用后重新獲取。
-
class
ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=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 錯誤碼拷貝。
-
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 應當為
CDLL,PyDLL,WinDLL或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 函數作為屬性公開。 請注意所有這些函數都應返回 Cint,當然這也不是絕對的,因此你必須分配正確的restype屬性以使用這些函數。
外部函數?
正如之前小節的說明,外部函數可作為被加載共享庫的屬性來訪問。 用此方式創建的函數對象默認接受任意數量的參數,接受任意 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(result, func, arguments) result 是外部函數返回的結果,由
restype屬性指明。func 是外部函數對象本身,這樣就允許重新使用相同的可調用對象來對多個函數進行檢查或后續處理。
arguments 是一個包含最初傳遞給函數調用的形參的元組,這樣就允許對所用參數的行為進行特別處理。
此函數所返回的對象將會由外部函數調用返回,但它還可以在外部函數調用失敗時檢查結果并引發異常。
-
-
-
exception
ctypes.ArgumentError? 此異常會在外部函數無法對某個傳入參數執行轉換時被引發。
函數原型?
外部函數也可通過實例化函數原型來創建。 函數原型類似于 C 中的函數原型;它們在不定義具體實現的情況下描述了一個函數(返回類型、參數類型、調用約定)。 工廠函數必須使用函數所需要的結果類型和參數類型來調用,并可被用作裝飾器工廠函數,在此情況下可以通過 @wrapper 語法應用于函數。 請參閱 回調函數 了解有關示例。
-
ctypes.CFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)? 返回的函數原型會創建使用標準 C 調用約定的函數。 該函數在調用過程中將釋放 GIL。 如果 use_errno 設為真值,則在調用之前和之后系統
errno變量的 ctypes 私有副本會與真正的errno值進行交換;use_last_error 會為 Windows 錯誤碼執行同樣的操作。
-
ctypes.WINFUNCTYPE(restype, *argtypes, use_errno=False, use_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_index, name[, 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.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(obj, type)? 此函數類似于 C 的強制轉換運算符。 它返回一個 type 的新實例,該實例指向與 obj 相同的內存塊。 type 必須為指針類型,而 obj 必須為可以被作為指針來解讀的對象。
-
ctypes.create_string_buffer(init_or_size, size=None)? 此函數會創建一個可變的字符緩沖區。 返回的對象是一個
c_char的 ctypes 數組。init_or_size 必須是一個指明數組大小的整數,或者是一個將被用來初始化數組條目的字節串對象。
如果將一個字節串對象指定為第一個參數,則將使緩沖區大小比其長度多一項以便數組的最后一項為一個 NUL 終結符。 可以傳入一個整數作為第二個參數以允許在不使用字節串長度的情況下指定數組大小。
-
ctypes.create_unicode_buffer(init_or_size, size=None)? 此函數會創建一個可變的 unicode 字符緩沖區。 返回的對象是一個
c_wchar的 ctypes 數組。init_or_size 必須是一個指明數組大小的整數,或者是一個將被用來初始化數組條目的字符串。
如果將一個字符串指定為第一個參數,則將使緩沖區大小比其長度多一項以便數組的最后一項為一個 NUL 終結符。 可以傳入一個整數作為第二個參數以允許在不使用字符串長度的情況下指定數組大小。
-
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 的文本描述。 如果未指定錯誤碼,則會通過調用 the last error code is used by calling the Windows api 函數 GetLastError 來獲得最新的錯誤碼。
-
ctypes.GetLastError()? 僅限 Windows:返回 Windows 在調用線程中設置的最新錯誤碼。 此函數會直接調用 Windows GetLastError() 函數,它并不返回錯誤碼的 ctypes 私有副本。
-
ctypes.get_last_error()? 僅限 Windows:返回調用線程中系統
LastError變量的 ctypes 私有副本的當前值。
-
ctypes.memmove(dst, src, count)? 與標準 C memmove 庫函數相同:將 count 個字節從 src 拷貝到 dst。 dst 和 src 必須為整數或可被轉換為指針的 ctypes 實例。
-
ctypes.memset(dst, c, count)? 與標準 C memset 庫函數相同:將位于地址 dst 的內存塊用 count 個字節的 c 值填充。 dst 必須為指定地址的整數或 ctypes 實例。
-
ctypes.POINTER(type)? 這個工廠函數創建并返回一個新的 ctypes 指針類型。 指針類型會被緩存并在內部重用,因此重復調用此函數耗費不大。 type 必須為 ctypes 類型。
-
ctypes.pointer(obj)? 此函數會創建一個新的指向 obj 的指針實例。 返回的對象類型為
POINTER(type(obj))。注意:如果你只是想向外部函數調用傳遞一個對象指針,你應當使用更為快速的
byref(obj)。
-
ctypes.resize(obj, size)? 此函數可改變 obj 的內部內存緩沖區大小,其參數必須為 ctypes 類型的實例。 沒有可能將緩沖區設為小于對象類型的本機大小值,該值由
sizeof(type(obj))給出,但將緩沖區加大則是可能的。
-
ctypes.set_last_error(value)? 僅限 Windows:設置調用線程中系統
LastError變量的 ctypes 私有副本的當前值為 value 并返回原來的值。
-
ctypes.sizeof(obj_or_type)? 返回 ctypes 類型或實例的內存緩沖區以字節表示的大小。 其功能與 C
sizeof運算符相同。
-
ctypes.string_at(address, size=-1)? 此函數返回從內存地址 address 開始的以字節串表示的 C 字符串。 如果指定了 size,則將其用作長度,否則將假定字符串以零值結尾。
-
ctypes.WinError(code=None, descr=None)? 僅限 Windows:此函數可能是 ctypes 中名字起得最差的函數。 它會創建一個 OSError 的實例。 如果未指定 code,則會調用
GetLastError來確定錯誤碼。 如果未指定 descr,則會調用FormatError()來獲取錯誤的文本描述。
-
ctypes.wstring_at(address, size=-1)? 此函數返回從內存地址 address 開始的以字符串表示的寬字節字符串。 如果指定了 size,則將其用作字符串中的字符數量,否則將假定字符串以零值結尾。
數據類型?
-
class
ctypes._CData? 這個非公有類是所有 ctypes 數據類型的共同基類。 另外,所有 ctypes 類型的實例都包含一個存放 C 兼容數據的內存塊;該內存塊的地址可由
addressof()輔助函數返回。 還有一個實例變量被公開為_objects;此變量包含其他在內存塊包含指針的情況下需要保持存活的 Python 對象。ctypes 數據類型的通用方法,它們都是類方法(嚴謹地說,它們是 metaclass 的方法):
-
from_buffer(source[, offset])? 此方法返回一個共享 source 對象緩沖區的 ctypes 實例。 source 對象必須支持可寫緩沖區接口。 可選的 offset 形參指定以字節表示的源緩沖區內偏移量;默認值為零。 如果源緩沖區不夠大則會引發
ValueError。
-
from_buffer_copy(source[, offset])? 此方法創建一個 ctypes 實例,從 source 對象緩沖區拷貝緩沖區,該對象必須是可讀的。 可選的 offset 形參指定以字節表示的源緩沖區內偏移量;默認值為零。 如果源緩沖區不夠大則會引發
ValueError。
-
from_address(address)? 此方法會使用 address 所指定的內存返回一個 ctypes 類型的實例,該參數必須為一個整數。
-
from_param(obj)? 此方法會將 obj 適配為一個 ctypes 類型。 它調用時會在當該類型存在于外部函數的
argtypes元組時傳入外部函數調用所使用的實際對象;它必須返回一個可被用作函數調用參數的對象。所有 ctypes 數據類型都帶有這個類方法的默認實現,它通常會返回 obj,如果該對象是此類型的實例的話。 某些類型也能接受其他對象。
-
in_dll(library, name)? 此方法返回一個由共享庫導出的 ctypes 類型。 name 為導出數據的符號名稱,library 為所加載的共享庫。
ctypes 數據類型的通用實例變量:
-
_b_needsfree_? 這個只讀變量在 ctypes 數據實例自身已分配了內存塊時為真值,否則為假值。
-
_objects? 這個成員或者為
None,或者為一個包含需要保持存活以使內存塊的內存保持有效的 Python 對象的字典。 這個對象只是出于調試目的而對外公開;絕對不要修改此字典的內容。
-
基礎數據類型?
-
class
ctypes._SimpleCData? 這個非公有類是所有基本 ctypes 數據類型的基類。 它在這里被提及是因為它包含基本 ctypes 數據類型共有的屬性。
_SimpleCData是_CData的子類,因此繼承了其方法和屬性。 非指針及不包含指針的 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_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_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 *數據類型。 不帶參數地調用此構造器將創建一個NULLPyObject *指針。
ctypes.wintypes 模塊提供了其他許多 Windows 專屬的數據類型,例如 HWND, WPARAM 或 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_。
-
_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_中的名稱創建新的屬性。-
