【高级教程】ctypes:从python菜鸟到c大神

众所周知,相比c++/c,python代码的简洁易懂是建立在一定的速度损耗之上。如果既要代码pythonic又要代码高效,或者比较直接地与系统打交道,那么,ctypes非常值得一探。

目录

1、初识ctypes

2、Hello,CALLING

2.1动态链接库(DLL)

2.2函数(FUNCTION)

2.2.1 A还是W

2.2.2 语法通不过却可以很6地跑

2.2.3 DLL函数索引

2.3 RUNNING,Functions

探讨:cdll? windll?

对比1:CDLL、OleDLL、WinDLL、PyDLL

对比2:cdll、windll、oledll、pydll

拓展:_FuncPtr

3、ctypes基本数据类型

3.1 ctypes数据类型

 3.2 创建ctypes数据类型对象

探讨:为什么不是c_init(0)?

3.3 更新ctypes数据类型对象

4、函数调用与ctypes参数

4.1 ctypes参数

4.2 自定义参数

4.3 函数原型

拓展:从原型创建函数

4.4 返回类型

4.5 指针参数与引用参数

5、ctypes高级数据类型

5.1 structure(结构)与union(联合)

5.2 structure/union内存对齐与字节次序

5.3 位字段

5.4 array

5.5 pointer

5.6 类型转换

5.7 不完全类型

5.8 回调函数

5.9 DLL导出变量

5.10彩蛋

5.10.1 交错的内部关系

5.10.2 是我非我

6、可变数据类型


1、初识ctypes

在wlanapi.h中,有这样一个声明:

DWORD WlanQueryInterface(
          HANDLE                hClientHandle,
          const GUID              *pInterfaceGuid,
          WLAN_INTF_OPCODE    OpCode,
          PVOID                  pReserved,
          PDWORD                pdwDataSize,
          PVOID                  *ppData,
          PWLAN_OPCODE_VALUE_TYPE pWlanOpcodeValueType
)

正如大家所知,python有自己的数据类型,所以即便有DLL入口也无法在代码中直接调用WlanQueryInterface,这个时候就要用到ctypes,以pywifi源码为例:

def __wlan_query_interface(self, handle, iface_guid, opcode, data_size, data, opcode_value_type):
        func = native_wifi.WlanQueryInterface
        func.argtypes = [HANDLE, POINTER(GUID), DWORD, c_void_p, POINTER(DWORD), POINTER(POINTER(DWORD)), POINTER(DWORD)]
        func.restypes = [DWORD]
        return func(handle, iface_guid, opcode, None, data_size, data, opcode_value_type)

def status(self, obj):
        """Get the wifi interface status."""
        data_size = DWORD()
        data = PDWORD()
        opcode_value_type = DWORD()
        self.__wlan_query_interface(self._handle, obj['guid'], 6, byref(data_size), byref(data), byref(opcode_value_type))
        return status_dict[data.contents.value]

不管怎样,pywifi提供的无线网卡查询方法(status)极大地弱化原始API(WlanQueryInterface)的查询能力,虽然只要一个简单的xxx.status(obj)就可以启动查询。

什么是ctypes?ctypes是python的一个外部函数库,提供c兼容的数据类型及调用DLL或共享库函数的入口,可用于对DLL/共享库函数的封装,封装之后就可以用“纯python”的形式调用这些函数(如上面的status)。

2、Hello,CALLING

2.1动态链接库(DLL)

动态链接库是一个已编译好、程序运行时可直接导入并使用的数据-函数库。动态链接库必须先载入,为此ctypes提供三个对象:cdll、windll(windows-only)、oledll(windows-only),并使得载入dll就如访问这些对象的属性一样

cdll:cdll对象载入使用标准cdecl调用约定的函数库。

windll:windll对象载入使用stdcall调用约定的函数库。

oledll:oledll对象载入使用stdcall调用约定的函数库,但它会假定这些函数返回一个windows系统HRESULT错误代码(函数调用失败时自动抛出OSError/WindowsError异常)。

msvcrt.dllkernel32.dll为例介绍dll的载入。

msvcrt.dll:包含使用cdecl调用约定声明的MS标准c函数库,通过cdll载入。

kernel32.dll:包含使用stdcall调用约定声明的windows内核级函数库,通过windll载入。

>>> from ctypes import *
>>> cdll.msvcrt
<CDLL 'msvcrt', handle 7ffbf6930000 at 0x183d91aeac8>
>>> windll.kernel32
<WinDLL 'kernel32', handle 7ffbf6720000 at 0x183d921ae80>
>>>

windows会自动添加“.dll”为文件后缀。通过cdll.msvcrt访问的标准c函数库可能使用一个过时的版本,该版本与python正在使用的函数版本不兼容。所以,尽可能地使用python自身功能特性,或者用import导入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/共享库(本机windows,以user32.dll为例):

>>> from ctypes.util import find_library
>>> from ctypes import *
>>> find_library('user32')
'C:\\Windows\\system32\\user32.dll'
>>> cdll.LoadLibrary('C:\\Windows\\system32\\user32.dll')
<CDLL 'C:\Windows\system32\user32.dll', handle 7ffa00110000 at 0x23eaf6eeb70>
>>>

对于用ctypes封装的共享库而言一个更好的习惯是运行时避免使用find_library()定位共享库,而是在开发时确定好库名并固化(hardcode)到库中。

2.2函数(FUNCTION)

如何获取DLL/共享库中的函数?

很简单:还是像访问一个类实例(这里是DLL对象)属性一样来载入。

所访问的函数都将作为dll载入对象的属性。

>>> from ctypes import *
>>> libc=cdll.msvcrt
>>> libc.printf
<_FuncPtr object at 0x00000183D91DFA08>
>>> help(libc.printf)
Help on _FuncPtr in module ctypes object:
printf = class _FuncPtr(_ctypes.PyCFuncPtr)
 |  Function Pointer 
 |  Method resolution order:
 |      _FuncPtr
 |      _ctypes.PyCFuncPtr
 |      _ctypes._CData
 |      builtins.object
 |  __call__(self, /, *args, **kwargs)
>>> windll.kernel32.GetModuleHandleA
<_FuncPtr object at 0x00000183D91DFAD8>
>>> windll.kernel32.MyOwnFunction
AttributeError: function 'MyOwnFunction' not found
>>>

2.2.1 A还是W

操作字符串的API在声明时会指定字符集。像kernel32.dll和user32.dll这样的win32 dll通常会导出同一个函数的ANSI版本(函数名末尾有一个A,如GetModuleHandA)和UNICODE版本(函数名末尾有一个W,如GetModuleHandW)。

/* ANSI version */
HMODULE GetModuleHandleA(LPCSTR lpModuleName);
/* UNICODE version */
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);

这是win32 API函数GetModuleHandle在c语言中的原型,它根据给定模块名返回一个模块句柄,并根据宏UNICODE是否定义决定GetModuleHandle代表此二版本中的哪一个。

windll不会试着基于魔法去确定GetModuleHandle的实际版本,就像很多事不能无中生有凭空想象,必须显式地指定所访问的是GetModuleHandleA还是GetModuleHandleW,然后用bytes或string对象调用。

2.2.2 语法通不过却可以很6地跑

有时候,从DLL导出的函数名是非法的python标识符(如??2@YAPAXI@Z),这个时候就得用getattr()来获取该函数:

>>> cdll.msvcrt.??0__non_rtti_object@@QEAA@AEBV0@@Z
SyntaxError: invalid syntax
>>> getattr(cdll.msvcrt, "??0__non_rtti_object@@QEAA@AEBV0@@Z")
<_FuncPtr object at 0x00000183D91DFBA8>
>>>

2.2.3 DLL函数索引

windows中,一些DLL不是通过名称而是通过次序导出函数,对于这些函数就可以通过索引(数字索引或名称索引)DLL对象来访问:

>>> windll.kernel32[1]
<_FuncPtr object at 0x000002FD3AAD0AD8>
>>> windll.kernel32[0]
AttributeError: function ordinal 0 not found
>>> windll.kernel32['GetModuleHandleA']
<_FuncPtr object at 0x000002FD3AAD0A08>

2.3 RUNNING,Functions

python中callable对象是怎么调用的,DLL函数就可以怎么调用。

下面以time()、GetModuleHandleA()为例来说明如何调用DLL函数。

>>> libc=cdll.msvcrt
>>> libc.time(None)
1591282222
>>> hex(windll.kernel32.GetModuleHandleA(None))
'0x1c700000'
>>>

如果函数调用之后ctypes检测到传递给函数的参数不合要求则抛出ValueError异常。这种行为是不可靠的,python3.6.2中就被反对使用,而在python3.7已经被移除。

探讨:cdll? windll?

理论上,当一个导出声明为stdcall的函数使用cdecl调用约定时会抛出ValueError异常(反之亦然):

>>> cdll.kernel32.GetModuleHandleA(None) 
ValueError: Procedure probably called with not enough arguments (4 bytes missing)
>>>
>>> windll.msvcrt.printf(b"spam") 
ValueError: Procedure probably called with too many arguments (4 bytes in excess)

上面是来自python官方文档的例子,本机(python 3.6.5 shell)实际操作如下:

>>> from ctypes import *
>>> cdll.kernel32.GetModuleHandleA(None)
477102080
>>> windll.kernel32.GetModuleHandleA(None)
477102080
>>> hex(cdll.kernel32.GetModuleHandleA(None))
'0x1c700000'
>>> windll.msvcrt.printf(b'spam')
4
>>>

为什么实际操作时两种调用约定都可以被cdll和windll使用?

直接查看ctypes源码(已去掉无关内容):

class CDLL(object):
    """An instance of this class represents a loaded dll/shared
    library, exporting functions using the standard C calling
    convention (named 'cdecl' on Windows).

    The exported functions can be accessed as attributes, or by
    indexing with the function name.  Examples:

    <obj>.qsort -> callable object
    <obj>['qsort'] -> callable object

    Calling the functions releases the Python GIL during the call and
    reacquires it afterwards.
    """

    _func_flags_ = _FUNCFLAG_CDECL
    _func_restype_ = c_int
    # default values for repr
    _name = '<uninitialized>'
    _handle = 0
    _FuncPtr = None

    def __init__(self, name, mode=DEFAULT_MODE, handle=None,
                 use_errno=False,
                 use_last_error=False):
        self._name = name
        flags = self._func_flags_
        if use_errno:
            flags |= _FUNCFLAG_USE_ERRNO
        if use_last_error:
            flags |= _FUNCFLAG_USE_LASTERROR

        class _FuncPtr(_CFuncPtr):
            _flags_ = flags
            _restype_ = self._func_restype_
        self._FuncPtr = _FuncPtr

        if handle is None:
            self._handle = _dlopen(self._name, mode)
        else:
            self._handle = handle

    def __repr__(self):
        return "<%s '%s', handle %x at %#x>" % \
               (self.__class__.__name__, self._name,
                (self._handle & (_sys.maxsize*2 + 1)),
                id(self) & (_sys.maxsize*2 + 1))

    def __getattr__(self, name):
        if name.startswith('__') and name.endswith('__'):
            raise AttributeError(name)
        func = self.__getitem__(name)
        setattr(self, name, func)
        return func

    def __getitem__(self, name_or_ordinal):
        func = self._FuncPtr((name_or_ordinal, self))
        if not isinstance(name_or_ordinal, int):
            func.__name__ = name_or_ordinal
        return func

class PyDLL(CDLL):
    """This class represents the Python library itself.  It allows
    accessing Python API functions.  The GIL is not released, and
    Python exceptions are handled correctly.
    """
    _func_flags_ = _FUNCFLAG_CDECL | _FUNCFLAG_PYTHONAPI

if _os.name == "nt":
    class WinDLL(CDLL):
        """This class represents a dll exporting functions using the
        Windows stdcall calling convention.
        """
        _func_flags_ = _FUNCFLAG_STDCALL

    class OleDLL(CDLL):
        """This class represents a dll exporting functions using the
        Windows stdcall calling convention, and returning HRESULT.
        HRESULT error values are automatically raised as OSError
        exceptions.
        """
        _func_flags_ = _FUNCFLAG_STDCALL
        _func_restype_ = HRESULT

class LibraryLoader(object):
    def __init__(self, dlltype):
        self._dlltype = dlltype

    def __getattr__(self, name):
        if name[0] == '_':
            raise AttributeError(name)
        dll = self._dlltype(name)
        setattr(self, name, dll)
        return dll

    def __getitem__(self, name):
        return getattr(self, name)

    def LoadLibrary(self, name):
        return self._dlltype(name)

cdll = LibraryLoader(CDLL)
pydll = LibraryLoader(PyDLL)

if _os.name == "nt":
    windll = LibraryLoader(WinDLL)
    oledll = LibraryLoader(OleDLL)

    if _os.name == "nt":
        GetLastError = windll.kernel32.GetLastError
    else:
        GetLastError = windll.coredll.GetLastError
    from _ctypes import get_last_error, set_last_error

    def WinError(code=None, descr=None):
        if code is None:
            code = GetLastError()
        if descr is None:
            descr = FormatError(code).strip()
        return OSError(None, descr, None, code)

分析上面源码可知,ctypes提供CDLL、PyDLL、 WinDLL、OleDLL四种类型的DLL对象,后三者是CDLL的子类,前二者是通用DLL,后二者专为windows系统定义。此四者主要区别在于_func_flags_的取值:

CDLL

WinDLL

_FUNCFLAG_CDECL

_FUNCFLAG_STDCALL

OleDLL

PyDLL

_FUNCFLAG_STDCALL

_FUNCFLAG_CDECL |

_FUNCFLAG_PYTHONAPI

三个子类的方法与属性都继承自CDLL,其中OleDLL还有一个例外的_func_restype_属性。

此外,ctypes提供cdll、windll、pydll、oledll四个LibraryLoader对象用于实际完成dll的载入。

>>> windll=LibraryLoader(WinDLL)
>>> windll.kernel32

因为windll.__dict__不存在名称’kernel32’,所以最终将调用LibraryLoader中__getattr__,开始实际上的WinDLL(‘kernell32’)实例化(会用到CDLL中的__init__,载入模块、获取模块句柄),实例对象加入windll的__dict__后被返回;windll.LoadLibrary(‘kernel32’)作用类似(返回新DLL对象);支持名称索引。

>>> windll.kernel32.GetModuleHandleA

windll.kernel32将返回一个WinDLL(‘kernell32’)对象,接着会调用CDLL中

__getattr__,__getitem__来获取GetModuleHandleA 的_FuncPtr对象,通过该对象调用函数。若函数载入方式只有windll.kernel32['GetModuleHandleA'],GetModuleHandleA将不被加入WinDLL(‘kernell32’)对象的__dict__(因为有__getattr__,在使用时感觉不到属性载入和名称索引载入的区别)。

仅基于以上ctypes源码分析还看不到windll和cdll在载入dll及相关函数时的本质差异,而两个关键之处_dlopen、_CFuncPtr来自_ctypes.pyd:

from _ctypes import LoadLibrary as _dlopen
from _ctypes import CFuncPtr as _CFuncPtr

所以_func_flags_是如何发挥作用并未得知。如果哪位大神已知晓为什么能混合调用,还望多多指教。

无论怎样,虽然两种方式都可以,但为避免不必要的潜在风险还是请遵循python官方文档的使用指导。

而想要知道一个函数的正确调用约定,就得从相关c头文件或文档中找出函数声明。

windows中,ctypes使用WIN32结构化的异常处理来防止以错误参数调用函数时产生的程序崩溃(如一般性保护故障):

>>> windll.kernel32.GetModuleHandleA(32)
OSError: exception: access violation reading 0x0000000000000020
>>> getattr(cdll.msvcrt, "??0__non_rtti_object@@QEAA@AEBV0@@Z")(123)
OSError: exception: access violation writing 0x000000000000008B
>>>

这里有足够多的方式通过ctypes击溃python,所以无论如何要非常小心。faulthandler模块对于调试“python事故”(比如错误的c库函数调用产生的段故障)非常有帮助。

对比1:CDLL、OleDLL、WinDLL、PyDLL

class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
DLL类型:cdecl调用约定
返回值类型:int

class ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
DLL类型:stdcall调用约定
返回值类型:HRESULT(指示函数调用失败时,已自动抛出异常)

class ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
DLL类型:stdcall调用约定
返回值类型:int
以上DLL导出函数在调用前释放GIL,调用结束后请求GIL。

class ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)
DLL类型:cdecl调用约定
返回值类型:int
PyDLL导出函数调用前无需释放GIL,且在调用结束后执行python错误标记检查,如有错误则抛出异常。PyDLL对直接调用python C API函数非常有用。

以上所有类都可以用至少带一个参数(此时为DLL/共享库路径名)的自身工厂函数来实例化。如果已经获取DLL/共享库句柄,则可以作为参数传给handle,否则将会用到底层平台dlopen或LoadLibrary函数将DLL/共享库载入进程,并取得相应句柄。

mode参数指定如何载入DLL/共享库,详情请参考dlopen(3)手册页。windows中mode参数被忽略。posix系统中,mode总要被加入RTLD_NOW,且不可配置。常用于mode的标志有:

ctypes.RTLD_GLOBAL:在该标志不可用的平台上其值被定义为0。

ctypes.RTLD_LOCAL:在该标志不可用的平台上其值同RTLD_GLOBAL。

ctypes.DEFAULT_MODE:默认的mode,用于载入DLL/共享库。在OS X 10.3该标志同RTLD_GLOBAL,其他平台同RTLD_LOCAL。

use_errno参数被置为True时,ctypes机制将以一种安全的方式访问系统errno错误代码。ctypes维持一份系统变量errno的本地线程副本。如果调用创建自带use_errno=True的DLL外部函数,ctypes会在函数调用前以自身副本errno和系统errno交换,而在调用之后又立即交换回来。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值