Python 源码探索(十)

it2024-11-28  13

探索 Python

前言_ctypes 初相识

1、前言

Hello    相信通过接触前面的文章,对 python 或多或少也增加了一点认识,大家都一样,学习和进步,工作和生活,都是在不断积累的路上,未来永远都属于心中有光的人。有光的地方也必然存在阴影,希望我们能一起快乐的度过每一分每一秒,坚持不懈的等待和享受属于我们每个人的青春。那么这次就来看看 _ctypes。    和之前一样,我自己对这个库是完全不了解的,甚至不知道它有什么用,但是通过不断的探索,就会解决这些疑问,枯燥的东西谁都不喜欢,这是很正常的,因为人类在有理性的同时,感性也是对半占的,同样想学好 python 的朋友请和我一起坚持下去,也欢迎多多和我互动,我都会及时回复,非常感谢那么多朋友一直以来的支持,谢谢!

2、_ctypes 初相识

_ctypes?类型?    _ctypes 这个模块有三百多行代码,那么作为 python 标准库之一,它又担任着怎样的角色呢,下面一起来看看吧。    首先,了解一个三方库的第一手就是去查阅官方提供的资料,我习惯查阅安装 python 时一起下载的说明文档,可以在 python 的安装目录下找到【Doc】文件夹,如下:    打开【Doc】文件夹就可以看见说明文档:    相信很多朋友都用的 win10,所以也可以用 Windows 的快捷搜索找到,如下:    第一次找的话,会出现几个结果,经常打开的话,就会直接找到,打开说明文档后,在文档左上角找到【索引】选项卡,键入这次的主题【ctypes】,如下:    这一大串的文档,不想看也很正常,下面总结了一些重点,帮助快速了解【ctypes】,一共十点,如下:    ① ctypes 是 python 的一个外部函数库,提供可以 C 兼容的数据类型,并且允许在 DLL 或者共享库中调用函数,DLL 以前也说过,是一种动态链接库,而且可以用来将这些库包装在纯 python 中,也通过将库作为这些对象的属性进行访问来加载它们,调用失败时,自动引发 OSError 异常,但在 3.3 版本中更改了,Windows 错误引发 WindowsError,是 OSError 的别名,以下是导入示例:    这是在 Windows 下的方式,在 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

–    这些基本的导入方法和提示。    ② 错误和崩溃,在Windows上,ctypes 使用 win32 结构化异常处理来防止使用无效参数值调用函数时因一般保护错误而导致崩溃:

>>> windll.kernel32.GetModuleHandleA(32) Traceback (most recent call last): File "<stdin>", line 1, in <module> OSError: exception: access violation reading 0x00000020 >>>

–    当使用 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) >>>

–    ctypes 有很多问题可以导致崩溃,所以使用时应多加小心,其中,Faulthandler 模​​块有助于调试崩溃,WinError 是一个函数,它将调用 Windows FormatMessage() api 以获取错误代码的字符串表示形式,并返回异常,WinError 带有一个可选的错误代码参数,如果没有使用该参数,它将调用 GetLastError() 来检索它。    ③ 数据类型,空值、整数、字节对象和 unicode 字符串是唯一可以直接用作这些函数调用中的参数的本机 python 对象,没有任何内容作为 C NULL 指针传递,字节对象和字符串作为指针传递到包含其数据(char *或 wchar_t *)的存储块,python 整数作为平台默认的 C int 类型传递,其值被屏蔽了以适应 C 类型,在继续使用其他参数类型调用函数之前,有必要了解有关 ctypes 数据类型的更多信息。    基本数据类型:ctypes 定义了许多与 C 兼容的原初数据类型,ctypes 类型、C 型和 python 类型,如下

ctypes 类型C 型python 类型c_bool_Boolbool (1)c_charchar1-character bytes object(1个字符的字节对象)c_wcharwchar_t1-character string(1个字符的字符串)c_bytecharintc_ubyteunsigned char(无符号查尔类型)intc_shortshortintc_ushortunsigned shortintc_intintintc_uintunsigned intintc_longlongintc_ulongunsigned longintc_longlong__int64 or long long(长整型)intc_ulonglongunsigned __int64 or unsigned long longintc_size_tsize_tintc_ssize_tssize_t or Py_ssize_tintc_floatfloatfloatc_doubledoublefloatc_longdoublelong doublefloatc_char_pchar * (NUL terminated)bytes object(字节对象) or Nonec_wchar_pwchar_t * (NUL terminated)string or None(空值)c_void_pvoid *int or 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) # 内存位置已更改 c_wchar_p(139966783348904) >>> print(c_s.value) Hi, there >>> print(s) # 第一个对象不变 Hello, World >>>

–    但是应该注意,不要将它们传递给期望指向可变内存的指针的函数,如果需要可变的内存块,ctypes 具有一个 create_string_buffer() 函数,该函数可以通过各种方式创建它们,也可以使用 raw 属性访问(或更改)当前存储块的内容,如果要以 NUL 终止的字符串访问它,可以使用 value 属性:

>>> from ctypes import * >>> p = create_string_buffer(3) # 创建一个3字节的缓冲区,初始化为NUL字节 >>> print(sizeof(p), repr(p.raw)) 3 b'\x00\x00\x00' >>> p = create_string_buffer(b"Hello") # 创建一个包含NUL终止字符串的缓冲区 >>> print(sizeof(p), repr(p.raw)) 6 b'Hello\x00' >>> print(repr(p.value)) b'Hello' >>> p = create_string_buffer(b"Hello", 10) # 创建一个10字节的缓冲区 >>> 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' >>>

–    ④ 传递指针(通过引用传递参数),有时 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 模块中定义的 Structure 和 Union 基类派生,每个子类必须定义一个 fields 属性,fields 必须是二元组的列表,其中包含字段名称和字段类型,字段类型必须是 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 # 类型错误:初始化程序过多 >>>

–    ⑥ 指针,指针实例是通过在 ctypes 类型上调用 pointer() 函数创建的:

>>> from ctypes import * >>> i = c_int(42) >>> pi = pointer(i) >>>

–    指针实例具有 content 属性,该属性返回指针所指向的对象,即上面的 i 对象:

>>> pi.contents c_long(42) >>>

–    指针实例也可以用整数索引:

>>> pi[0] 99 >>>

–    ⑦ 类型转换,通常,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 type 字段设置为 NULL,可以指定 None:

>>> bar.values = None >>>

–    对于这些情况,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 >>>

–    ⑧ 回调功能,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()。然后将使用两个指向项目的指针来调用该回调,如果第一个项目小于第二个项目,则它必须返回一个负整数,如果相等则返回零,否则返回一个正整数,因此,我们的回调函数接收指向整数的指针,并且必须返回一个整数,首先,我们为回调函数创建类型:

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

–    ⑨ 查找共享库,使用编译语言编程时,在编译/链接程序以及运行程序时将访问共享库,find_library() 函数的目的是按照类似于编译器或运行时加载程序的方式来定位库(在具有多个版本的共享库的平台上,应加载最新的库),而 ctypes 库加载器的行为类似于当程序运行时,直接调用运行时加载程序,ctypes.util 模块提供了一个功能,可以帮助确定要加载的库,ctypes.util.find_library(name) 尝试查找库并返回路径名,name 是没有任何前缀,比如 lib,后缀(如 .so,.dylib 或版本号)的库名(这是posix链接器选项-l所使用的形式),如果找不到库,则返回 None,确切的功能取决于系统,在 Linux 上,find_library() 尝试运行外部程序(/ sbin / ldconfig,gcc,objdump 和 ld)以查找库文件,它返回库文件的文件名,在 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' >>>

–    在 OS X 上,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() 来定位该库。    ⑩ 加载共享库,有几种方法可以将共享库加载到 python 进程中,一种方法是实例化以下类之一:

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

–    此类的实例表示已加载的共享库,这些库中的函数使用标准的 C 调用约定,并假定返回 int。

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

–    仅限 Windows:此类的实例表示已加载的共享库,这些库中的函数使用 stdcall 调用约定,并被假定为返回 Windows 特定的 HRESULT 代码,HRESULT 值包含指定函数调用是成功还是失败的信息,以及其他错误代码,如果返回值表示失败,则会自动引发 OSError。    最后,功能原型,外部函数也可以通过实例化函数原型来创建,函数原型类似于 C 中的函数原型。        感谢 ,感谢 C 友们的支持

最新回复(0)