ctypes — Python 的外部函数库 — Python 文档
ctypes — Python 的外部函数库
ctypes 是 Python 的外部函数库。 它提供与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。 它可用于将这些库包装在纯 Python 中。
ctypes教程
注意:本教程中的代码示例使用 doctest 来确保它们确实有效。 由于某些代码示例在 Linux、Windows 或 Mac OS X 下的行为不同,因此它们在注释中包含 doctest 指令。
注意:一些代码示例引用了 ctypes c_int 类型。 在 sizeof(long) == sizeof(int)
它是 c_long 的别名的平台上。 因此,如果您期望 c_int 打印 c_long,您不应该感到困惑——它们实际上是相同的类型。
加载动态链接库
ctypes 导出 cdll,在 Windows 上 windll 和 oledll 对象,用于加载动态链接库。
您可以通过将库作为这些对象的属性进行访问来加载库。 cdll 加载使用标准 cdecl
调用约定导出函数的库,而 windll 库使用 stdcall
调用约定调用函数。 oledll 也使用 stdcall
调用约定,并假设函数返回 Windows HRESULT
错误代码。 错误代码用于在函数调用失败时自动引发 OSError 异常。
3.3 版更改:Windows 错误用于引发 WindowsError,现在是 OSError 的别名。
下面是一些适用于 Windows 的示例。 请注意,msvcrt
是包含大多数标准 C 函数的 MS 标准 C 库,并使用 cdecl 调用约定:
>>> from ctypes import *
>>> print(windll.kernel32)
<WinDLL 'kernel32', handle ... at ...>
>>> print(cdll.msvcrt)
<CDLL 'msvcrt', handle ... at ...>
>>> libc = cdll.msvcrt
>>>
Windows 会自动附加通常的 .dll
文件后缀。
笔记
通过 cdll.msvcrt
访问标准 C 库将使用该库的过时版本,该版本可能与 Python 使用的版本不兼容。 在可能的情况下,使用本机 Python 功能,或者导入并使用 msvcrt
模块。
在 Linux 上,需要指定文件名 包括 扩展名才能加载库,因此不能使用属性访问来加载库。 应该使用 dll 加载器的 LoadLibrary()
方法,或者您应该通过调用构造函数创建 CDLL 的实例来加载库:
>>> cdll.LoadLibrary("libc.so.6")
<CDLL 'libc.so.6', handle ... at ...>
>>> libc = CDLL("libc.so.6")
>>> libc
<CDLL 'libc.so.6', handle ... at ...>
>>>
从加载的 dll 访问函数
函数作为 dll 对象的属性被访问:
>>> from ctypes import *
>>> libc.printf
<_FuncPtr object at 0x...>
>>> print(windll.kernel32.GetModuleHandleA)
<_FuncPtr object at 0x...>
>>> print(windll.kernel32.MyOwnFunction)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "ctypes.py", line 239, in __getattr__
func = _StdcallFuncPtr(name, self)
AttributeError: function 'MyOwnFunction' not found
>>>
请注意,像 kernel32
和 user32
这样的 win32 系统 dll 通常会导出函数的 ANSI 和 UNICODE 版本。 UNICODE 版本在导出时在名称后附加了 W
,而 ANSI 版本在导出时在名称后附加了 A
。 win32 GetModuleHandle
函数返回给定模块名称的 模块句柄 ,具有以下 C 原型,并使用宏将其中之一公开为 GetModuleHandle
取决于是否定义了 UNICODE:
/* ANSI version */
HMODULE GetModuleHandleA(LPCSTR lpModuleName);
/* UNICODE version */
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);
windll 不会尝试通过魔法选择其中之一,您必须通过显式指定 GetModuleHandleA
或 GetModuleHandleW
来访问您需要的版本,然后用字节或字符串调用它对象分别。
有时,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 可调用函数一样调用这些函数。 此示例使用 time()
函数,该函数返回自 Unix 纪元以来以秒为单位的系统时间,以及 GetModuleHandleA()
函数,该函数返回一个 win32 模块句柄。
此示例使用 NULL
指针调用这两个函数(None
应用作 NULL
指针):
>>> print(libc.time(None))
1150640792
>>> print(hex(windll.kernel32.GetModuleHandleA(None)))
0x1d000000
>>>
当您使用 cdecl
调用约定调用 stdcall
函数时,会引发 ValueError,反之亦然:
>>> cdll.kernel32.GetModuleHandleA(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Procedure probably called with not enough arguments (4 bytes missing)
>>>
>>> windll.msvcrt.printf(b"spam")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Procedure probably called with too many arguments (4 bytes in excess)
>>>
要找出正确的调用约定,您必须查看 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
>>>
然而,有足够的方法可以使 Python 与 ctypes 崩溃,所以无论如何你都应该小心。 faulthandler 模块有助于调试崩溃(例如 由错误的 C 库调用产生的分段错误)。
None
、整数、字节对象和(unicode)字符串是唯一可以在这些函数调用中直接用作参数的原生 Python 对象。 None
作为 C NULL
指针传递,字节对象和字符串作为指向包含其数据的内存块的指针传递(char* 或 wchar_t* )。 Python 整数作为平台默认的 C int 类型传递,它们的值被屏蔽以适应 C 类型。
在继续使用其他参数类型调用函数之前,我们必须了解更多关于 ctypes 数据类型的信息。
基本数据类型
ctypes 定义了一些原始的 C 兼容数据类型:
ctypes 类型 | C型 | 蟒蛇型 |
---|---|---|
c_bool
|
_布尔 | 布尔 (1) |
c_char
|
字符 | 1 个字符的字节对象 |
c_wchar
|
wchar_t
|
1 个字符的字符串 |
c_byte
|
字符 | 整数 |
c_ubyte
|
无符号的字符 | 整数 |
c_short
|
短的 | 整数 |
c_ushort
|
无符号短 | 整数 |
c_int
|
整数 | 整数 |
c_uint
|
无符号整数 | 整数 |
c_long
|
长 | 整数 |
c_ulong
|
无符号长 | 整数 |
c_longlong
|
__int64 或 长长
|
整数 |
c_ulonglong
|
unsigned __int64 或 unsigned long long | 整数 |
c_size_t
|
size_t
|
整数 |
c_ssize_t
|
ssize_t 或 Py_ssize_t
|
整数 |
c_float
|
浮动 | 浮动 |
c_double
|
双倍的 | 浮动 |
c_longdouble
|
长双 | 浮动 |
c_char_p
|
char*(NUL 终止) | 字节对象或 None
|
c_wchar_p
|
wchar_t*(NUL 终止) | 字符串或 None
|
c_void_p
|
空白* | 整数或 None
|
- 构造函数接受任何具有真值的对象。
所有这些类型都可以通过使用正确类型和值的可选初始化程序调用它们来创建:
>>> c_int()
c_long(0)
>>> c_wchar_p("Hello, World")
c_wchar_p(140018365411392)
>>> c_ushort(-3)
c_ushort(65533)
>>>
由于这些类型是可变的,它们的值也可以在之后更改:
>>> i = c_int(42)
>>> print(i)
c_long(42)
>>> print(i.value)
42
>>> i.value = -99
>>> print(i.value)
-99
>>>
为指针类型 c_char_p、c_wchar_p 和 c_void_p 的实例分配一个新值会改变它们指向的 内存位置 , ]不是内存块的内容(当然不是,因为Python字节对象是不可变的):
>>> 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()
函数(仍可作为别名使用),以及早期 ctypes 版本中的 c_string()
函数。 要创建包含 C 类型 wchar_t
Unicode 字符的可变内存块,请使用 create_unicode_buffer() 函数。
调用函数,续
请注意,printf 打印到真正的标准输出通道,not 到 sys.stdout,因此这些示例仅适用于控制台提示符,而不适用于 IDLE或 PythonWin:
>>> printf = libc.printf
>>> printf(b"Hello, %s\n", b"World!")
Hello, World!
14
>>> printf(b"Hello, %S\n", "World!")
Hello, World!
14
>>> printf(b"%d bottles of beer\n", 42)
42 bottles of beer
19
>>> printf(b"%f bottles of beer\n", 42.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ArgumentError: argument 2: exceptions.TypeError: Don't know how to convert parameter 2
>>>
如前所述,除整数、字符串和字节对象之外的所有 Python 类型都必须包装在其对应的 ctypes 类型中,以便将它们转换为所需的 C 数据类型:
>>> printf(b"An int %d, a double %f\n", 1234, c_double(3.14))
An int 1234, a double 3.140000
31
>>>
使用您自己的自定义数据类型调用函数
您还可以自定义 ctypes 参数转换,以允许将您自己的类的实例用作函数参数。 ctypes 查找 _as_parameter_
属性并将其用作函数参数。 当然,它必须是整数、字符串或字节之一:
>>> class Bottles:
... def __init__(self, number):
... self._as_parameter_ = number
...
>>> bottles = Bottles(42)
>>> printf(b"%d bottles of beer\n", bottles)
42 bottles of beer
19
>>>
如果您不想将实例的数据存储在 _as_parameter_
实例变量中,您可以定义一个 属性 ,以便根据请求提供该属性。
指定所需的参数类型(函数原型)
可以通过设置 argtypes
属性来指定从 DLL 导出的函数所需的参数类型。
argtypes
必须是 C 数据类型的序列(printf
函数在这里可能不是一个很好的例子,因为它根据格式字符串需要一个变量数和不同类型的参数,在另一方面,这对于试验此功能非常方便):
>>> printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
>>> printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2)
String 'Hi', Int 10, Double 2.200000
37
>>>
指定格式可以防止不兼容的参数类型(就像 C 函数的原型一样),并尝试将参数转换为有效类型:
>>> printf(b"%d %d %d", 1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ArgumentError: argument 2: exceptions.TypeError: wrong type
>>> printf(b"%s %d %f\n", b"X", 2, 3)
X 2 3.000000
13
>>>
如果您定义了自己的类并传递给函数调用,则必须实现一个 from_param()
类方法,以便它们能够在 argtypes
序列中使用它们。 from_param()
类方法接收传递给函数调用的 Python 对象,它应该进行类型检查或任何需要以确保该对象是可接受的,然后返回对象本身,它的 _as_parameter_
属性,或者在这种情况下您想作为 C 函数参数传递的任何内容。 同样,结果应该是整数、字符串、字节、ctypes 实例或具有 _as_parameter_
属性的对象。
返回类型
默认情况下,函数假定返回 C int 类型。 其他返回类型可以通过设置函数对象的restype
属性来指定。
这是一个更高级的例子,它使用 strchr
函数,它需要一个字符串指针和一个字符,并返回一个指向字符串的指针:
>>> strchr = libc.strchr
>>> strchr(b"abcdef", ord("d"))
8059983
>>> strchr.restype = c_char_p # c_char_p is a pointer to a string
>>> strchr(b"abcdef", ord("d"))
b'def'
>>> print(strchr(b"abcdef", ord("x")))
None
>>>
如果要避免上面的 ord("x")
调用,可以设置 argtypes
属性,第二个参数将从单个字符的 Python 字节对象转换为 C 字符:
>>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]
>>> strchr(b"abcdef", b"d")
'def'
>>> strchr(b"abcdef", b"def")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ArgumentError: argument 2: exceptions.TypeError: one character string expected
>>> print(strchr(b"abcdef", b"x"))
None
>>> strchr(b"abcdef", b"d")
'def'
>>>
如果外部函数返回整数,您还可以使用可调用的 Python 对象(例如函数或类)作为 restype
属性。 将使用 C 函数返回的 整数 调用可调用对象,并且此调用的结果将用作函数调用的结果。 这对于检查错误返回值并自动引发异常很有用:
>>> GetModuleHandle = windll.kernel32.GetModuleHandleA
>>> def ValidHandle(value):
... if value == 0:
... raise WinError()
... return value
...
>>>
>>> GetModuleHandle.restype = ValidHandle
>>> GetModuleHandle(None)
486539264
>>> GetModuleHandle("something silly")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in ValidHandle
OSError: [Errno 126] The specified module could not be found.
>>>
WinError
是一个函数,它将调用 Windows FormatMessage()
api 来获取错误代码的字符串表示,而 返回 异常。 WinError
带有一个可选的错误代码参数,如果没有使用它,它会调用 GetLastError() 来检索它。
请注意,通过 errcheck
属性可以使用更强大的错误检查机制; 有关详细信息,请参阅参考手册。
传递指针(或:通过引用传递参数)
有时一个 C api 函数需要一个指向数据类型的 指针 作为参数,可能是写入相应的位置,或者如果数据太大而无法通过值传递。 这也称为 通过引用 传递参数。
ctypes 导出 byref() 函数,用于通过引用传递参数。 使用pointer()函数也可以达到同样的效果,虽然pointer()做了很多工作,因为它构造了一个真正的指针对象,所以使用速度更快]byref() 如果你不需要 Python 本身的指针对象:
>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print(i.value, f.value, repr(s.value))
0 0.0 b''
>>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s",
... byref(i), byref(f), s)
3
>>> print(i.value, f.value, repr(s.value))
1 3.1400001049 b'Hello'
>>>
结构和工会
结构和联合必须从 ctypes 模块中定义的 Structure 和 Union 基类派生。 每个子类必须定义一个 _fields_
属性。 _fields_
必须是 2 元组 的列表,包含 字段名称 和 字段类型 。
字段类型必须是 ctypes 类型,如 c_int,或任何其他派生的 ctypes 类型:结构、联合、数组、指针。
下面是一个简单的 POINT 结构示例,其中包含两个名为 x 和 y 的整数,并且还展示了如何在构造函数中初始化结构:
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = [("x", c_int),
... ("y", c_int)]
...
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20
>>> point = POINT(y=5)
>>> print(point.x, point.y)
0 5
>>> POINT(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: too many initializers
>>>
但是,您可以构建更复杂的结构。 通过将结构用作字段类型,结构本身可以包含其他结构。
这是一个 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))
字段 描述符 可以从 类 中检索,它们对调试很有用,因为它们可以提供有用的信息:
>>> print(POINT.x)
<Field type=c_long, ofs=0, size=4>
>>> print(POINT.y)
<Field type=c_long, ofs=4, size=4>
>>>
结构/联合对齐和字节顺序
默认情况下,Structure 和 Union 字段的对齐方式与 C 编译器的对齐方式相同。 可以通过在子类定义中指定 _pack_
类属性来覆盖此行为。 这必须设置为正整数并指定字段的最大对齐方式。 这也是 #pragma pack(n)
在 MSVC 中所做的。
ctypes 使用结构和联合的本机字节顺序。 要构建具有非本机字节顺序的结构,您可以使用 BigEndianStructure、LittleEndianStructure、BigEndianUnion
和 LittleEndianUnion
基类之一。 这些类不能包含指针字段。
结构体和联合体中的位域
可以创建包含位域的结构和联合。 位域仅适用于整数域,位宽被指定为 _fields_
元组中的第三项:
>>> class Int(Structure):
... _fields_ = [("first_16", c_int, 16),
... ("second_16", c_int, 16)]
...
>>> print(Int.first_16)
<Field type=c_long, ofs=0:0, bits=16>
>>> print(Int.second_16)
<Field type=c_long, ofs=0:16, bits=16>
>>>
数组
数组是序列,包含固定数量的相同类型的实例。
创建数组类型的推荐方法是将数据类型与正整数相乘:
TenPointsArrayType = POINT * 10
这是一个有点人为的数据类型的示例,该结构包含 4 个 POINT 以及其他内容:
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = ("x", c_int), ("y", c_int)
...
>>> class MyStruct(Structure):
... _fields_ = [("a", c_int),
... ("b", c_float),
... ("point_array", POINT * 4)]
>>>
>>> print(len(MyStruct().point_array))
4
>>>
实例以通常的方式创建,通过调用类:
arr = TenPointsArrayType()
for pt in arr:
print(pt.x, pt.y)
上面的代码打印了一系列 0 0
行,因为数组内容被初始化为零。
还可以指定正确类型的初始化程序:
>>> from ctypes import *
>>> TenIntegers = c_int * 10
>>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> print(ii)
<c_long_Array_10 object at 0x...>
>>> for i in ii: print(i, end=" ")
...
1 2 3 4 5 6 7 8 9 10
>>>
指针
指针实例是通过在 ctypes 类型上调用 pointer() 函数来创建的:
>>> from ctypes import *
>>> i = c_int(42)
>>> pi = pointer(i)
>>>
指针实例有一个 contents 属性,它返回指针指向的对象,上面的 i
对象:
>>> pi.contents
c_long(42)
>>>
请注意, ctypes 没有 OOR(原始对象返回),每次检索属性时它都会构造一个新的等效对象:
>>> pi.contents is i
False
>>> pi.contents is pi.contents
False
>>>
将另一个 c_int 实例分配给指针的内容属性会导致指针指向存储它的内存位置:
>>> i = c_int(99)
>>> pi.contents = i
>>> pi.contents
c_long(99)
>>>
指针实例也可以用整数索引:
>>> pi[0]
99
>>>
分配给整数索引会更改指向的值:
>>> print(i)
c_long(99)
>>> pi[0] = 22
>>> print(i)
c_long(22)
>>>
也可以使用不同于 0 的索引,但您必须知道自己在做什么,就像在 C 中一样:您可以访问或更改任意内存位置。 通常,如果您从 C 函数接收指针,并且您 知道 该指针实际上指向一个数组而不是单个项目,则您只会使用此功能。
在幕后,pointer() 函数不仅仅是创建指针实例,它还必须首先创建指针 types。 这是通过 POINTER() 函数完成的,该函数接受任何 ctypes 类型,并返回一个新类型:
>>> PI = POINTER(c_int)
>>> PI
<class 'ctypes.LP_c_long'>
>>> PI(42)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: expected c_long instead of int
>>> PI(c_int(42))
<ctypes.LP_c_long object at 0x...>
>>>
不带参数调用指针类型会创建一个 NULL
指针。 NULL
指针有一个 False
布尔值:
>>> null_ptr = POINTER(c_int)()
>>> print(bool(null_ptr))
False
>>>
ctypes 在解引用指针时检查 NULL
(但解引用无效的非 NULL
指针会使 Python 崩溃):
>>> null_ptr[0]
Traceback (most recent call last):
....
ValueError: NULL pointer access
>>>
>>> null_ptr[0] = 1234
Traceback (most recent call last):
....
ValueError: NULL pointer access
>>>
类型转换
通常,ctypes 会进行严格的类型检查。 这意味着,如果在函数的 argtypes
列表中有 POINTER(c_int)
或作为结构定义中成员字段的类型,则只接受完全相同类型的实例。 此规则有一些例外,其中 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
在 class 语句本身中不可用。 在 ctypes 中,我们可以定义 cell
类,然后在 class 语句之后设置 _fields_
属性:
>>> from ctypes import *
>>> class cell(Structure):
... pass
...
>>> cell._fields_ = [("name", c_char_p),
... ("next", POINTER(cell))]
>>>
让我们试试吧。 我们创建了两个 cell
的实例,并让它们相互指向,最后沿着指针链走几次:
>>> c1 = cell()
>>> c1.name = b"foo"
>>> c2 = cell()
>>> c2.name = b"bar"
>>> c1.next = pointer(c2)
>>> c2.next = pointer(c1)
>>> p = c1
>>> for i in range(8):
... print(p.name, end=" ")
... p = p.next[0]
...
foo bar foo bar foo bar foo bar
>>>
回调函数
ctypes 允许从 Python 可调用对象创建 C 可调用函数指针。 这些有时称为 回调函数 。
首先,您必须为回调函数创建一个类。 该类知道调用约定、返回类型以及此函数将接收的参数的数量和类型。
CFUNCTYPE() 工厂函数使用 cdecl
调用约定为回调函数创建类型。 在 Windows 上,WINFUNCTYPE() 工厂函数使用 stdcall
调用约定为回调函数创建类型。
这两个工厂函数都以结果类型作为第一个参数被调用,回调函数期望的参数类型作为剩余的参数。
我将在这里展示一个使用标准 C 库的 qsort()
函数的示例,该函数用于在回调函数的帮助下对项目进行排序。 qsort()
将用于对整数数组进行排序:
>>> IntArray5 = c_int * 5
>>> ia = IntArray5(5, 1, 7, 33, 99)
>>> qsort = libc.qsort
>>> qsort.restype = None
>>>
qsort()
必须使用指向要排序的数据的指针、数据数组中的项数、一项的大小以及指向比较函数、回调的指针来调用。 然后将使用两个指向项目的指针调用回调,如果第一个项目小于第二个项目,则它必须返回一个负整数,如果它们相等,则返回零,否则返回一个正整数。
所以我们的回调函数接收指向整数的指针,并且必须返回一个整数。 首先我们为回调函数创建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
>>>
笔记
确保保留对 CFUNCTYPE() 对象的引用,只要它们从 C 代码中使用即可。 ctypes 没有,如果你不这样做,它们可能会被垃圾收集,在进行回调时使你的程序崩溃。
另外,请注意,如果在 Python 控制之外创建的线程中调用回调函数(例如 通过调用回调的外部代码),ctypes 在每次调用时创建一个新的虚拟 Python 线程。 这种行为在大多数情况下是正确的,但这意味着使用 threading.local 存储的值将 not 在不同的回调中存活,即使这些调用来自同一个 C 线程。
访问从 dll 导出的值
一些共享库不仅导出函数,还导出变量。 Python 库本身中的一个示例是 Py_OptimizeFlag,一个整数设置为 0、1 或 2,具体取决于给定的 -O 或 -OO 标志在启动时。
ctypes 可以使用该类型的 in_dll()
类方法访问这样的值。 pythonapi 是一个预定义的符号,可以访问 Python C api:
>>> opt_flag = c_int.in_dll(pythonapi, "Py_OptimizeFlag")
>>> print(opt_flag)
c_long(0)
>>>
如果解释器以 -O 开始,则样本将打印 c_long(1)
,或 c_long(2)
如果已指定 -OO。
一个扩展示例也演示了如何使用指针访问 Python 导出的 PyImport_FrozenModules 指针。
引用该值的文档:
这个指针被初始化为指向一个 struct _frozen 记录数组,由一个成员都是
NULL
或零的记录终止。 导入冻结模块时,将在此表中搜索。 第三方代码可以利用此技巧来提供动态创建的冻结模块集合。
所以操纵这个指针甚至可以证明是有用的。 为了限制示例大小,我们仅展示如何使用 ctypes 读取此表:
>>> from ctypes import *
>>>
>>> class struct_frozen(Structure):
... _fields_ = [("name", c_char_p),
... ("code", POINTER(c_ubyte)),
... ("size", c_int)]
...
>>>
我们已经定义了 struct _frozen 数据类型,所以我们可以得到指向表的指针:
>>> FrozenTable = POINTER(struct_frozen)
>>> table = FrozenTable.in_dll(pythonapi, "PyImport_FrozenModules")
>>>
由于 table
是 struct_frozen
记录数组的 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
__hello__ 161
__phello__ -161
__phello__.spam 161
>>>
标准 Python 有一个冻结模块和一个冻结包(由负的 size
成员表示)这一事实并不为人所知,它仅用于测试。 例如,尝试使用 import __hello__
。
惊喜
在 ctypes 中有一些边缘,你可能会期待一些与实际发生的情况不同的东西。
考虑以下示例:
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = ("x", c_int), ("y", c_int)
...
>>> class RECT(Structure):
... _fields_ = ("a", POINT), ("b", POINT)
...
>>> p1 = POINT(1, 2)
>>> p2 = POINT(3, 4)
>>> rc = RECT(p1, p2)
>>> print(rc.a.x, rc.a.y, rc.b.x, rc.b.y)
1 2 3 4
>>> # now swap the two points
>>> rc.a, rc.b = rc.b, rc.a
>>> print(rc.a.x, rc.a.y, rc.b.x, rc.b.y)
3 4 3 4
>>>
嗯。 我们当然希望最后一条语句打印 3 4 1 2
。 发生了什么? 以下是上面 rc.a, rc.b = rc.b, rc.a
行的步骤:
>>> temp0, temp1 = rc.b, rc.a
>>> rc.a = temp0
>>> rc.b = temp1
>>>
请注意,temp0
和 temp1
是仍在使用上述 rc
对象的内部缓冲区的对象。 因此,执行 rc.a = temp0
会将 temp0
的缓冲区内容复制到 rc
的缓冲区中。 这反过来又会改变 temp1
的内容。 所以,最后一个赋值 rc.b = temp1
没有达到预期的效果。
请记住,从结构、联合和数组中检索子对象不会 复制 子对象,而是检索访问根对象的底层缓冲区的包装器对象。
另一个可能与人们期望的行为不同的示例是:
>>> s = c_char_p()
>>> s.value = b"abc def ghi"
>>> s.value
b'abc def ghi'
>>> s.value is s.value
False
>>>
为什么打印 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 参考
外部函数
如上一节所述,外部函数可以作为加载的共享库的属性进行访问。 以这种方式创建的函数对象默认接受任意数量的参数,接受任何 ctypes 数据实例作为参数,并返回库加载器指定的默认结果类型。 它们是私有类的实例:
- class ctypes._FuncPtr
C 可调用外部函数的基类。
外部函数的实例也是 C 兼容的数据类型; 它们代表 C 函数指针。
这种行为可以通过分配给外部函数对象的特殊属性来定制。
- restype
分配一个 ctypes 类型来指定外部函数的结果类型。 将
None
用于 void,该函数不返回任何内容。可以分配一个不是 ctypes 类型的可调用 Python 对象,在这种情况下,假定函数返回 C int,并且将使用此整数调用可调用对象,允许进一步处理或错误检查。 不推荐使用它,为了更灵活的后处理或错误检查,请使用 ctypes 数据类型作为 restype 并将可调用对象分配给 errcheck 属性。
- argtypes
分配一个 ctypes 类型的元组来指定函数接受的参数类型。 使用
stdcall
调用约定的函数只能使用与此元组长度相同数量的参数来调用; 使用 C 调用约定的函数也接受额外的、未指定的参数。当调用外部函数时,每个实参都传递给 argtypes 元组中项的
from_param()
类方法,该方法允许将实参适配到外部函数接受。 例如, argtypes 元组中的 c_char_p 项将使用 ctypes 转换规则将作为参数传递的字符串转换为字节对象。新:现在可以将项目放入不是 ctypes 类型的 argtypes 中,但每个项目必须有一个
from_param()
方法,该方法返回一个可用作参数的值(整数、字符串、ctypes 实例)。 这允许定义可以适应自定义对象作为函数参数的适配器。
- errcheck
为该属性分配一个 Python 函数或另一个可调用函数。 将使用三个或更多参数调用可调用对象:
- callable(result, func, arguments)
result 是外部函数返回的内容,由
restype
属性指定。func 是外部函数对象本身,这允许重用相同的可调用对象来检查或后处理多个函数的结果。
arguments 是一个包含最初传递给函数调用的参数的元组,这允许对所用参数的行为进行专门化。
此函数返回的对象将从外部函数调用返回,但如果外部函数调用失败,它也可以检查结果值并引发异常。
- exception ctypes.ArgumentError
- 当外部函数调用无法转换传递的参数之一时,会引发此异常。
函数原型
也可以通过实例化函数原型来创建外部函数。 函数原型类似于 C 中的函数原型; 它们描述了一个函数(返回类型、参数类型、调用约定)而不定义实现。 工厂函数必须使用所需的结果类型和函数的参数类型来调用,并且可以用作装饰器工厂,因此可以通过 @wrapper
语法应用于函数。 有关示例,请参阅 回调函数 。
- ctypes.CFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)
- 返回的函数原型创建使用标准 C 调用约定的函数。 该函数将在调用期间释放 GIL。 如果use_errno设置为true,系统errno变量的ctypes私有副本在调用前后与真实的errno值交换; use_last_error 对 Windows 错误代码执行相同操作。
- ctypes.WINFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)
- 仅限 Windows:返回的函数原型创建使用
stdcall
调用约定的函数,但在 Windows CE 上除外,其中 WINFUNCTYPE() 与 CFUNCTYPE() 相同。 该函数将在调用期间释放 GIL。 use_errno 和 use_last_error 含义同上。
- ctypes.PYFUNCTYPE(restype, *argtypes)
- 返回的函数原型创建使用 Python 调用约定的函数。 该函数将 不 在调用过程中释放 GIL。
这些工厂函数创建的函数原型可以通过不同的方式实例化,具体取决于调用中参数的类型和数量:
- prototype(address)
- 返回指定地址处的外部函数,该函数必须是整数。
- prototype(callable)
- 从 Python callable 创建 C 可调用函数(回调函数)。
- prototype(func_spec[, paramflags])
- 返回共享库导出的外部函数。 func_spec 必须是一个二元组
(name_or_ordinal, library)
。 第一项是作为字符串的导出函数的名称,或者作为小整数的导出函数的序数。 第二项是共享库实例。
- prototype(vtbl_index, name[, paramflags[, iid]])
返回将调用 COM 方法的外部函数。 vtbl_index是虚函数表的索引,一个小的非负整数。 name 是 COM 方法的名称。 iid 是一个可选的指向接口标识符的指针,用于扩展错误报告。
COM 方法使用特殊的调用约定:除了在
argtypes
元组中指定的那些参数之外,它们还需要一个指向 COM 接口的指针作为第一个参数。可选的 paramflags 参数创建具有比上述功能更多功能的外部函数包装器。
paramflags 必须是与
argtypes
长度相同的元组。此元组中的每一项都包含有关参数的更多信息,它必须是包含一项、两项或三项的元组。
第一项是一个整数,包含参数的方向标志组合:
- 1
- 指定函数的输入参数。
- 2
- 输出参数。 外部函数填充一个值。
- 4
- 默认为整数零的输入参数。
可选的第二项是字符串形式的参数名称。 如果指定了此项,则可以使用命名参数调用外部函数。
可选的第三项是该参数的默认值。
此示例演示如何包装 Windows MessageBoxW
函数,以便它支持默认参数和命名参数。 windows 头文件中的 C 声明是这样的:
WINUSERAPI int WINAPI
MessageBoxW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType);
这是 ctypes 的包装:
>>> from ctypes import c_int, WINFUNCTYPE, windll
>>> from ctypes.wintypes import HWND, LPCWSTR, UINT
>>> prototype = WINFUNCTYPE(c_int, HWND, LPCWSTR, LPCWSTR, UINT)
>>> paramflags = (1, "hwnd", 0), (1, "text", "Hi"), (1, "caption", "Hello from ctypes"), (1, "flags", 0)
>>> MessageBox = prototype(("MessageBoxW", windll.user32), paramflags)
MessageBox
外部函数现在可以通过以下方式调用:
>>> MessageBox()
>>> MessageBox(text="Spam, spam, spam")
>>> MessageBox(flags=2, text="foo bar")
第二个示例演示了输出参数。 win32 GetWindowRect
函数通过将它们复制到调用者必须提供的 RECT
结构中来检索指定窗口的尺寸。 这是 C 声明:
WINUSERAPI BOOL WINAPI
GetWindowRect(
HWND hWnd,
LPRECT lpRect);
这是 ctypes 的包装:
>>> from ctypes import POINTER, WINFUNCTYPE, windll, WinError
>>> from ctypes.wintypes import BOOL, HWND, RECT
>>> prototype = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
>>> paramflags = (1, "hwnd"), (2, "lprect")
>>> GetWindowRect = prototype(("GetWindowRect", windll.user32), paramflags)
>>>
如果有一个输出参数值,带有输出参数的函数将自动返回输出参数值,或者当有多个输出参数值时,将自动返回一个包含输出参数值的元组,因此 GetWindowRect 函数现在在调用时返回一个 RECT 实例。
输出参数可以结合errcheck
协议进行进一步的输出处理和错误检查。 win32 GetWindowRect
api 函数返回一个 BOOL
来表示成功或失败,因此该函数可以进行错误检查,并在 api 调用失败时引发异常:
>>> def errcheck(result, func, args):
... if not result:
... raise WinError()
... return args
...
>>> GetWindowRect.errcheck = errcheck
>>>
如果 errcheck
函数返回它接收到的参数元组不变,ctypes 继续它对输出参数所做的正常处理。 如果你想返回一个窗口坐标元组而不是一个 RECT
实例,你可以检索函数中的字段并返回它们,正常处理将不再发生:
>>> def errcheck(result, func, args):
... if not result:
... raise WinError()
... rc = args[1]
... return rc.left, rc.top, rc.bottom, rc.right
...
>>> GetWindowRect.errcheck = errcheck
>>>
实用功能
- ctypes.addressof(obj)
- 以整数形式返回内存缓冲区的地址。 obj 必须是 ctypes 类型的实例。
- ctypes.alignment(obj_or_type)
- 返回 ctypes 类型的对齐要求。 obj_or_type 必须是 ctypes 类型或实例。
- ctypes.byref(obj[, offset])
返回一个指向 obj 的轻量级指针,它必须是一个 ctypes 类型的实例。 offset 默认为零,并且必须是将添加到内部指针值的整数。
byref(obj, offset)
对应这个 C 代码:(((char *)&obj) + offset)
返回的对象只能用作外部函数调用参数。 它的行为类似于
pointer(obj)
,但构建速度要快得多。
- ctypes.cast(obj, type)
- 此函数类似于 C 中的强制转换运算符。 它返回一个 type 的新实例,它指向与 obj 相同的内存块。 type 必须是指针类型,obj 必须是可以解释为指针的对象。
- ctypes.create_string_buffer(init_or_size, size=None)
此函数创建一个可变字符缓冲区。 返回的对象是 c_char 的 ctypes 数组。
init_or_size 必须是指定数组大小的整数,或用于初始化数组项的字节对象。
如果将字节对象指定为第一个参数,则缓冲区将比其长度大一个项目,以便数组中的最后一个元素是 NUL 终止字符。 一个整数可以作为第二个参数传递,如果不应该使用字节的长度,它允许指定数组的大小。
- ctypes.create_unicode_buffer(init_or_size, size=None)
此函数创建一个可变的 unicode 字符缓冲区。 返回的对象是 c_wchar 的 ctypes 数组。
init_or_size 必须是指定数组大小的整数,或用于初始化数组项的字符串。
如果将字符串指定为第一个参数,则缓冲区将比字符串的长度大一个项目,以便数组中的最后一个元素是 NUL 终止字符。 一个整数可以作为第二个参数传递,如果不应该使用字符串的长度,它允许指定数组的大小。
- ctypes.DllCanUnloadNow()
- 仅适用于 Windows:此函数是一个钩子,它允许使用 ctypes 实现进程内 COM 服务器。 它是从 _ctypes 扩展 dll 导出的 DllCanUnloadNow 函数调用的。
- ctypes.DllGetClassObject()
- 仅适用于 Windows:此函数是一个钩子,它允许使用 ctypes 实现进程内 COM 服务器。 它是从
_ctypes
扩展 dll 导出的 DllGetClassObject 函数调用的。
- ctypes.util.find_library(name)
尝试查找库并返回路径名。 name 是没有任何前缀的库名,如
lib
,后缀如.so
,.dylib
或版本号(这是用于 posix 链接器的形式选项-l
)。 如果找不到库,则返回None
。确切的功能取决于系统。
- ctypes.util.find_msvcrt()
仅限 Windows:返回 Python 和扩展模块使用的 VC 运行时库的文件名。 如果无法确定库的名称,则返回
None
。如果您需要释放内存,例如,由调用
free(void *)
的扩展模块分配,那么使用分配内存的同一库中的函数很重要。
- ctypes.FormatError([code])
- 仅限 Windows:返回错误代码 code 的文本描述。 如果未指定错误代码,则通过调用 Windows api 函数 GetLastError 使用最后一个错误代码。
- ctypes.GetLastError()
- 仅限 Windows:返回 Windows 在调用线程中设置的最后一个错误代码。 此函数直接调用Windows GetLastError() 函数,它不返回错误代码的ctypes-private 副本。
- ctypes.get_errno()
- 返回调用线程中系统 errno 变量的 ctypes-private 副本的当前值。
- ctypes.get_last_error()
- 仅限 Windows:返回调用线程中系统
LastError
变量的 ctypes-private 副本的当前值。
- ctypes.memmove(dst, src, count)
- 与标准 C memmove 库函数相同:将 count 个字节从 src 复制到 dst。 dst 和 src 必须是可以转换为指针的整数或 ctypes 实例。
- ctypes.memset(dst, c, count)
- 与标准 C memset 库函数相同:用 count 个字节的值 c 填充地址 dst 处的内存块。 dst 必须是指定地址或 ctypes 实例的整数。
- ctypes.POINTER(type)
- 这个工厂函数创建并返回一个新的 ctypes 指针类型。 指针类型在内部被缓存和重用,所以重复调用这个函数很便宜。 type 必须是 ctypes 类型。
- ctypes.pointer(obj)
此函数创建一个新的指针实例,指向 obj。 返回的对象的类型为
POINTER(type(obj))
。注意:如果您只想将指向对象的指针传递给外部函数调用,您应该使用
byref(obj)
,它要快得多。
- ctypes.resize(obj, size)
- 此函数调整 obj 的内部内存缓冲区大小,它必须是 ctypes 类型的实例。 不可能使缓冲区小于对象类型的原始大小,如
sizeof(type(obj))
给出的,但可以扩大缓冲区。
- ctypes.set_errno(value)
- 将调用线程中系统 errno 变量的 ctypes-private 副本的当前值设置为 value 并返回之前的值。
- ctypes.set_last_error(value)
- 仅限 Windows:将调用线程中系统
LastError
变量的 ctypes-private 副本的当前值设置为 value 并返回先前的值。
- ctypes.sizeof(obj_or_type)
- 返回 ctypes 类型或实例内存缓冲区的大小(以字节为单位)。 与 C
sizeof
运算符相同。
- ctypes.string_at(address, size=- 1)
- 此函数返回从内存地址 address 开始的 C 字符串作为字节对象。 如果指定了大小,则将其用作大小,否则假定字符串以零结尾。
- ctypes.WinError(code=None, descr=None)
仅限 Windows:这个函数可能是 ctypes 中命名最差的东西。 它创建了一个 OSError 实例。 如果未指定 code,则调用
GetLastError
以确定错误代码。 如果未指定 descr,则调用 FormatError() 以获取错误的文本描述。3.3 版更改: 曾经创建 WindowsError 的实例。
- ctypes.wstring_at(address, size=- 1)
- 此函数以字符串形式返回从内存地址 address 开始的宽字符串。 如果指定了 size,则将其用作字符串的字符数,否则假定字符串以零结尾。
数据类型
- class ctypes._CData
这个非公共类是所有 ctypes 数据类型的公共基类。 除其他外,所有 ctypes 类型实例都包含一个存储 C 兼容数据的内存块; 内存块的地址由 addressof() 辅助函数返回。 另一个实例变量公开为 _objects; 这包含其他需要保持活动状态的 Python 对象,以防内存块包含指针。
ctypes数据类型的常用方法,这些都是类方法(准确的说是元类的方法):
- from_buffer(source[, offset])
此方法返回一个 ctypes 实例,该实例共享 source 对象的缓冲区。 source 对象必须支持可写缓冲区接口。 可选的 offset 参数以字节为单位指定源缓冲区的偏移量; 默认为零。 如果源缓冲区不够大,则会引发 ValueError。
- from_buffer_copy(source[, offset])
此方法创建一个 ctypes 实例,从必须可读的 source 对象缓冲区复制缓冲区。 可选的 offset 参数以字节为单位指定源缓冲区的偏移量; 默认为零。 如果源缓冲区不够大,则会引发 ValueError。
- from_address(address)
此方法使用 address 指定的内存返回一个 ctypes 类型实例,该内存必须是整数。
- from_param(obj)
此方法使 obj 适应 ctypes 类型。 当类型存在于外部函数的
argtypes
元组中时,使用外部函数调用中使用的实际对象调用它; 它必须返回一个可以用作函数调用参数的对象。所有 ctypes 数据类型都有此类方法的默认实现,如果它是类型的实例,则通常返回 obj。 有些类型也接受其他对象。
- in_dll(library, name)
此方法返回共享库导出的 ctypes 类型实例。 name为导出数据的符号名,library为加载的共享库。
ctypes 数据类型的常见实例变量:
- _b_base_
有时 ctypes 数据实例不拥有它们包含的内存块,而是共享基础对象的部分内存块。 _b_base_ 只读成员是拥有内存块的根 ctypes 对象。
- _b_needsfree_
当 ctypes 数据实例本身分配了内存块时,这个只读变量为真,否则为假。
- _objects
该成员是
None
或包含需要保持活动状态的 Python 对象的字典,以便内存块内容保持有效。 此对象仅用于调试; 永远不要修改这本词典的内容。
基本数据类型
- class ctypes._SimpleCData
这个非公共类是所有基本 ctypes 数据类型的基类。 在这里提到它是因为它包含基本 ctypes 数据类型的公共属性。 _SimpleCData 是 _CData 的子类,所以继承了它们的方法和属性。 现在可以腌制不包含也不包含指针的 ctypes 数据类型。
实例有一个属性:
- value
该属性包含实例的实际值。 对于整数和指针类型,它是一个整数,对于字符类型,它是一个单字符字节对象或字符串,对于字符指针类型,它是一个 Python 字节对象或字符串。
当从 ctypes 实例中检索
value
属性时,通常每次都会返回一个新对象。 ctypes 不实现原对象返回,总是构造一个新对象。 对于所有其他 ctypes 对象实例也是如此。
基本数据类型在作为外部函数调用结果返回时,或者例如通过检索结构字段成员或数组项时,会透明地转换为本地 Python 类型。 换句话说,如果一个外部函数有一个 c_char_p 的 restype
,你将总是收到一个 Python 字节对象,而不是 一个 c_char_p 实例.
基本数据类型的子类 不会 继承此行为。 因此,如果外部函数 restype
是 c_void_p 的子类,您将从函数调用中收到该子类的实例。 当然,您可以通过访问 value
属性来获取指针的值。
这些是基本的 ctypes 数据类型:
- class ctypes.c_byte
- 表示 C signed char 数据类型,并将值解释为小整数。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_char
- 表示 C char 数据类型,并将值解释为单个字符。 构造函数接受一个可选的字符串初始化器,字符串的长度必须正好是一个字符。
- class ctypes.c_char_p
- 当它指向以零结尾的字符串时,表示 C char* 数据类型。 对于也可能指向二进制数据的通用字符指针,必须使用
POINTER(c_char)
。 构造函数接受一个整数地址或一个字节对象。
- class ctypes.c_double
- 表示 C double 数据类型。 构造函数接受一个可选的 float 初始值设定项。
- class ctypes.c_longdouble
- 表示 C long double 数据类型。 构造函数接受一个可选的 float 初始值设定项。 在
sizeof(long double) == sizeof(double)
它是 c_double 的别名的平台上。
- class ctypes.c_float
- 表示 C float 数据类型。 构造函数接受一个可选的 float 初始值设定项。
- class ctypes.c_int
- 表示 C signed int 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。 在
sizeof(int) == sizeof(long)
它是 c_long 的别名的平台上。
- class ctypes.c_int8
- 表示 C 8 位 signed int 数据类型。 通常是 c_byte 的别名。
- class ctypes.c_int16
- 表示 C 16 位 signed int 数据类型。 通常是 c_short 的别名。
- class ctypes.c_int32
- 表示 C 32 位 signed int 数据类型。 通常是 c_int 的别名。
- class ctypes.c_int64
- 表示 C 64 位 signed int 数据类型。 通常是 c_longlong 的别名。
- class ctypes.c_long
- 表示 C signed long 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_longlong
- 表示 C signed long long 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_short
- 表示 C signed short 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_size_t
- 表示 C
size_t
数据类型。
- class ctypes.c_ssize_t
表示 C
ssize_t
数据类型。3.2 版中的新功能。
- class ctypes.c_ubyte
- 表示 C unsigned char 数据类型,它将值解释为小整数。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_uint
- 表示 C unsigned int 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。 在
sizeof(int) == sizeof(long)
它是 c_ulong 的别名的平台上。
- class ctypes.c_uint8
- 表示 C 8 位 unsigned int 数据类型。 通常是 c_ubyte 的别名。
- class ctypes.c_uint16
- 表示 C 16 位 unsigned int 数据类型。 通常是 c_ushort 的别名。
- class ctypes.c_uint32
- 表示 C 32 位 unsigned int 数据类型。 通常是 c_uint 的别名。
- class ctypes.c_uint64
- 表示 C 64 位 unsigned int 数据类型。 通常是 c_ulonglong 的别名。
- class ctypes.c_ulong
- 表示 C unsigned long 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_ulonglong
- 表示 C unsigned long long 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_ushort
- 表示 C unsigned short 数据类型。 构造函数接受一个可选的整数初始化器; 没有进行溢出检查。
- class ctypes.c_void_p
- 表示 C void* 类型。 该值表示为整数。 构造函数接受一个可选的整数初始值设定项。
- class ctypes.c_wchar
- 表示 C
wchar_t
数据类型,并将值解释为单字符 unicode 字符串。 构造函数接受一个可选的字符串初始化器,字符串的长度必须正好是一个字符。
- class ctypes.c_wchar_p
- 表示 C wchar_t* 数据类型,它必须是指向以零结尾的宽字符串的指针。 构造函数接受一个整数地址或字符串。
- class ctypes.c_bool
- 表示 C bool 数据类型(更准确地说,是 C99 中的 _Bool)。 它的值可以是
True
或False
,构造函数接受任何具有真值的对象。
- class ctypes.HRESULT
- 仅限 Windows:表示
HRESULT
值,其中包含函数或方法调用的成功或错误信息。
- class ctypes.py_object
- 表示 C PyObject* 数据类型。 不带参数调用它会创建一个
NULL
PyObject* 指针。
ctypes.wintypes
模块提供了相当多的其他 Windows 特定数据类型,例如 HWND
、WPARAM
或 DWORD
。 还定义了一些有用的结构,如 MSG
或 RECT
。
结构化数据类型
- class ctypes.Union(*args, **kw)
- 本机字节顺序的联合的抽象基类。
- class ctypes.BigEndianStructure(*args, **kw)
- big endian 字节顺序结构的抽象基类。
- class ctypes.LittleEndianStructure(*args, **kw)
- little endian 字节顺序结构的抽象基类。
具有非本地字节顺序的结构不能包含指针类型字段,或包含指针类型字段的任何其他数据类型。
- class ctypes.Structure(*args, **kw)
native 字节顺序结构的抽象基类。
具体的结构和联合类型必须通过继承这些类型之一来创建,并且至少定义一个 _fields_ 类变量。 ctypes 将创建 描述符 ,允许通过直接属性访问读取和写入字段。 这些是
- _fields_
定义结构字段的序列。 项必须是 2 元组或 3 元组。 第一项是字段名称,第二项指定字段类型; 它可以是任何 ctypes 数据类型。
对于像 c_int 这样的整数类型字段,可以给出第三个可选项。 它必须是一个小的正整数,用于定义字段的位宽。
字段名称在一个结构或联合中必须是唯一的。 未选中此项,当名称重复时,只能访问一个字段。
可以在定义 Structure 子类的 class 语句之后定义 _fields_ 类变量 ,这允许创建直接或间接引用自身的数据类型:
class List(Structure): pass List._fields_ = [("pnext", POINTER(List)), ... ]
然而,_fields_ 类变量必须在第一次使用该类型之前定义(创建一个实例,在其上调用 sizeof(),等等)。 稍后对 _fields_ 类变量的赋值将引发 AttributeError。
可以定义结构类型的子子类,它们继承基类的字段加上子子类中定义的 _fields_(如果有)。
- _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.lptdesc
和td.u.lptdesc
是等价的,但前者更快,因为它不需要创建临时联合实例:td = TYPEDESC() td.vt = VT_PTR td.lptdesc = POINTER(some_type) td.u.lptdesc = POINTER(some_type)
可以定义结构的子子类,它们继承基类的字段。 如果子类定义具有单独的 _fields_ 变量,则在此指定的字段将附加到基类的字段。
结构和联合构造函数接受位置参数和关键字参数。 位置参数用于按照它们出现在 _fields_ 中的相同顺序初始化成员字段。 构造函数中的关键字参数被解释为属性赋值,因此它们将使用相同的名称初始化 _fields_,或者为 _fields_ 中不存在的名称创建新属性。
数组和指针
- class ctypes.Array(*args)
数组的抽象基类。
创建具体数组类型的推荐方法是将任何 ctypes 数据类型乘以一个正整数。 或者,您可以子类化此类型并定义 _length_ 和 _type_ 类变量。 可以使用标准下标和切片访问来读取和写入数组元素; 对于切片读取,结果对象是 不是 本身是 数组 。
- _length_
一个正整数,指定数组中的元素数。 超出范围的下标会导致 IndexError。 将由 len() 返回。
- _type_
指定数组中每个元素的类型。
数组子类构造函数接受位置参数,用于按顺序初始化元素。
- class ctypes._Pointer
指针的私有抽象基类。
具体的指针类型是通过使用将要指向的类型调用 POINTER() 来创建的; 这是由 pointer() 自动完成的。
如果指针指向数组,则可以使用标准下标和切片访问读取和写入其元素。 指针对象没有大小,因此 len() 将引发 TypeError。 负下标将在 之前从内存中读取 指针(如在 C 中),超出范围的下标可能会因访问冲突而崩溃(如果你很幸运)。
- _type_
指定指向的类型。
- contents
返回指针指向的对象。 分配给该属性会将指针更改为指向所分配的对象。