ctypes — Python 的外部函式庫

原始碼: Lib/ctypes


ctypes 是 Python 的一個外部函式庫。它提供了與 C 語言相容的資料型別,並允許呼叫 DLL 或共享庫中的函式。可使用該模組以純 Python 方式對這些庫進行封裝。

ctypes 教程

注意:本教程中的程式碼示例使用 doctest 來確保它們實際可用。由於某些程式碼示例在 Linux、Windows 或 macOS 下的行為不同,它們在註釋中包含了 doctest 指令。

注意:一些程式碼示例引用了 ctypes 的 c_int 型別。在 sizeof(long) == sizeof(int) 的平臺上,它是 c_long 的別名。因此,如果你期望看到 c_int 卻打印出了 c_long,不必感到困惑——它們實際上是同一個型別。

從已載入的 DLL 中訪問函式

函式作為 DLL 物件的屬性來訪問。

>>> 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
>>>

請注意,像 kernel32user32 這樣的 win32 系統 DLL 通常會匯出 ANSI 和 UNICODE 兩個版本的函式。UNICODE 版本的函式名後會附加一個 W,而 ANSI 版本的函式名後會附加一個 A。win32 的 GetModuleHandle 函式,它為一個給定的模組名返回一個*模組控制代碼*,具有以下 C 原型,並使用一個宏來根據是否定義了 UNICODE 來將其中的一個暴露為 GetModuleHandle

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

windll 不會嘗試透過魔法來選擇其中之一,你必須透過明確指定 GetModuleHandleAGetModuleHandleW 來訪問你需要的版本,然後分別用位元組串或字串物件來呼叫它。

有時,DLL 匯出的函式名稱不是有效的 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 可呼叫物件一樣呼叫這些函式。這個例子使用了 rand() 函式,它不接受任何引數並返回一個偽隨機整數。

>>> print(libc.rand())
1804289383

在 Windows 上,你可以呼叫 GetModuleHandleA() 函式,它返回一個 win32 模組控制代碼(傳遞 None 作為單個引數以 NULL 指標呼叫它)。

>>> 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)
>>>

要找出正確的呼叫約定,你需要檢視 C 標頭檔案或你想要呼叫的函式的文件。

在 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_int8

int8_t

int

c_int16

int16_t

int

c_int32

int32_t

int

c_int64

int64_t

int

c_uint

unsigned int

int

c_uint8

uint8_t

int

c_uint16

uint16_t

int

c_uint32

uint32_t

int

c_uint64

uint64_t

int

c_long

long

int

c_ulong

unsigned long

int

c_longlong

__int64long long

int

c_ulonglong

unsigned __int64unsigned long long

int

c_size_t

size_t

int

c_ssize_t

ssize_tPy_ssize_t

int

c_time_t

time_t

int

c_float

浮點數

浮點數

c_double

double

浮點數

c_longdouble

long double

浮點數

c_char_p

char* (NUL 結尾)

位元組物件或 None

c_wchar_p

wchar_t* (NUL 結尾)

字串或 None

c_void_p

void*

整數或 None

  1. 建構函式接受任何具有真值的物件。

此外,如果 C 和 libffi 都支援符合 IEC 60559 標準的複數運算(附件 G),則可以使用以下複數型別:

ctypes 型別

C 型別

Python 型別

c_float_complex

float complex

complex

c_double_complex

double complex

complex

c_longdouble_complex

long double complex

complex

所有這些型別都可以透過呼叫它們並帶有一個可選的、型別和值都正確的初始化器來建立。

>>> 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_pc_void_p 的例項賦一個新值會改變它們指向的*記憶體位置*,而*不是*記憶體塊的內容(當然不是,因為 Python 字串物件是不可變的)。

>>> 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() 函式取代了舊的 c_buffer() 函式(它仍然作為一個別名存在)。要建立一個包含 C 型別為 wchar_t 的 unicode 字元的可變記憶體塊,請使用 create_unicode_buffer() 函式。

呼叫函式,續

請注意,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>
ctypes.ArgumentError: argument 2: 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 呼叫可變引數函式與呼叫具有固定數量引數的函式完全相同。在某些平臺上,特別是蘋果平臺的 ARM64,可變引數函式的呼叫約定與常規函式不同。

在這些平臺上,需要為常規的、非可變引數的函式引數指定 argtypes 屬性。

libc.printf.argtypes = [ctypes.c_char_p]

因為指定該屬性不會影響可移植性,所以建議總是為所有可變引數函式指定 argtypes

使用自定義資料型別呼叫函式

你還可以自定義 ctypes 的引數轉換,以允許使用你自己類的例項作為函式引數。ctypes 會查詢一個 _as_parameter_ 屬性,並將其用作函式引數。該屬性必須是整數、字串、位元組串、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>
ctypes.ArgumentError: argument 2: TypeError: 'int' object cannot be interpreted as ctypes.c_char_p
>>> 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 屬性來指定其他返回型別。

time() 的 C 原型是 time_t time(time_t *)。因為 time_t 可能與預設返回型別 int 的型別不同,所以你應該指定 restype 屬性。

>>> libc.time.restype = c_time_t

引數型別可以使用 argtypes 來指定。

>>> libc.time.argtypes = (POINTER(c_time_t),)

要以 NULL 指標作為第一個引數來呼叫該函式,請使用 None

>>> print(libc.time(None))
1150640792

這裡有一個更高階的例子,它使用了 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")
b'def'
>>> strchr(b"abcdef", b"def")
Traceback (most recent call last):
ctypes.ArgumentError: argument 2: TypeError: one character bytes, bytearray or integer expected
>>> print(strchr(b"abcdef", b"x"))
None
>>> strchr(b"abcdef", b"d")
b'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 API 函式期望一個指向資料型別的*指標*作為引數,可能是為了寫入相應的位置,或者如果資料太大而無法按值傳遞。這也被稱為*透過引用傳遞引數*。

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 模組中定義的 StructureUnion 基類。每個子類必須定義一個 _fields_ 屬性。_fields_ 必須是一個由*二元組*組成的列表,每個元組包含一個*欄位名*和一個*欄位型別*。

欄位型別必須是像 c_int 這樣的 ctypes 型別,或者任何其他派生的 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
>>>

然而,你可以構建更復雜的結構體。一個結構體本身可以透過使用一個結構體作為欄位型別來包含其他結構體。

這是一個 RECT 結構體,它包含兩個名為 *upperleft* 和 *lowerright* 的 POINT。

>>> 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))

欄位描述符可以從*類*中檢索,它們對於除錯很有用,因為它們可以提供有用的資訊。請參見 CField

>>> POINT.x
<ctypes.CField 'x' type=c_int, ofs=0, size=4>
>>> POINT.y
<ctypes.CField 'y' type=c_int, ofs=4, size=4>
>>>

警告

ctypes 不支援按值向函式傳遞帶有位域的聯合體或結構體。雖然這在 32 位 x86 上可能有效,但庫不保證在一般情況下也有效。帶有位域的聯合體和結構體應始終透過指標傳遞給函式。

結構體/聯合體佈局、對齊和位元組序

預設情況下,結構體和聯合體欄位的佈局方式與 C 編譯器相同。透過在子類定義中指定一個 _layout_ 類屬性,可以完全覆蓋此行為;詳情請參閱該屬性的文件。

可以透過分別設定類屬性 _pack_ 和/或 _align_ 來指定欄位和/或結構體本身的最大對齊方式。詳情請參閱屬性文件。

ctypes 對結構體和聯合體使用本地位元組序。要構建非本地位元組序的結構體,你可以使用 BigEndianStructure, LittleEndianStructure, BigEndianUnion, 和 LittleEndianUnion 基類中的一個。這些類不能包含指標欄位。

結構體和聯合體中的位域

可以建立包含位域的結構體和聯合體。位域只適用於整數字段,位寬在 _fields_ 元組中作為第三項指定。

>>> class Int(Structure):
...     _fields_ = [("first_16", c_int, 16),
...                 ("second_16", c_int, 16)]
...
>>> print(Int.first_16)
<ctypes.CField 'first_16' type=c_int, ofs=0, bit_size=16, bit_offset=0>
>>> print(Int.second_16)
<ctypes.CField 'second_16' type=c_int, ofs=0, bit_size=16, bit_offset=16>

需要注意的是,位域在記憶體中的分配和佈局並未被 C 標準定義;它們的實現是編譯器特定的。預設情況下,Python 將嘗試匹配當前平臺“原生”編譯器的行為。有關預設行為以及如何更改它的詳細資訊,請參見 _layout_ 屬性。

陣列

陣列是序列,包含固定數量的相同型別的例項。

建立陣列型別的推薦方法是將資料型別與一個正整數相乘。

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
>>>

將另一個 c_int 例項賦給指標的 contents 屬性會導致指標指向儲存該例項的記憶體位置。

>>> 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 語言中一樣:你可以訪問或更改任意記憶體位置。通常你只在從 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
>>>

無 GIL 時的執行緒安全

從 Python 3.13 開始,GIL 可以在自由執行緒構建中被停用。在 ctypes 中,對單個物件的併發讀寫是安全的,但跨多個物件則不安全。

>>> number = c_int(42)
>>> pointer_a = pointer(number)
>>> pointer_b = pointer(number)

在上述程式碼中,如果 GIL 被停用,只有一個物件能夠同時對該地址進行讀寫才是安全的。所以,pointer_a 可以在多個執行緒間共享和寫入,但前提是 pointer_b 沒有同時嘗試做同樣的事情。如果這是一個問題,可以考慮使用 threading.Lock 來同步對記憶體的訪問。

>>> import threading
>>> lock = threading.Lock()
>>> # Thread 1
>>> with lock:
...    pointer_a.contents = 24
>>> # Thread 2
>>> with lock:
...    pointer_b.contents = 42

型別轉換

通常,ctypes 會進行嚴格的型別檢查。這意味著,如果函式引數型別列表 argtypes 中有 POINTER(c_int),或者結構體定義中的成員欄位型別是它,那麼只接受完全相同型別的例項。這個規則有一些例外,ctypes 會接受其他物件。例如,你可以傳遞相容的陣列例項來代替指標型別。所以,對於 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() 轉換。

要將一個 POINTER 型別的欄位設定為 NULL,你可以賦 None

>>> bar.values = None
>>>

有時你會有不相容型別的例項。在 C 語言中,你可以將一種型別強制轉換為另一種型別。ctypes 提供了一個 cast() 函式,可以以同樣的方式使用。上面定義的 Bar 結構體接受 POINTER(c_int) 指標或 c_int 陣列作為其 values 欄位,但不接受其他型別的例項。

>>> 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 例項轉換為指向不同 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
>>>

因為新的 class cell 在類語句本身中是不可用的。在 ctypes 中,我們可以在類語句之後定義 cell 類,然後再設定 _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() 必須用指向待排序資料的指標、資料陣列中的專案數、單個專案的大小以及指向比較函式(即回撥函式)的指標來呼叫。然後,回撥函式將被兩個指向專案的指標呼叫,如果第一個專案小於第二個,它必須返回一個負整數;如果它們相等,則返回零;否則返回一個正整數。

所以我們的回撥函式接收指向整數的指標,並且必須返回一個整數。首先我們為回撥函式建立 type

>>> 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
>>>

備註

只要 C 程式碼還在使用 CFUNCTYPE() 物件,請確保你保留對它們的引用。ctypes 不會這樣做,如果你不保留引用,它們可能會被垃圾回收,導致在回撥時程式崩潰。

另外,請注意,如果回撥函式是在 Python 控制之外建立的執行緒中被呼叫的(例如,由呼叫回撥的外部程式碼建立),ctypes 會在每次呼叫時建立一個新的虛擬 Python 執行緒。這種行為對於大多數目的來說是正確的,但它意味著儲存在 threading.local 中的值在不同的回撥之間將*不*會保留,即使這些呼叫是從同一個 C 執行緒發出的。

訪問從 DLL 匯出的值

一些共享庫不僅匯出函式,還匯出變數。Python 庫本身的一個例子是 Py_Version,即編碼在單個常量整數中的 Python 執行時版本號。

ctypes 可以透過型別的 in_dll() 類方法來訪問這樣的值。pythonapi 是一個預定義的符號,用於訪問 Python C API。

>>> version = ctypes.c_int.in_dll(ctypes.pythonapi, "Py_Version")
>>> print(hex(version.value))
0x30c00a0

一個擴充套件的例子,它也演示了指標的使用,訪問了由 Python 匯出的 PyImport_FrozenModules 指標。

引用該值的文件:

該指標被初始化為指向一個 _frozen 記錄的陣列,陣列以一個所有成員均為 NULL 或零的記錄結尾。當一個凍結模組被匯入時,會在此表中搜索。第三方程式碼可以利用這一點來提供一個動態建立的凍結模組集合。

所以操縱這個指標甚至可能是有用的。為了限制示例的大小,我們只展示如何用 ctypes 來讀取這個表。

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

我們已經定義了 _frozen 資料型別,所以我們可以獲取指向表的指標。

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

因為 table 是一個指向 struct_frozen 記錄陣列的 pointer,我們可以遍歷它,但我們必須確保我們的迴圈會終止,因為指標沒有大小。遲早它可能會因訪問衝突或其他原因而崩潰,所以當我們遇到 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
zipimport 12345
>>>

標準 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
>>>

請注意,temp0temp1 仍然是使用上面 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 例項化的物件的值只能設定為位元組或整數。

為什麼它列印 False?ctypes 例項是包含一個記憶體塊和一些訪問記憶體內容的描述符的物件。在記憶體塊中儲存一個 Python 物件並不會儲存物件本身,而是儲存了物件的 contents。每次再次訪問內容時都會構造一個新的 Python 物件!

可變大小的資料型別

ctypes 為可變大小的陣列和結構體提供了一些支援。

resize() 函式可用於調整現有 ctypes 物件的記憶體緩衝區大小。該函式以物件作為第一個引數,以請求的大小(以位元組為單位)作為第二個引數。記憶體塊不能小於物件型別指定的自然記憶體塊,如果嘗試這樣做會引發 ValueError

>>> 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 參考

查詢共享庫

在使用編譯型語言程式設計時,共享庫在編譯/連結程式時以及程式執行時被訪問。

find_library() 函式的目的是以類似於編譯器或執行時載入器的方式定位一個庫(在有多個共享庫版本的平臺上,應載入最新的版本),而 ctypes 庫載入器的行為就像程式執行時一樣,直接呼叫執行時載入器。

ctypes.util 模組提供了一個函式,可以幫助確定要載入的庫。

ctypes.util.find_library(name)

嘗試查詢一個庫並返回其路徑名。name 是庫名,不帶任何字首如 lib、字尾如 .so.dylib 或版本號(這是 posix 連結器選項 -l 使用的形式)。如果找不到庫,則返回 None

確切的功能取決於系統。

在 Linux 上,find_library() 嘗試執行外部程式(/sbin/ldconfiggccobjdumpld)來查詢庫檔案。它返回庫檔案的檔名。

在 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'
>>>

在 macOS 和 Android 上,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() 在執行時定位庫。

列出已載入的共享庫

在編寫依賴於從共享庫載入的程式碼時,瞭解哪些共享庫已經被載入到當前程序中可能會很有用。

ctypes.util 模組提供了 dllist() 函式,該函式呼叫不同平臺提供的不同 API,以幫助確定哪些共享庫已經被載入到當前程序中。

此函式的具體輸出將取決於系統。在大多數平臺上,此列表的第一個條目代表當前程序本身,可能是一個空字串。例如,在基於 glibc 的 Linux 上,返回值可能如下所示:

>>> from ctypes.util import dllist
>>> dllist()
['', 'linux-vdso.so.1', '/lib/x86_64-linux-gnu/libm.so.6', '/lib/x86_64-linux-gnu/libc.so.6', ... ]

載入共享庫

有幾種方法可以將共享庫載入到 Python 程序中。一種方法是例項化以下類之一:

class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=None)

這個類的例項代表已載入的共享庫。這些庫中的函式使用標準的 C 呼叫約定,並假定返回 int

在 Windows 上,即使 DLL 名稱存在,建立 CDLL 例項也可能失敗。當載入的 DLL 的一個依賴 DLL 未找到時,會引發一個 OSError 錯誤,訊息為*“[WinError 126] 找不到指定的模組。”*。這個錯誤訊息不包含缺失 DLL 的名稱,因為 Windows API 不返回此資訊,這使得這個錯誤難以診斷。要解決此錯誤並確定哪個 DLL 未找到,您需要找到依賴 DLL 的列表,並使用 Windows 除錯和跟蹤工具確定哪個未找到。

在 3.12 版本發生變更: name 引數現在可以是一個類路徑物件

參見

Microsoft DUMPBIN 工具 – 一個用於查詢 DLL 依賴項的工具。

class ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=None)

此類的例項代表已載入的共享庫,這些庫中的函式使用 stdcall 呼叫約定,並假定返回 Windows 特定的 HRESULT 程式碼。HRESULT 值包含指定函式呼叫是失敗還是成功的資訊,以及附加的錯誤程式碼。如果返回值表示失敗,則會自動引發 OSError

可用性: Windows

在 3.3 版本發生變更: 過去會引發 WindowsError,現在它是 OSError 的別名。

在 3.12 版本發生變更: name 引數現在可以是一個類路徑物件

class ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=None)

這個類的例項代表已載入的共享庫,這些庫中的函式使用 stdcall 呼叫約定,並預設假定返回 int 型別。

可用性: Windows

在 3.12 版本發生變更: name 引數現在可以是一個類路徑物件

在呼叫這些庫匯出的任何函式之前,Python 的 全域性直譯器鎖 會被釋放,呼叫之後會重新獲取。

class ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)

該類的例項行為與 CDLL 例項類似,不同之處在於 Python GIL 在函式呼叫期間不會被釋放,並且在函式執行後會檢查 Python 錯誤旗標。如果設定了錯誤旗標,則會引發 Python 異常。

因此,這隻適用於直接呼叫 Python C API 函式。

在 3.12 版本發生變更: name 引數現在可以是一個類路徑物件

所有這些類都可以透過至少一個引數來例項化,即共享庫的路徑名。如果你有一個已載入的共享庫的控制代碼,可以將其作為名為 handle 的引數傳遞,否則將使用底層平臺的 dlopen()LoadLibrary() 函式將庫載入到程序中,並獲取其控制代碼。

mode 引數可用於指定庫的載入方式。有關詳細資訊,請參閱 dlopen(3) 手冊頁。在 Windows 上,mode 被忽略。在 posix 系統上,RTLD_NOW 總是會被新增,且不可配置。

use_errno 引數,當設定為 true 時,會啟用一個 ctypes 機制,允許以安全的方式訪問系統 errno 錯誤號。ctypes 維護一個執行緒本地的系統 errno 變數副本;如果你呼叫使用 use_errno=True 建立的外部函式,那麼函式呼叫前的 errno 值會與 ctypes 的私有副本交換,函式呼叫後也會立即進行同樣的操作。

函式 ctypes.get_errno() 返回 ctypes 私有副本的值,而函式 ctypes.set_errno() 將 ctypes 私有副本更改為新值並返回舊值。

use_last_error 引數,當設定為 true 時,為 Windows 錯誤程式碼啟用相同的機制,該錯誤程式碼由 GetLastError()SetLastError() Windows API 函式管理;ctypes.get_last_error()ctypes.set_last_error() 用於請求和更改 ctypes 的 Windows 錯誤程式碼私有副本。

winmode 引數在 Windows 上用於指定庫的載入方式(因為 mode 被忽略)。它接受任何對 Win32 API LoadLibraryEx 的 flags 引數有效的值。如果省略,預設將使用能產生最安全 DLL 載入的標誌,這可以避免 DLL 劫持等問題。傳遞 DLL 的完整路徑是確保載入正確庫和依賴項的最安全方法。

在 3.8 版本發生變更: 添加了 winmode 引數。

ctypes.RTLD_GLOBAL

用作 mode 引數的旗標。在不提供此旗標的平臺上,它被定義為整數零。

ctypes.RTLD_LOCAL

用作 mode 引數的旗標。在不提供此旗標的平臺上,它與 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 應該是 CDLLPyDLLWinDLLOleDLL 型別之一。

__getattr__() 具有特殊的行為:它允許透過將共享庫作為庫載入器例項的屬性來載入它。結果會被快取,因此重複的屬性訪問每次都會返回同一個庫。

LoadLibrary(name)

將共享庫載入到程序中並返回它。此方法總是返回一個新的庫例項。

這些預製的庫載入器是可用的:

ctypes.cdll

建立 CDLL 例項。

ctypes.windll

建立 WinDLL 例項。

可用性: Windows

ctypes.oledll

建立 OleDLL 例項。

可用性: Windows

ctypes.pydll

建立 PyDLL 例項。

為了直接訪問 C Python API,提供了一個現成的 Python 共享庫物件:

ctypes.pythonapi

一個 PyDLL 的例項,它將 Python C API 函式作為屬性公開。請注意,所有這些函式都假定返回 C 的 int 型別,這當然不總是正確的,所以你必須為這些函式指定正確的 restype 屬性才能使用它們。

透過這些物件載入庫會引發一個 審計事件 ctypes.dlopen,並附帶字串引數 name,即用於載入庫的名稱。

訪問已載入庫上的函式會引發一個審計事件 ctypes.dlsym,並附帶引數 library(庫物件)和 name(符號名稱,為字串或整數)。

在只有庫控制代碼可用而不是物件的情況下,訪問函式會引發一個審計事件 ctypes.dlsym/handle,並附帶引數 handle(原始庫控制代碼)和 name

外部函式

如上一節所述,外部函式可以作為已載入共享庫的屬性來訪問。以這種方式建立的函式物件預設接受任意數量的引數,接受任何 ctypes 資料例項作為引數,並返回庫載入器指定的預設結果型別。

它們是一個私有本地類 _FuncPtr (未在 ctypes 中公開) 的例項,該類繼承自私有類 _CFuncPtr

>>> import ctypes
>>> lib = ctypes.CDLL(None)
>>> issubclass(lib._FuncPtr, ctypes._CFuncPtr)
True
>>> lib._FuncPtr is ctypes._CFuncPtr
False
class ctypes._CFuncPtr

C 可呼叫外部函式的基類。

外部函式的例項也是 C 相容的資料型別;它們表示 C 函式指標。

這種行為可以透過給外部函式物件的特殊屬性賦值來定製。

restype

指定一個 ctypes 型別來規定外部函式的返回型別。對於不返回任何內容的 void 函式,請使用 None

可以賦值一個非 ctypes 型別的可呼叫 Python 物件,在這種情況下,函式被假定返回一個 C int,並且該可呼叫物件將被這個整數呼叫,從而允許進一步的處理或錯誤檢查。不推薦使用此方法,為了更靈活的後處理或錯誤檢查,請使用 ctypes 資料型別作為 restype 並將一個可呼叫物件賦給 errcheck 屬性。

argtypes

指定一個 ctypes 型別的元組,用於規定函式接受的引數型別。使用 stdcall 呼叫約定的函式只能用與此元組長度相同的引數數量來呼叫;使用 C 呼叫約定的函式還接受額外的、未指定的引數。

當呼叫一個外部函式時,每個實際引數都會傳遞給 argtypes 元組中各項的 from_param() 類方法,此方法允許將實際引數適配為外部函式可接受的物件。例如,argtypes 元組中的一個 c_char_p 項會使用 ctypes 轉換規則將作為引數傳遞的字串轉換為位元組物件。

新增:現在可以在 argtypes 中放入非 ctypes 型別的項,但每一項都必須有一個 from_param() 方法,該方法返回一個可用作引數的值(整數、字串、ctypes 例項)。這允許定義可以將自定義物件適配為函式引數的介面卡。

errcheck

將一個 Python 函式或其他可呼叫物件賦給此屬性。該可呼叫物件將以三個或更多引數被呼叫:

callable(result, func, arguments)

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

func 是外部函式物件本身,這允許重用同一個可呼叫物件來檢查或後處理多個函式的結果。

arguments 是一個包含最初傳遞給函式呼叫的引數的元組,這允許根據所用引數來定製行為。

此函式返回的物件將從外部函式呼叫中返回,但它也可以檢查結果值,並在外部函式呼叫失敗時引發異常。

在 Windows 上,當一個外部函式呼叫引發系統異常(例如,由於訪問衝突)時,它將被捕獲並替換為適當的 Python 異常。此外,將引發一個審計事件 ctypes.set_exception,引數為 code,允許審計鉤子用自己的異常替換它。

某些呼叫外部函式的方式以及此模組中的某些函式可能會引發一個審計事件 ctypes.call_function,引數為 function pointerarguments

函式原型

外部函式也可以透過例項化函式原型來建立。函式原型類似於 C 中的函式原型;它們描述一個函式(返回型別、引數型別、呼叫約定)而不定義實現。必須用期望的結果型別和函式的引數型別來呼叫這些工廠函式,它們可以用作裝飾器工廠,因此可以透過 @wrapper 語法應用於函式。請參閱 回撥函式 檢視示例。

ctypes.CFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)

返回的函式原型建立使用標準 C 呼叫約定的函式。函式在呼叫期間將釋放 GIL。如果 use_errno 設定為 true,則在呼叫前後,ctypes 私有的系統 errno 變數副本會與真實的 errno 值交換;use_last_error 對 Windows 錯誤程式碼執行相同的操作。

ctypes.WINFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)

返回的函式原型建立使用 stdcall 呼叫約定的函式。函式在呼叫期間將釋放 GIL。use_errnouse_last_error 的含義與上面相同。

可用性: Windows

ctypes.PYFUNCTYPE(restype, *argtypes)

返回的函式原型建立使用 Python 呼叫約定的函式。函式在呼叫期間不會釋放 GIL。

這些工廠函式建立的函式原型可以透過不同的方式例項化,具體取決於呼叫中引數的型別和數量:

prototype(address)

返回指定地址處的外部函式,該地址必須是一個整數。

prototype(callable)

從 Python callable 建立一個 C 可呼叫函式(回撥函式)。

prototype(func_spec[, paramflags])

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

prototype(vtbl_index, name[, paramflags[, iid]])

返回一個將呼叫 COM 方法的外部函式。vtbl_index 是虛擬函式表中的索引,是一個小的非負整數。name 是 COM 方法的名稱。iid 是一個可選的介面識別符號指標,用於擴充套件錯誤報告。

如果未指定 iid,則在 COM 方法呼叫失敗時會引發 OSError。如果指定了 iid,則會引發 COMError

COM 方法使用一種特殊的呼叫約定:除了在 argtypes 元組中指定的引數外,它們還需要一個指向 COM 介面的指標作為第一個引數。

可用性: Windows

可選的 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 的輕量級指標,obj 必須是 ctypes 型別的例項。offset 預設為零,並且必須是一個將加到內部指標值上的整數。

byref(obj, offset) 對應於以下 C 程式碼:

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

返回的物件只能用作外部函式呼叫的引數。它的行為類似於 pointer(obj),但構造速度快得多。

ctypes.CopyComPointer(src, dst)

將一個 COM 指標從 src 複製到 dst 並返回 Windows 特定的 HRESULT 值。

如果 src 不為 NULL,則會呼叫其 AddRef 方法,增加引用計數。

相反,在賦新值之前,dst 的引用計數不會減少。除非 dstNULL,否則呼叫者有責任在必要時透過呼叫其 Release 方法來減少引用計數。

可用性: Windows

在 3.14 版本加入。

ctypes.cast(obj, type)

此函式類似於 C 中的強制型別轉換運算子。它返回一個 type 的新例項,該例項指向與 obj 相同的記憶體塊。type 必須是指標型別,而 obj 必須是可以解釋為指標的物件。

ctypes.create_string_buffer(init, size=None)
ctypes.create_string_buffer(size)

此函式建立一個可變的字元緩衝區。返回的物件是 c_char 的 ctypes 陣列。

如果給定了 size(且不為 None),它必須是一個 int。它指定了返回陣列的大小。

如果給定了 init 引數,它必須是 bytes。它用於初始化陣列項。未以此方式初始化的位元組被設定為零 (NUL)。

如果未給定 size(或者為 None),則緩衝區會比 init 大一個元素,實際上是添加了一個 NUL 終止符。

如果同時給定了兩個引數,size 必須不小於 len(init)

警告

如果 size 等於 len(init),則不會新增 NUL 終止符。不要將這樣的緩衝區視為 C 字串。

例如:

>>> bytes(create_string_buffer(2))
b'\x00\x00'
>>> bytes(create_string_buffer(b'ab'))
b'ab\x00'
>>> bytes(create_string_buffer(b'ab', 2))
b'ab'
>>> bytes(create_string_buffer(b'ab', 4))
b'ab\x00\x00'
>>> bytes(create_string_buffer(b'abcdef', 2))
Traceback (most recent call last):
   ...
ValueError: byte string too long

引發一個 審計事件 ctypes.create_string_buffer,並附帶引數 init, size

ctypes.create_unicode_buffer(init, size=None)
ctypes.create_unicode_buffer(size)

此函式建立一個可變的 unicode 字元緩衝區。返回的物件是 c_wchar 的 ctypes 陣列。

該函式接受與 create_string_buffer() 相同的引數,但 init 必須是字串,且 size 計數的是 c_wchar

引發一個 審計事件 ctypes.create_unicode_buffer,並附帶引數 init, size

ctypes.DllCanUnloadNow()

此函式是一個鉤子,允許使用 ctypes 實現程序內 COM 伺服器。它從 _ctypes 擴充套件 dll 匯出的 DllCanUnloadNow 函式中呼叫。

可用性: Windows

ctypes.DllGetClassObject()

此函式是一個鉤子,允許使用 ctypes 實現程序內 COM 伺服器。它從 _ctypes 擴充套件 dll 匯出的 DllGetClassObject 函式中呼叫。

可用性: Windows

ctypes.util.find_library(name)

嘗試查詢一個庫並返回其路徑名。name 是庫名,不帶任何字首(如 lib)、字尾(如 .so, .dylib)或版本號(這是 posix 連結器選項 -l 使用的形式)。如果找不到庫,則返回 None

確切的功能取決於系統。

ctypes.util.find_msvcrt()

返回 Python 和擴充套件模組使用的 VC 執行時庫的檔名。如果無法確定庫的名稱,則返回 None

如果你需要釋放記憶體,例如,由擴充套件模組透過呼叫 free(void *) 分配的記憶體,那麼使用分配記憶體的同一個庫中的函式來釋放它非常重要。

可用性: Windows

ctypes.util.dllist()

嘗試提供載入到當前程序中的共享庫的路徑列表。這些路徑未經任何規範化或處理。如果底層平臺 API 失敗,該函式可能會引發 OSError。具體功能取決於系統。

在大多數平臺上,列表的第一個元素代表當前的可執行檔案。它可能是一個空字串。

可用性:Windows、macOS、iOS、glibc、BSD libc、musl

在 3.14 版本加入。

ctypes.FormatError([code])

返回錯誤程式碼 code 的文字描述。如果未指定錯誤程式碼,則透過呼叫 Windows API 函式 GetLastError() 來使用最後一個錯誤程式碼。

可用性: Windows

ctypes.GetLastError()

返回 Windows 在呼叫執行緒中設定的最後一個錯誤程式碼。此函式直接呼叫 Windows 的 GetLastError() 函式,它不返回 ctypes 私有的錯誤程式碼副本。

可用性: Windows

ctypes.get_errno()

返回呼叫執行緒中系統 errno 變數的 ctypes 私有副本的當前值。

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

ctypes.get_last_error()

返回呼叫執行緒中系統 LastError 變數的 ctypes 私有副本的當前值。

可用性: Windows

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

ctypes.memmove(dst, src, count)

與標準 C 庫函式 memmove 相同:將 count 位元組從 src 複製到 dstdstsrc 必須是可以轉換為指標的整數或 ctypes 例項。

ctypes.memset(dst, c, count)

與標準 C 庫函式 memset 相同:用 count 個值為 c 的位元組填充地址 dst 處的記憶體塊。dst 必須是指定地址的整數或 ctypes 例項。

ctypes.POINTER(type, /)

建立或返回一個 ctypes 指標型別。指標型別在內部被快取和重用,因此重複呼叫此函式開銷很小。type 必須是 ctypes 型別。

CPython 實現細節: 生成的指標型別被快取在 type__pointer_type__ 屬性中。可以在首次呼叫 POINTER 之前設定此屬性,以設定自定義指標型別。但是,不鼓勵這樣做:如果不依賴可能在未來 Python 版本中更改的實現細節,手動建立合適的指標型別很困難。

ctypes.pointer(obj, /)

建立一個新的指標例項,指向 obj。返回的物件型別為 POINTER(type(obj))

注意:如果你只是想將一個物件的指標傳遞給外部函式呼叫,你應該使用 byref(obj),它快得多。

ctypes.resize(obj, size)

此函式調整 obj 的內部記憶體緩衝區大小,obj 必須是 ctypes 型別的例項。不能使緩衝區小於物件型別的本機大小(由 sizeof(type(obj)) 給出),但可以擴大緩衝區。

ctypes.set_errno(value)

將呼叫執行緒中系統 errno 變數的 ctypes 私有副本的當前值設定為 value 並返回之前的值。

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

ctypes.set_last_error(value)

將呼叫執行緒中系統 LastError 變數的 ctypes 私有副本的當前值設定為 value 並返回之前的值。

可用性: Windows

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

ctypes.sizeof(obj_or_type)

返回 ctypes 型別或例項記憶體緩衝區的位元組大小。與 C 的 sizeof 運算子作用相同。

ctypes.string_at(ptr, size=-1)

返回 void *ptr 處的位元組字串。如果指定了 size,則用作大小,否則假定字串以零結尾。

引發一個 審計事件 ctypes.string_at,並附帶引數 ptr, size

ctypes.WinError(code=None, descr=None)

建立 OSError 的一個例項。如果未指定 code,則呼叫 GetLastError() 來確定錯誤程式碼。如果未指定 descr,則呼叫 FormatError() 來獲取錯誤的文字描述。

可用性: Windows

在 3.3 版本發生變更: 過去會建立 WindowsError 的一個例項,現在它是 OSError 的別名。

ctypes.wstring_at(ptr, size=-1)

返回 void *ptr 處的寬字元字串。如果指定了 size,則用作字串的字元數,否則假定字串以零結尾。

引發一個 審計事件 ctypes.wstring_at,並附帶引數 ptr, size

ctypes.memoryview_at(ptr, size, readonly=False)

返回一個長度為 sizememoryview 物件,它引用從 void *ptr 開始的記憶體。

如果 readonly 為 true,則返回的 memoryview 物件不能用於修改底層記憶體。(透過其他方式進行的更改仍會反映在返回的物件中。)

此函式與 string_at() 類似,主要區別在於它不會建立指定記憶體的副本。它是 memoryview((c_byte * size).from_address(ptr)) 的語義等效(但更高效)的替代方案。(雖然 from_address() 只接受整數,但 ptr 也可以作為 ctypes.POINTERbyref() 物件給出。)

引發一個 審計事件 ctypes.memoryview_at,並附帶引數 address, size, readonly

在 3.14 版本加入。

資料型別

class ctypes._CData

這個非公開類是所有 ctypes 資料型別的通用基類。除其他事項外,所有 ctypes 型別例項都包含一個儲存 C 相容資料的記憶體塊;該記憶體塊的地址由 addressof() 輔助函式返回。另一個例項變數以 _objects 的形式公開;它包含其他需要保持存活的 Python 物件,以防記憶體塊包含指標。

ctypes 資料型別的通用方法,這些都是類方法(準確地說,它們是 元類 的方法):

from_buffer(source[, offset])

此方法返回一個共享 source 物件緩衝區的 ctypes 例項。source 物件必須支援可寫緩衝區介面。可選的 offset 引數指定源緩衝區中的位元組偏移量;預設為零。如果源緩衝區不夠大,則會引發 ValueError

引發一個 審計事件 ctypes.cdata/buffer,並附帶引數 pointer, size, offset

from_buffer_copy(source[, offset])

此方法建立一個 ctypes 例項,從必須可讀的 source 物件緩衝區複製資料。可選的 offset 引數指定源緩衝區中的位元組偏移量;預設為零。如果源緩衝區不夠大,則會引發 ValueError

引發一個 審計事件 ctypes.cdata/buffer,並附帶引數 pointer, size, offset

from_address(address)

此方法返回一個使用由 address 指定的記憶體的 ctypes 型別例項,address 必須是一個整數。

此方法以及其他間接呼叫此方法的方法,會引發一個 審計事件 ctypes.cdata,並附帶引數 address

from_param(obj)

此方法將 obj 適配為 ctypes 型別。當該型別出現在外部函式的 argtypes 元組中時,它會與外部函式呼叫中使用的實際物件一起被呼叫;它必須返回一個可用作函式呼叫引數的物件。

所有 ctypes 資料型別都有此classmethod的預設實現,通常在 obj 是該型別的例項時返回 obj。某些型別也接受其他物件。

in_dll(library, name)

此方法返回由共享庫匯出的 ctypes 型別例項。name 是匯出資料的符號名稱,library 是已載入的共享庫。

ctypes 資料型別的通用類變數:

__pointer_type__

透過為相應的 ctypes 資料型別呼叫 POINTER() 建立的指標型別。如果尚未建立指標型別,則該屬性不存在。

在 3.14 版本加入。

ctypes 資料型別的通用例項變數:

_b_base_

有時 ctypes 資料例項不擁有它們包含的記憶體塊,而是共享基物件的記憶體塊的一部分。只讀成員 _b_base_ 是擁有該記憶體塊的根 ctypes 物件。

_b_needsfree_

這個只讀變數在 ctypes 資料例項自己分配了記憶體塊時為 true,否則為 false。

_objects

此成員要麼是 None,要麼是包含需要保持存活以使記憶體塊內容保持有效的 Python 物件的字典。此物件僅用於除錯;切勿修改此字典的內容。

基本資料型別

class ctypes._SimpleCData

這個非公開類是所有基本 ctypes 資料型別的基類。在此提及它是因為它包含了基本 ctypes 資料型別的通用屬性。_SimpleCData_CData 的子類,因此它繼承了它們的方法和屬性。現在,不包含指標且本身不是指標的 ctypes 資料型別可以被 pickle。

例項只有一個屬性:

value

此屬性包含例項的實際值。對於整數和指標型別,它是一個整數;對於字元型別,它是一個單字元的位元組物件或字串;對於字元指標型別,它是一個 Python 位元組物件或字串。

當從 ctypes 例項中檢索 value 屬性時,通常每次都會返回一個新物件。ctypes 實現返回原始物件的功能,總是構造一個新物件。這對於所有其他 ctypes 物件例項也是如此。

基本資料型別,當作為外部函式呼叫結果返回時,或者例如透過檢索結構欄位成員或陣列成員時,會被透明地轉換為原生的 Python 型別。換句話說,如果一個外部函式的 restypec_char_p,你將總是收到一個 Python 位元組物件,而不是一個 c_char_p 例項。

基本資料型別的子類繼承此行為。因此,如果一個外部函式的 restypec_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_double_complex

表示 C double complex 資料型別(如果可用)。建構函式接受一個可選的 complex 初始化值。

在 3.14 版本加入。

class ctypes.c_float_complex

表示 C float complex 資料型別(如果可用)。建構函式接受一個可選的 complex 初始化值。

在 3.14 版本加入。

class ctypes.c_longdouble_complex

表示 C long double complex 資料型別(如果可用)。建構函式接受一個可選的 complex 初始化值。

在 3.14 版本加入。

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_time_t

表示 C time_t 資料型別。

3.12 新版功能.

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)。它的值可以是 TrueFalse,建構函式接受任何具有真值的物件。

class ctypes.HRESULT

表示一個 HRESULT 值,其中包含函式或方法呼叫的成功或錯誤資訊。

可用性: Windows

class ctypes.py_object

表示 C PyObject* 資料型別。不帶引數呼叫它會建立一個 NULL PyObject* 指標。

在 3.14 版本發生變更: py_object 現在是一個 泛型

ctypes.wintypes 模組提供了相當多其他 Windows 特定的資料型別,例如 HWNDWPARAMDWORD。一些有用的結構體如 MSGRECT 也被定義了。

結構化資料型別

class ctypes.Union(*args, **kw)

原生位元組序聯合體的抽象基類。

聯合體與結構體共享共同的屬性和行為;詳見 Structure 文件。

class ctypes.BigEndianUnion(*args, **kw)

大端位元組序聯合體的抽象基類。

在 3.11 版本中新增。

class ctypes.LittleEndianUnion(*args, **kw)

小端位元組序聯合體的抽象基類。

在 3.11 版本中新增。

class ctypes.BigEndianStructure(*args, **kw)

大端位元組序結構體的抽象基類。

class ctypes.LittleEndianStructure(*args, **kw)

小端位元組序結構體的抽象基類。

非原生位元組序的結構體和聯合體不能包含指標型別欄位,或任何其他包含指標型別欄位的資料型別。

class ctypes.Structure(*args, **kw)

原生位元組序結構體的抽象基類。

具體的結構體和聯合體型別必須透過子類化這些型別之一來建立,並且至少要定義一個 _fields_ 類變數。ctypes 將建立描述符,允許透過直接的屬性訪問來讀寫欄位。這些是

_fields_

一個定義結構體欄位的序列。其中的項必須是二元組或三元組。第一項是欄位的名稱,第二項指定欄位的型別;它可以是任何 ctypes 資料型別。

對於像 c_int 這樣的整數型別欄位,可以給出第三個可選的項。它必須是一個小的正整數,定義欄位的位寬。

在一個結構體或聯合體中,欄位名必須是唯一的。這一點不會被檢查,當名稱重複時,只有一個欄位可以被訪問。

可以在定義 Structure 子類的 class 語句之後定義 _fields_ 類變數,這允許建立直接或間接引用自身的資料型別。

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

_fields_ 類變數只能設定一次。之後的賦值將引發 AttributeError

此外,_fields_ 類變數必須在結構體或聯合體型別首次使用前定義:建立例項或子類,對其呼叫 sizeof() 等。之後對 _fields_ 的賦值將引發 AttributeError。如果在這樣的使用之前沒有設定 _fields_,那麼結構體或聯合體將沒有自己的欄位,就好像 _fields_ 是空的一樣。

結構體型別的子類的子類會繼承基類的欄位,再加上在子類的子類中定義的 _fields_(如果有的話)。

_pack_

一個可選的小整數,允許在例項中覆蓋結構體欄位的對齊方式。

這僅對與 MSVC 相容的記憶體佈局實現(見 _layout_)。

_pack_ 設定為 0 與完全不設定它是一樣的。否則,該值必須是 2 的正整數次冪。其效果等同於 C 中的 #pragma pack(N),只是 ctypes 可能允許比編譯器接受的更大的 n

_fields_ 被賦值時,_pack_ 必須已經被定義,否則它將不起作用。

自 3.14 版本起不推薦使用,將在 3.19 版本中移除: 由於歷史原因,如果 _pack_ 不為零,預設將使用與 MSVC 相容的佈局。在非 Windows 平臺上,這個預設行為已被棄用,並計劃在 Python 3.19 中成為一個錯誤。如果這是你想要的行為,請明確地將 _layout_ 設定為 'ms'

_align_

一個可選的小整數,允許在將結構體打包或解包到/從記憶體時增加其對齊方式。

該值不能為負數。其效果等同於 GCC 上的 __attribute__((aligned(N))) 或 MSVC 上的 #pragma align(N),只是 ctypes 可能允許編譯器會拒絕的值。

_align_ 只能增加結構體的對齊要求。將其設定為 0 或 1 沒有效果。

不鼓勵使用非 2 的冪次的值,這可能導致意外的行為。

_fields_ 被賦值時,_align_ 必須已經被定義,否則它將不起作用。

在 3.13 版本加入。

_layout_

一個可選的字串,用於命名結構體/聯合體的佈局。目前可以設定為

  • "ms":微軟編譯器(MSVC)使用的佈局。在 GCC 和 Clang 上,可以透過 __attribute__((ms_struct)) 選擇此佈局。

  • "gcc-sysv":GCC 使用的 System V 或“類 SysV”資料模型的佈局,如在 Linux 和 macOS 上使用的。使用此佈局時,_pack_ 必須未設定或為零。

如果未明確設定,ctypes 將使用與平臺約定相匹配的預設值。此預設值可能會在未來的 Python 版本中更改(例如,當新平臺獲得官方支援時,或者當發現相似平臺之間的差異時)。目前預設值為

  • 在 Windows 上:"ms"

  • 當指定了 _pack_ 時:"ms"。(這已被棄用;參見 _pack_ 文件。)

  • 否則:"gcc-sysv"

_fields_ 被賦值時,_layout_ 必須已經被定義,否則它將不起作用。

在 3.14 版本加入。

_anonymous_

一個可選的序列,列出未命名(匿名)欄位的名稱。_anonymous_ 必須在 _fields_ 被賦值時已經定義,否則它將不起作用。

此變數中列出的欄位必須是結構體或聯合體型別的欄位。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.lptdesctd.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.CField(*args, **kw)

StructureUnion 欄位的描述符。例如:

>>> class Color(Structure):
...     _fields_ = (
...         ('red', c_uint8),
...         ('green', c_uint8),
...         ('blue', c_uint8),
...         ('intense', c_bool, 1),
...         ('blinking', c_bool, 1),
...    )
...
>>> Color.red
<ctypes.CField 'red' type=c_ubyte, ofs=0, size=1>
>>> Color.green.type
<class 'ctypes.c_ubyte'>
>>> Color.blue.byte_offset
2
>>> Color.intense
<ctypes.CField 'intense' type=c_bool, ofs=3, bit_size=1, bit_offset=0>
>>> Color.blinking.bit_offset
1

所有屬性都是隻讀的。

CField 物件是透過 _fields_ 建立的;不要直接例項化該類。

在 3.14 版本加入: 以前,描述符只有 offsetsize 屬性以及一個可讀的字串表示;CField 類不能直接使用。

name

欄位的名稱,為字串。

type

欄位的型別,為 ctypes 類

offset
byte_offset

欄位的偏移量,以位元組為單位。

對於位欄位,這是底層位元組對齊的*儲存單元*的偏移量;參見 bit_offset

byte_size

欄位的大小,以位元組為單位。

對於位欄位,這是底層*儲存單元*的大小。通常,它與位欄位的型別大小相同。

size

對於非位欄位,等同於 byte_size

對於位欄位,這包含一個向後相容的位壓縮值,它結合了 bit_sizebit_offset。請優先使用明確的屬性。

is_bitfield

如果這是一個位欄位,則為 True。

bit_offset
bit_size

位欄位在其*儲存單元*內的位置,即在從 byte_offset 開始的 byte_size 位元組記憶體中。

要獲取欄位的值,請將儲存單元讀取為整數,左移 bit_offset 位,並取 bit_size 個最低有效位。

對於非位欄位,bit_offset 為零,bit_size 等於 byte_size * 8

is_anonymous

如果此欄位是匿名的,則為 True,即它包含應合併到包含它的結構體或聯合體中的巢狀子欄位。

陣列和指標

class ctypes.Array(*args)

陣列的抽象基類。

建立具體陣列型別的推薦方法是將任何 ctypes 資料型別與一個非負整數相乘。或者,您可以子類化此型別並定義 _length__type_ 類變數。可以使用標準的下標和切片訪問來讀寫陣列元素;對於切片讀取,結果物件本身*不是*一個 Array

_length_

一個正整數,指定陣列中的元素數量。超出範圍的下標會導致 IndexError。它將由 len() 返回。

_type_

指定陣列中每個元素的型別。

陣列子類的建構函式接受位置引數,用於按順序初始化元素。

ctypes.ARRAY(type, length)

建立一個數組。等同於 type * length,其中 *type* 是一個 ctypes 資料型別,*length* 是一個整數。

此函式被軟棄用,推薦使用乘法。目前沒有移除它的計劃。

class ctypes._Pointer

私有的、指標的抽象基類。

具體的指標型別是透過呼叫 POINTER() 並傳入將要指向的型別來建立的;這由 pointer() 自動完成。

如果指標指向一個數組,可以使用標準的下標和切片訪問來讀寫其元素。指標物件沒有大小,所以 len() 會引發 TypeError。負數下標會從指標*之前*的記憶體中讀取(如在 C 中一樣),而超出範圍的下標可能會因訪問衝突而崩潰(如果你幸運的話)。

_type_

指定所指向的型別。

contents

返回指標所指向的物件。對此屬性進行賦值會改變指標,使其指向所賦的物件。

異常

exception ctypes.ArgumentError

當外部函式呼叫無法轉換傳遞的某個引數時,會引發此異常。

exception ctypes.COMError(hresult, text, details)

當 COM 方法呼叫失敗時,會引發此異常。

hresult

代表錯誤程式碼的整數值。

text

錯誤訊息。

details

五元組 (descr, source, helpfile, helpcontext, progid)

descr 是文字描述。source 是引發錯誤的類或應用程式的與語言相關的 ProgIDhelpfile 是幫助檔案的路徑。helpcontext 是幫助上下文識別符號。progid 是定義該錯誤的介面的 ProgID

可用性: Windows

在 3.14 版本加入。