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 版本加入。