author:Once Day date: 2023年12月6日
漫漫长路,才刚刚开始…
全系列文档查看:python基础_CSDN博客。
参考文档:
ctypes
是一个 Python 标准库,它提供了和 C 语言库交互的能力。利用 ctypes
,你可以在 Python 中加载动态链接库(DLLs 或在 Unix-like 系统中的 shared objects),并且可以调用这些库中的函数。这使得Python可以使用已经编译好的代码,这通常是为了性能或者重用现有的C代码。
要使用 ctypes
,首先需要导入该模块:
import ctypes
然后,你可以加载一个库,调用其中的函数,传递参数,以及获取返回值。
# 对于 Windows DLL
my_library = ctypes.WinDLL('mylibrary.dll')
# 对于 Unix-like 系统上的 shared object
my_library = ctypes.CDLL('libmylibrary.so')
要调用库中的函数,可以直接通过库对象访问函数,就像访问其属性一样:
result = my_library.my_function(1, 2)
但在调用之前,通常需要指定函数的参数类型和返回类型,以便 ctypes
可以正确地处理数据转换。
# 指定函数的参数类型
my_library.my_function.argtypes = (ctypes.c_int, ctypes.c_int)
# 指定返回类型
my_library.my_function.restype = ctypes.c_int
result = my_library.my_function(1, 2)
ctypes
提供了很多与 C 语言中对应的数据类型:
c_int
, c_short
, c_long
, c_longlong
, 整数类型。c_uint
, c_ushort
, c_ulong
, c_ulonglong
, 无符号整数类型。c_float
, c_double
, 浮点数类型。c_char
, c_wchar
,字符类型。c_void_p
,void 指针类型。ctypes
还允许你定义结构体和联合体,以及创建和操作指针:
# 定义结构体
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_int),
('y', ctypes.c_int)]
# 创建结构体的实例
point = Point(10, 20)
# 传递结构体的指针
my_library.some_function(ctypes.byref(point))
在使用 ctypes
调用外部函数时,错误处理非常重要。库函数可能会根据其定义返回错误代码,或者可能产生异常。你需要根据库的文档和函数声明来妥善处理这些情况。
ctypes
库是 Python 编程语言中用于与 C 语言库交互的一个有力工具。
以下是 ctypes
的一些优点:
ctypes
允许你直接从 Python 代码调用 C 库函数,无需编写包装器或者扩展模块。ctypes
随 Python 标准库提供,因此不需要额外安装。ctypes
在多数平台上工作得很好,包括 Windows、Linux 和 macOS。ctypes
可以很容易地使用。ctypes
可以处理许多不同的数据类型,并且能够很好地处理指针、结构体和联合体。以下是 ctypes
的一些缺点:
ctypes
进行封装可能会涉及大量的工作,特别是要正确处理数据结构和内存管理。ctypes
调用 C 函数比纯 Python 快,但它比使用 C API 编写的原生 Python 扩展模块慢,因为它在运行时必须解析和转换数据类型。ctypes
的使用者负责处理所有内存管理的方面,这可能导致内存泄露和程序崩溃,尤其是如果没有正确地管理引用和生命周期。ctypes
的定义,这可能导致维护成本。ctypes
不会像 CPython 扩展模块那样自动处理 C 代码中的错误,需要手动检查并处理错误。ctypes
是类型不安全的,如果调用者使用了错误的类型,可能会导致不可预知的行为,甚至崩溃。ctypes 适合于轻量级别的编程场景,例如直接拿到未开源的C动态库,或者需要快速编写python脚本测试的场景。在Python本身就可以实现的情况下,应该优先使用Python自身的功能而不要使用操作系统提供的API接口,。
本节以Linux平台作为实验平台,关于Windows平台可以参考官方文档(更加复杂,但原理相差不大)。
ctypes在Linux平台可以导入cdll
对象,在 Windows 系统中则可以导入windll
和oledll
动态链接对象。
cdll
加载使用标准cdecl
调用约定导出函数的库。windll
使用stdcall
调用约定调用函数。oledll
使用stdcall
调用约定,并假定函数返回Windows HRESULT
错误代码。 当函数调用失败时会使用错误代码自动引发OSError
异常。下面加载C标准库,并且操作其中的函数:
>>> from ctypes import * # 导入ctypes模块
>>> libc = CDLL("libc.so.6") # 加载动态库,创建CDLL的实例
>>> libc
<CDLL 'libc.so.6', handle 7fb249b0d3f0 at 0x7fb2492e5ae0>
>>>print(libc.rand()) # 这里输出一个随机数
1804289383
如上面所示,只要导入了动态库,其中导出的变量和函数可以随便使用。
需要注意,如果动态库之间存在依赖,比如下面动态库B依赖A,那么导入动态B之间,需要先导入A,否则会报存在未定义符号的错误。可使用ldd
查看动态库的依赖关系:
ubuntu->python:$ sudo ldd /usr/local/lib/libyang.so
linux-vdso.so.1 (0x00007fff363f6000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f07e201e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f07e1df6000)
/lib64/ld-linux-x86-64.so.2 (0x00007f07e2249000)
当调用一个不存在的函数时,会直接报错,如下:
>>> libc.unknown
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.10/ctypes/__init__.py", line 387, in __getattr__
func = self.__getitem__(name)
File "/usr/lib/python3.10/ctypes/__init__.py", line 392, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /lib/x86_64-linux-gnu/libc.so.6: undefined symbol: unknown
如果遇到C动态库函数内存问题,导致coredump,那么可以使用faulthandler来打印调用栈错误,从而更好定位:
onceday->python:# python3 -q -X faulthandler
>>> import ctypes
>>> ctypes.string_at(0)
Fatal Python error: Segmentation fault
Current thread 0x00007f1f2d53d1c0 (most recent call first):
File "/usr/lib/python3.10/ctypes/__init__.py", line 517 in string_at
File "<stdin>", line 1 in <module>
Segmentation fault (core dumped)
有四类Python对象是可以自动转换为C函数参数,如下:
None
将作为 C NULL
指针传入。Unicode
字符串将作为指向包含其数据 (char*
或 char_t*
) 的内存块的指针传入。下面表格来自ctypes官方文档,ctypes 基础数据类型 :
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_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | __int64 或 long long | int |
c_ulonglong | unsigned __int64 或 unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t或 Py_ssize_ | int |
c_time_t | time_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char* (以 NUL 结尾) | 字节串对象或 None |
c_wchar_p | wchar_t* (以 NUL 结尾) | 字符串或 None |
c_void_p | void* | int 或 None |
这些ctypes对象架起了一道桥梁,沟通Python类型值和C类型值,使用它们非常简单,如下:
(1) 使用合适的值和类型来初始化它们:
>>> c_int()
c_int(0)
>>> c_int(10)
c_int(10)
>>> c_int(9876543210) # 溢出的位被截断
c_int(1286608618)
>>> c_ushort(-1)
c_ushort(65535)
>>> c_ushort(1)
c_ushort(1)
>>> c_char_p("hello!") # c_char_p不能直接接受python字符串(为unicode字符串)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bytes or integer address expected instead of str instance
>>> c_wchar_p("hello!")
c_wchar_p(139723023540784)
>>> c_char_p(b"hello!")
c_char_p(139723023824512)
(2) 创建Ctypes对象后可以更改它们的值:
>>> s = "Hello, World"
>>> c_s = c_wchar_p(s)
>>> print(c_s)
c_wchar_p(139723020918896)
>>> print(c_s.value)
Hello, World
>>> c_s.value = "new value"
>>> print(c_s.value)
new value
>>> print(c_s)
c_wchar_p(139723023824528)
>>> print(s)
Hello, World
对于指针对象,赋值时会改变指向的内存地址,而不是指向内存区域的数据,对于其他对象,则会改变其内存区域的值。
对于直接引用Python字符串或者字节流的ctypes指针类型(c_char_p
, c_wchar_p
和c_void_p
等),不能将它们作为参数传递给会改变指针所指向内存的函数(Python的bytes对象是不可变的)。
这种情况下,需要使用create_string_buffer()
函数,可以创建可变内容的内存块,并通过raw属性获取。示例如下:
>>> p_str = create_string_buffer(8)
>>> print(sizeof(p_str), p_str.raw)
8 b'\x00\x00\x00\x00\x00\x00\x00\x00'
>>> p_str.value = b"hello!" # 通过value可以在创建对象后再赋值
>>> print(sizeof(p_str), p_str.raw)
8 b'hello!\x00\x00'
>>> p_str.value = b"hello world!" # 赋值超过大小,会触发错误
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: byte string too long
如果要创建一个包含 C 类型 wchar_t 的 unicode 字符的可变内存块,可以使用**create_unicode_buffer()**函数。
对于可变参数函数,可以如下直接调用:
>>> libc = CDLL("libc.so.6")
>>> printf = libc.printf
>>> printf(b"hello, %s\n", b"ctypes")
hello, ctypes
14
>>> printf(b"hello, %s%s%s%s%s%s\n", b"c", b"t", b"y", b"p", b"e", b"s")
hello, ctypes
14
虽然大部分平台上通过调用ctypes调用可变参数函数和固定参数函数是一样的,但是针对部分特殊平台,可变函数调用约定有一些特殊。在这些平台上要求为常规、非可变函数参数指定 argtypes 属性:
>>> libc.printf.argtypes = [ctypes.c_char_p]
>>> print(printf.argtypes)
[<class 'ctypes.c_char_p'>]
指定该属性不会影响可移植性,所以建议总是为所有可变函数指定 argtypes。
这里使用printf来作为例子,尽管它是一个可变参数函数,这里主要说明如何自动转换参数为合适的Ctypes类型。
(1) 可以通过自定义 ctypes 参数转换方式来允许将你自己的类实例作为函数参数。
ctypes 会寻找 _as_parameter_
属性并使用它作为函数参数。 属性必须是整数、字符串、字节串、ctypes 实例或者带有 _as_parameter_
属性的对象:
>>> class Book:
... def __init__(self, name, pages):
... self._as_parameter_ = c_char_p(bytes(f'{name}-{pages}', "ascii"))
...
>>> book = Book("New book", 66)
>>> book
<__main__.Book object at 0x7f3bb64d8790>
>>> libc = CDLL("libc.so.6")
>>> printf = libc.printf
>>> printf(b"Book: %s.\n", book)
如果你不想将实例数据存储在 _as_parameter_
实例变量中,可以定义一个根据请求提供属性的property
。
(2) 指定选择函数的原型,通过原型参数,在执行C函数时,ctypes将能够实现自动转换功能。
>>> 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
>>> printf(b"%d %d %d", 1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 2: <class 'TypeError'>: wrong type
指定数据类型可以防止不合理的参数传递(就像 C 函数的原型),并且会自动尝试将参数转换为需要的类型:
如果定义了自己的类并将其传递给函数调用,则必须为它们实现 from_param()
类方法才能够在 argtypes
序列中使用它们。 from_param()
类方法将接受传递给函数调用的 Python 对象,它应该进行类型检查或者其他必要的操作以确保这个对象是可接受的,然后返回对象本身、它的 _as_parameter_
属性或在此情况下作为 C 函数参数传入的任何东西。
结果应该是整数、字符串、字节串、ctypes
实例或具有 _as_parameter_
属性的对象。
>>> class Cstring:
... @classmethod
... def from_param(cls, pyobj):
... return c_char_p(bytes(pyobj, "ascii"))
...
>>> printf.argtypes = [Cstring]
>>> printf("Cstring\n")
Cstring
8
默认情况下都会假定函数返回C int
类型。 其他返回类型可通过设置函数对象的restype
属性来指定。
time()
的 C 原型是time_t time(time_t *)
,可以指定该函数的返回类型为c_ulong
,如下:
>>> ctime = libc.time
>>> ctime.restype = c_ulong
>>> ctime.argtypes = [POINTER(c_ulong)]
>>> ctime(None)
1703168837
调用该函数时如果要将 NULL
指针作为第一个参数,可以使用 None
。
可以定义一个python回调函数,其在c函数调用后处理返回的参数,如下:
callable(result, func, arguments)
result
是外部函数返回的结果,由 restype
属性指明。func
是外部函数对象本身,这样就允许重新使用相同的可调用对象来对多个函数进行检查或后续处理。arguments
是一个包含最初传递给函数调用的形参的元组,这样就允许对所用参数的行为进行特别处理。然后将该函数赋值给函数对象的errcheck
属性,如下:
>>> def check_error(result, func, args):
... print(result)
... print(func)
... print(args)
... return "Success"
...
>>> ctime.errcheck = check_error
>>> ctime(None)
1703169973
<_FuncPtr object at 0x7f3bb50fe440>
(None,)
'Success'
C函数接口往往会使用大量的指针参数,对于Python来说,指针隐藏在内部实现中,因此需要做一层转换,实现传递参数引用。
可以使用 byref()
函数传递参数引用(类似于指针传递),使用pointer()
函数也能达到同样的效果,只不过pointer()
需要更多步骤,因为它要先构造一个真实指针对象。所以在 Python 代码本身不需要使用这个指针对象的情况下,使用byref()
效率更高。
>>> buf = create_string_buffer(32)
>>> libc.snprintf(byref(buf), 32, b"hello, %s!/n", b"world")
15
>>> print(buf)
<ctypes.c_char_Array_32 object at 0x7f3bb4f5e7c0>
>>> print(buf.value)
b'hello, world!/n'
结构体和联合必须派生自Structure
和Union
基类,这两个基类是在ctypes
模块中定义的。 每个子类都必须定义_fields_
属性。 _fields_
必须是一个 2元组 的列表,其中包含一个字段名称和一个字段类型。
type 字段必须是一个 ctypes 类型,比如 c_int,或者其他 ctypes 类型: 结构体、联合、数组、指针。
>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = [("x", c_int),
... ("y", c_int)]
...
>>> print(POINT.x)
<Field type=c_int, ofs=0, size=4>
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20
>>> point = POINT(y=5)
>>> print(point.x, point.y)
0 5
可以嵌套的构建复杂的结构体,如下:
>>> 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))
带位域的结构体一般不支持以值的方法传递给函数,建议使用指针传递值。ctypes
中的结构体和联合使用的是本地字节序。要使用非本地字节序,可以使用BigEndianStructure
, LittleEndianStructure
, BigEndianUnion
, 和LittleEndianUnion
作为基类。这些类不能包含指针字段。如下所示:
>>> class Int(Structure):
... _fields_ = [("first_16", c_int, 16),
... ("second_16", c_int, 16)]
...
>>> print(Int.first_16)
<Field type=c_int, ofs=0:0, bits=16>
>>> print(Int.second_16)
<Field type=c_int, ofs=0:16, bits=16>
数组是包含多个类型相同元素的集合,在Python中,可以直接使用类型乘以数目来创建数组:
>>> TenPointsArrayType = POINT * 10
>>> print(TenPointsArrayType)
<class '__main__.POINT_Array_10'>
下面是一个关于结构体数组的例子,对于其他类型而言,操作是一样的:
>>> from ctypes import *
>>> class Book(Structure):
... _fields_ = ("name", c_wchar_p), ("pages", c_int)
...
>>> class Books(Structure):
... _fields_ = [("num", c_int),
... ("data", Book * 4)]
...
>>> print(len(Books().data))
4
>>> mybooks = Books(2, (("C book", 100), ("Python book", 88)))
>>> print(mybooks.data[0])
<__main__.Book object at 0x7f3bb4f5e840>
>>> print(mybooks.data[0].name)
C book
>>> print(mybooks.data[1].name)
Python book
>>> print(mybooks.data[2].name)
None
可以将ctypes
类型数据传入pointer()
函数创建指针:
>>> from ctypes import *
>>> n = c_int(1888)
>>> n.value
1888
>>> pn = pointer(n)
>>> pn
<__main__.LP_c_int object at 0x7f3bb4f5e840>
>>> pn.contents
c_int(1888)
ctypes 并没有OOR(返回原始对象), 每次访问这个属性时都会构造返回一个新的相同对象:
>>> pn.contents is pn.contents
False
>>> a = pn.contents
>>> b = pn.contents
>>> id(a)
139894415813184
>>> id(b)
139894415812800
通过对contents
属性进行赋值,可以将该指针指向另外一个ctypes对象的地址。
>>> pn.contents = c_int(1999)
>>> pn.contents
c_int(1999)
也可以通过数组下标的方式访问和改变指针的值:
>>> pn[0]
1999
>>> pn[0] = 2000
>>> pn[0]
2000
这里需要注意,ctypes没有判断该指针指向的范围,因此如果使用超过0的索引,那么可能造成内存覆写,和C指针一样,需要使用者对内存使用进行负责。
解引用指针时,如果是NULL指针,ctypes会检查该错误,并且报出ValueError: NULL pointer access
错误,但是非零无效的指针值,ctypes无法检查,这会造成Python崩溃。
如果在函数的argtypes
列表中有POINTER(c_int)
或在结构体定义中将其用作成员字段的类型,则只接受完全相同类型的实例。 此规则也有一些例外情况,在这些情况下 ctypes 可以接受其他对象。 例如,可以传入兼容的数组实例而不是指针类型。
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])
此外,如果函数参数在argtypes
中明确声明为指针类型 (如POINTER(c_int)
),则可以向函数传递所指向的类型的对象 (在本例中为 c_int
)。 在这种情况下,ctypes
将自动应用所需的byref()
转换。
在ctypes中,可以使用cast
将一个类型强制转换为另一个,即C语言中的强制类型转换功能。
>>> a = (c_char * 8)()
>>> a
<__main__.c_char_Array_8 object at 0x7f3bb4f5eb40>
>>> cast(a, POINTER(c_int))
<__main__.LP_c_int object at 0x7f3bb4f5ea40>
C语言声明时,可以定义不完整类型,即还没有定义成员的结构体、联合或者数组,通常用于前置声明,然后在后面定义。
struct cell; /* forward declaration */
struct cell {
char *name;
struct cell *next;
};
如果Python中也这么直接定义,就会出现报错,显示模块未定义:
>>> 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 3, in cell
NameError: name 'cell' is not defined. Did you mean: 'cdll'?
因此,在Python中,需要先定义Classs,再指定类的_fields_
属性:
from ctypes import *
class cell(Structure):
pass
cell._fields_ = [("name", c_char_p),
("next", POINTER(cell))]
对于Python而言,还需要构建能在C函数中被调用的Python函数,当然,无法直接调用,而是由ctypes和ffi构建C到Python的转换层来实现。
ctypes
允许创建一个指向 Python 可调用对象的C函数。它们有时候被称为回调函数。
首先,需要为回调函数创建一个类,这个类知道调用约定,包括返回值类型以及函数接收的参数类型及个数:
CFUNCTYPE()
工厂函数使用 cdecl 调用约定创建回调函数类型。WINFUNCTYPE()
工厂函数使用stdcall
调用约定为回调函数创建类型。CFUNCTYPE()
和WINFUNCTYPE()
函数的第一个参数是返回值类型,回调函数的参数类型作为剩余参数。
下面这个例子是官方文档的经典例子,即使用qsort()
函数:
IntArray5 = c_int * 5
ia = IntArray5(5, 1, 7, 33, 99)
qsort = libc.qsort
qsort.restype = None
qsort()
被调用时必须传入一个指向要排序的数据的指针、数据数组中的条目数、每条目的大小以及一个指向比较函数即回调函数的指针。回调函数将附带两个指向条目的指针进行调用,如果第一个条目小于第二个条目则它必须返回一个负整数,如果两者相等则返回零,在其他情况下则返回一个正整数。
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 a[0] - b[0]
cmp_func = CMPFUNC(py_cmp_func)
执行结果如下:
>>> 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
特别注意,需要维持CFUNCTYPE()
对象的引用周期与它们在 C 代码中的使用期一样长。ctypes
不会确保这一点,如果不这样做,它们可能会被垃圾回收,导致程序在执行回调函数时发生崩溃。
因此,推荐使用装饰器语法来实现函数转换,这样可以自动保持CFUNCTYPE
对象生命周期:
@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]
如果回调函数在Python之外的另外一个线程使用(比如,外部代码调用这个回调函数), ctypes 会在每一次调用上创建一个虚拟 Python线程。这个行为在大多数情况下是合理的,但也意味着如果有数据使用 threading.local
方式存储,将无法访问,就算它们是在同一个C线程中调用的。
某些共享库不仅会导出函数,还会导出变量,ctypes也提供了接口用于读取动态库中的全局变量,即通过类型的 in_dll()
类方法访问这样的值。
in_dll(library, name)
此方法返回一个由共享库导出的ctypes
类型。name
为导出数据的符号名称,library
为所加载的共享库。
下面以libc库中的程序名字全局变量来示例,即extern char *program_invocation_short_name*
,基于argv[0]
的值。
>>> name = c_char_p.in_dll(libc, "program_invocation_short_name")
>>> name
c_char_p(140736709220096)
>>> print(name.value)
b'python3'