ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。
目前创新互联公司已为1000+的企业提供了网站建设、域名、网络空间、网站运营、企业网站设计、忻州网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
注:本教程中的示例代码使用 doctest 来保证它们能正确运行。 由于有些代码示例在 Linux, Windows 或 macOS 上的行为有所不同,它们在注释中包含了一些 doctest 指令。
注意:部分示例代码引用了 ctypes c_int 类型。在 sizeof(long) == sizeof(int)
的平台上此类型是 c_long 的一个别名。所以,在程序输出 c_long 而不是你期望的 c_int 时不必感到迷惑 —- 它们实际上是同一种类型。
ctypes 导出了 cdll 对象,在 Windows 系统中还导出了 windll 和 oledll 对象用于载入动态连接库。
通过操作这些对象的属性,你可以载入外部的动态链接库。cdll 载入按标准的 cdecl
调用协议导出的函数,而 windll 导入的库按 stdcall
调用协议调用其中的函数。 oledll 也按 stdcall
调用协议调用其中的函数,并假定该函数返回的是 Windows HRESULT
错误代码,并当函数调用失败时,自动根据该代码甩出一个 OSError 异常。
在 3.3 版更改: 原来在 Windows 下抛出的异常类型 WindowsError 现在是 OSError 的一个别名。
这是一些 Windows 下的例子。注意:msvcrt
是微软 C 标准库,包含了大部分 C 标准函数,这些函数都是以 cdecl 调用协议进行调用的。
>>> from ctypes import *
>>> print(windll.kernel32)
>>> print(cdll.msvcrt)
>>> libc = cdll.msvcrt
>>>
Windows 会自动添加通常的 .dll
文件扩展名。
备注
通过 cdll.msvcrt
调用的标准 C 函数,可能会导致调用一个过时的,与当前 Python 所不兼容的函数。因此,请尽量使用标准的 Python 函数,而不要使用 msvcrt
模块。
在 Linux 下,必须使用 包含 文件扩展名的文件名来导入共享库。因此不能简单使用对象属性的方式来导入库。因此,你可以使用方法 LoadLibrary()
,或构造 CDLL 对象来导入库。
>>> cdll.LoadLibrary("libc.so.6")
>>> libc = CDLL("libc.so.6")
>>> libc
>>>
通过操作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 "
", line 1, in File "ctypes.py", line 239, in __getattr__
func = _StdcallFuncPtr(name, self)
AttributeError: function 'MyOwnFunction' not found
>>>
注意:Win32 系统的动态库,比如 kernel32
和 user32
,通常会同时导出同一个函数的 ANSI 版本和 UNICODE 版本。UNICODE 版本通常会在名字最后以 W
结尾,而 ANSI 版本的则以 A
结尾。 win32的 GetModuleHandle
函数会根据一个模块名返回一个 模块句柄,该函数暨同时包含这样的两个版本的原型函数,并通过宏 UNICODE 是否定义,来决定宏 GetModuleHandle
导出的是哪个具体函数。
/* ANSI version */
HMODULE GetModuleHandleA(LPCSTR lpModuleName);
/* UNICODE version */
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);
windll 不会通过这样的魔法手段来帮你决定选择哪一种函数,你必须显式的调用 GetModuleHandleA
或 GetModuleHandleW
,并分别使用字节对象或字符串对象作参数。
有时候,dlls的导出的函数名不符合 Python 的标识符规范,比如 "??2@YAPAXI@Z"
。此时,你必须使用 getattr() 方法来获得该函数。
>>> getattr(cdll.msvcrt, "??2@YAPAXI@Z")
<_FuncPtr object at 0x...>
>>>
Windows 下,有些 dll 导出的函数没有函数名,而是通过其顺序号调用。对此类函数,你也可以通过 dll 对象的数值索引来操作这些函数。
>>> cdll.kernel32[1]
<_FuncPtr object at 0x...>
>>> cdll.kernel32[0]
Traceback (most recent call last):
File "
", line 1, in File "ctypes.py", line 310, in __getitem__
func = _StdcallFuncPtr(name, self)
AttributeError: function ordinal 0 not found
>>>
你可以貌似是调用其它 Python 函数那样直接调用这些函数。在这个例子中,我们调用了 time()
函数,该函数返回一个系统时间戳(从 Unix 时间起点到现在的秒数),而``GetModuleHandleA()`` 函数返回一个 win32 模块句柄。
此函数中调用的两个函数都使用了空指针(用 None
作为空指针):
>>> print(libc.time(None))
1150640792
>>> print(hex(windll.kernel32.GetModuleHandleA(None)))
0x1d000000
>>>
如果你用 cdecl
调用方式调用 stdcall
约定的函数,则会甩出一个异常 ValueError。反之亦然。
>>> cdll.kernel32.GetModuleHandleA(None)
Traceback (most recent call last):
File "
", line 1, in ValueError: Procedure probably called with not enough arguments (4 bytes missing)
>>>
>>> windll.msvcrt.printf(b"spam")
Traceback (most recent call last):
File "
", line 1, in ValueError: Procedure probably called with too many arguments (4 bytes in excess)
>>>
你必须阅读这些库的头文件或说明文档来确定它们的正确的调用协议。
在 Windows 中,ctypes 使用 win32 结构化异常处理来防止由于在调用函数时使用非法参数导致的程序崩溃。
>>> windll.kernel32.GetModuleHandleA(32)
Traceback (most recent call last):
File "
", line 1, in OSError: exception: access violation reading 0x00000020
>>>
然而,总有许多办法,通过调用 ctypes 使得 Python 程序崩溃。因此,你必须小心使用。 faulthandler 模块可以用于帮助诊断程序崩溃的原因。(比如由于错误的C库函数调用导致的段错误)。
None
, integers, bytes objects and (unicode) strings are the only native Python objects that can directly be used as parameters in these function calls. None
is passed as a C NULL
pointer, bytes objects and strings are passed as pointer to the memory block that contains their data (char* or wchar_t*). Python integers are passed as the platforms default C int type, their value is masked to fit into the C type.
在我们开始调用函数前,我们必须先了解作为函数参数的 ctypes 数据类型。
ctypes 定义了一些和C兼容的基本数据类型:
ctypes 类型 |
C 类型 |
Python 类型 |
---|---|---|
所有这些类型都可以通过使用正确类型和值的可选初始值调用它们来创建:
>>> c_int()
c_long(0)
>>> c_wchar_p("Hello, World")
c_wchar_p(140018365411392)
>>> c_ushort(-3)
c_ushort(65533)
>>>
由于这些类型是可变的,它们的值也可以在以后更改:
>>> i = c_int(42)
>>> print(i)
c_long(42)
>>> print(i.value)
42
>>> i.value = -99
>>> print(i.value)
-99
>>>
当给指针类型的对象 c_char_p, c_wchar_p 和 c_void_p 等赋值时,将改变它们所指向的 内存地址,而 不是 它们所指向的内存区域的 内容 (这是理所当然的,因为 Python 的 bytes 对象是不可变的):
>>> s = "Hello, World"
>>> c_s = c_wchar_p(s)
>>> print(c_s)
c_wchar_p(139966785747344)
>>> print(c_s.value)
Hello World
>>> c_s.value = "Hi, there"
>>> print(c_s) # the memory location has changed
c_wchar_p(139966783348904)
>>> print(c_s.value)
Hi, there
>>> print(s) # first object is unchanged
Hello, World
>>>
但你要注意不能将它们传递给会改变指针所指内存的函数。如果你需要可改变的内存块,ctypes 提供了 create_string_buffer() 函数,它提供多种方式创建这种内存块。当前的内存块内容可以通过 raw
属性存取,如果你希望将它作为NUL结束的字符串,请使用 value
属性:
>>> from ctypes import *
>>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes
>>> print(sizeof(p), repr(p.raw))
3 b'\x00\x00\x00'
>>> p = create_string_buffer(b"Hello") # create a buffer containing a NUL terminated string
>>> print(sizeof(p), repr(p.raw))
6 b'Hello\x00'
>>> print(repr(p.value))
b'Hello'
>>> p = create_string_buffer(b"Hello", 10) # create a 10 byte buffer
>>> print(sizeof(p), repr(p.raw))
10 b'Hello\x00\x00\x00\x00\x00'
>>> p.value = b"Hi"
>>> print(sizeof(p), repr(p.raw))
10 b'Hi\x00lo\x00\x00\x00\x00\x00'
>>>
The create_string_buffer() function replaces the old c_buffer()
function (which is still available as an alias). To create a mutable memory block containing unicode characters of the C type wchar_t, use the create_unicode_buffer() function.
注意 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 "
", line 1, in 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 参数转换方式来允许自定义类型作为参数。 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 "
", line 1, in ArgumentError: argument 2: 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_
属性的对象。
By default functions are assumed to return the C int type. Other return types can be specified by setting the restype
attribute of the function object.
这是个更高级的例子,它调用了 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 "
", line 1, in ArgumentError: argument 2: 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 "
", line 1, in File "
", line 3, in ValidHandle OSError: [Errno 126] The specified module could not be found.
>>>
WinError
函数可以调用 Windows 的 FormatMessage()
API 获取错误码的字符串说明,然后 返回 一个异常。 WinError
接收一个可选的错误码作为参数,如果没有的话,它将调用 GetLastError() 获取错误码。
请注意,使用 errcheck
属性可以实现更强大的错误检查手段;详情请见参考手册。
有时候 C 函数接口可能由于要往某个地址写入值,或者数据太大不适合作为值传递,从而希望接收一个 指针 作为数据参数类型。这和 传递参数引用 类似。
ctypes 暴露了 byref() 函数用于通过引用传递参数,使用 pointer() 函数也能达到同样的效果,只不过 pointer() 需要更多步骤,因为它要先构造一个真实指针对象。所以在 Python 代码本身不需要使用这个指针对象的情况下,使用 byref() 效率更高。
>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print(i.value, f.value, repr(s.value))
0 0.0 b''
>>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s",
... byref(i), byref(f), s)
3
>>> print(i.value, f.value, repr(s.value))
1 3.1400001049 b'Hello'
>>>
结构体和联合必须继承自 ctypes 模块中的 Structure 和 Union 。子类必须定义 _fields_
属性。 _fields_
是一个二元组列表,二元组中包含 field name 和 field type 。
type 字段必须是一个 ctypes 类型,比如 c_int,或者其他 ctypes 类型: 结构体、联合、数组、指针。
这是一个简单的 POINT 结构体,它包含名称为 x 和 y 的两个变量,还展示了如何通过构造函数初始化结构体。
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = [("x", c_int),
... ("y", c_int)]
...
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20
>>> point = POINT(y=5)
>>> print(point.x, point.y)
0 5
>>> POINT(1, 2, 3)
Traceback (most recent call last):
File "
", line 1, in TypeError: too many initializers
>>>
当然,你可以构造更复杂的结构体。一个结构体可以通过设置 type 字段包含其他结构体或者自身。
这是以一个 RECT 结构体,他包含了两个 POINT ,分别叫 upperleft 和 lowerright:
>>> class RECT(Structure):
... _fields_ = [("upperleft", POINT),
... ("lowerright", POINT)]
...
>>> rc = RECT(point)
>>> print(rc.upperleft.x, rc.upperleft.y)
0 5
>>> print(rc.lowerright.x, rc.lowerright.y)
0 0
>>>
嵌套结构体可以通过几种方式构造初始化:
>>> r = RECT(POINT(1, 2), POINT(3, 4))
>>> r = RECT((1, 2), (3, 4))
可以通过 类 获取字段 descriptor ,它能提供很多有用的调试信息。
>>> print(POINT.x)
>>> print(POINT.y)
>>>
警告
ctypes 不支持带位域的结构体、联合以值的方式传给函数。这可能在 32 位 x86 平台上可以正常工作,但是对于一般情况,这种行为是未定义的。带位域的结构体、联合应该总是通过指针传递给函数。
默认情况下,结构体和联合的字段与 C 的字节对齐是一样的。也可以在定义子类的时候指定类的 _pack_
属性来覆盖这种行为。 它必须设置为一个正整数,表示字段的最大对齐字节。这和 MSVC 中的 #pragma pack(n)
功能一样。
ctypes 中的结构体和联合使用的是本地字节序。要使用非本地字节序,可以使用 BigEndianStructure, LittleEndianStructure, BigEndianUnion, and LittleEndianUnion 作为基类。这些类不能包含指针字段。
结构体和联合中是可以包含位域字段的。位域只能用于整型字段,位长度通过 _fields_
中的第三个参数指定:
>>> class Int(Structure):
... _fields_ = [("first_16", c_int, 16),
... ("second_16", c_int, 16)]
...
>>> print(Int.first_16)
>>> print(Int.second_16)
>>>
数组是一个序列,包含指定个数元素,且必须类型相同。
创建数组类型的推荐方式是使用一个类型乘以一个正数:
TenPointsArrayType = POINT * 10
下面是一个构造的数据案例,结构体中包含了4个 POINT 和一些其他东西。
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = ("x", c_int), ("y", c_int)
...
>>> class MyStruct(Structure):
... _fields_ = [("a", c_int),
... ("b", c_float),
... ("point_array", POINT * 4)]
>>>
>>> print(len(MyStruct().point_array))
4
>>>
和平常一样,通过调用它创建实例:
arr = TenPointsArrayType()
for pt in arr:
print(pt.x, pt.y)
以上代码会打印几行 0 0
,因为数组内容被初始化为 0.
也能通过指定正确类型的数据来初始化:
>>> from ctypes import *
>>> TenIntegers = c_int * 10
>>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> print(ii)
>>> for i in ii: print(i, end=" ")
...
1 2 3 4 5 6 7 8 9 10
>>>
可以将 ctypes 类型数据传入 pointer() 函数创建指针:
>>> from ctypes import *
>>> i = c_int(42)
>>> pi = pointer(i)
>>>
指针实例拥有 contents 属性,它返回指针指向的真实对象,如上面的 i
对象:
>>> pi.contents
c_long(42)
>>>
注意 ctypes 并没有 OOR (返回原始对象), 每次访问这个属性时都会构造返回一个新的相同对象:
>>> pi.contents is i
False
>>> pi.contents is pi.contents
False
>>>
将这个指针的 contents 属性赋值为另一个 c_int 实例将会导致该指针指向该实例的内存地址:
>>> i = c_int(99)
>>> pi.contents = i
>>> pi.contents
c_long(99)
>>>
指针对象也可以通过整数下标进行访问:
>>> pi[0]
99
>>>
通过整数下标赋值可以改变指针所指向的真实内容:
>>> print(i)
c_long(99)
>>> pi[0] = 22
>>> print(i)
c_long(22)
>>>
使用 0 以外的索引也是合法的,但是你必须确保知道自己为什么这么做,就像 C 语言中: 你可以访问或者修改任意内存内容。 通常只会在函数接收指针是才会使用这种特性,而且你 知道 这个指针指向的是一个数组而不是单个值。
内部细节, pointer() 函数不只是创建了一个指针实例,它首先创建了一个指针 类型 。这是通过调用 POINTER() 函数实现的,它接收 ctypes 类型为参数,返回一个新的类型:
>>> PI = POINTER(c_int)
>>> PI
>>> PI(42)
Traceback (most recent call last):
File "
", line 1, in TypeError: expected c_long instead of int
>>> PI(c_int(42))
>>>
无参调用指针类型可以创建一个 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 具有严格的类型检查。这代表着, 如果在函数
名称栏目:创新互联Python教程:ctypes —- Python 的外部函数库
攀枝花网站建设、攀枝花网站运维推广公司-贝锐智能,是专注品牌与效果的网络营销公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源:
贝锐智能
argtypes
链接地址:http://www.mswzjz.cn/qtweb/news26/46776.html