【高級教程】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交換,而在調用之後又立即交換回來。

ctypes.get_errno()函數返回ctypes私有副本errno的值,ctypes_set_errno()函數設置ctypes私有副本errno值並返回設置前的值。

use_last_error參數被置爲True時,將使Windows API GetLastError()和SetLastError()管理的windows錯誤代碼有着相同機制。ctypes.get_last_error()和ctypes.set_last_error()用於獲取和改變ctypes的windows錯誤代碼私有副本。

這些類實例沒有公共方法。DLL/共享庫導出的函數既可以作爲屬性訪問也可以通過索引來訪問。但要注意通過屬性訪問函數會緩存訪問結果,因此重複訪問時每次都返回相同對象。而通過索引訪問每次會返回新的對象。

>>> libc=cdll.msvcrt
>>> libc.time==libc.time
True
>>> libc['time']==libc['time']
False
>>>

對比2:cdll、windll、oledll、pydll

ctypes.LibraryLoader(dlltype):這個類用於載入函數庫,dlltype爲DLL類型(CDLL、PyDLL、WinDLL、OleDLL之一),允許以訪問屬性的方式載入函數庫(將會緩存載入結果,重複訪問時返回的函數庫相同),或者通過LoadLibrary(name)方法載入(返回新對象)。注意區別於DLL對象的函數訪問。

ctypes.cdll:創建CDLL實例;

ctypes.windll:創建WinDLL實例;

ctypes.olddll:創建OleDLL實例;

ctypes.pydll:創建PyDLL實例;

ctypes.pythonapi:創建以python C API爲屬性PyDLL實例,用於直接訪問c python api,是一個只讀的python共享函數庫。導出的函數都假定返回c語言int值,但實際上並不總是返回int值,所以得爲這類函數設置正確的restype屬性。

拓展:_FuncPtr

如前所說,外部函數可以作爲已載共享庫的屬性來訪問。函數對象創建時默認接受任意數量參數(任意ctypes數據類型實例),並返回dll載入對象(如cdll、windll等)指定的默認類型返回值。函數對象其實是一個私有類的實例:

class ctypes._FuncPtr,用(服務)於c外部可調用函數的基類。

函數對象實例也是c兼容數據類型,代表c函數指針。這種(代表)行爲(函數指針的實際定義)可通過賦予外部函數對象特殊屬性來自行定義。

restype:設置函數返回值類型。如果函數無返回值則爲None。

可以爲restype設置一個非ctypes類型的python可調用對象,但這種情況下的外部函數必須是被假定爲返回c語言int型值的,且該值被傳遞給python可調用對象作進一步處理或錯誤檢測。這種用法是不被提倡的,以ctypes數據類型爲restype並設置errcheck屬性爲一個可調用對象的後置處理或錯誤檢測要合適得多。

argtypes:一個指定外部函數參數類型的ctypes類型元組(也可以是列表)。對於stdcall調用約定的函數,只有當函數參數的個數、類型與元組元素一一對應時才能被調用,而對於cdecl調用約定的函數則還可以接受額外的、(元組中)未指定的參數。

在調用外部函數時,實際參數傳給argtypes元組中各類型的from_param()類方法,該方法將實際參數調配成函數能接受的對象。例如,argtypes元組中的c_char_p能使用ctypes轉換規則將傳遞來的字符串參數轉換成字節串對象。

新特性:現在已經可以把項目放入非ctypes類型的argtypes(即argtypes的元素不再僅限於ctypes類型),但是各項必須提供from_param()方法返回一個值(整數實例、字符串實例、ctypes實例等)作爲調配後的參數。這種特性允許定義一個將自定義對象轉換成能適應函數參數類型的“調配器”。

一個外部函數在調用時若無法轉換任意一個傳遞給它的參數將拋出ctypes.ArgumentsError異常。

errcheck:該屬性只能賦值python函數或其他可調用對象。可調用對象在被調用時帶有三個或更多參數:callable(result,func,arguments)。

result是外部函數返回值,由restype屬性指定。

func是外部函數對象自身,這就允許再次使用同一可調用對象來對若干函數返回結果進行錯誤檢測或後續處理。

arguments是傳遞給函數調用的原始參數元組,這可以定製參數的使用。

errcheck的返回結果將從外部函數調用返回,但它也能檢查(外部函數)返回值,如果函數調用失敗還能拋出異常。

以上屬性只對ctypes._FuncPtr實例(即DLL外部函數對象)有效。

3、ctypes基本數據類型

在調用DLL函數時,None、int、bytes、str是python中僅有的可直接作爲函數參數的原生對象。當函數參數爲NULL指針時,可以傳遞None爲參數;當函數參數是一個包含數據的內存塊指針(char*或wchar_t*)時,可以傳遞bytes或str爲參數;當函數參數爲系統平臺默認int類型時,可以傳遞int爲參數(爲適應c數據類型其值將被部分屏蔽)。

>>> import sys
>>> sys.maxsize
9223372036854775807

那麼,對於其他類型參數,應該怎樣傳遞?

3.1 ctypes數據類型

ctypes定義了大量原始的c兼容數據類型,以滿足函數調用時的各種參數傳遞需要。

c基本數據類型

類型

32位平臺(B

64位平臺(B

char

1

1

short

2

2

int

4

4

long

4

 (≥4) 8

long long

8

8

float

4

4

double

8

8

size_t

4

8

ssize_t

4

8

ctypes基本數據類型與對應關係

ctypes type

C type

Python type

c_bool

_Bool

bool (1)

c_char

char                       

1-character bytes object

c_wchar

wchar_t                    

1-character str

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

int

c_ulonglong

unsigned __int64 or

unsigned long long

int

c_size_t

size_t

int

c_ssize_t

ssize_t or Py_ssize_t

int

c_float

float

float

c_double

double

float

c_longdouble

long double

float

c_char_p

char * (NUL terminated)

bytes or None

c_wchar_p

wchar_t * (NUL terminated)

str or None

c_void_p

void *

int or None

 3.2 創建ctypes數據類型對象

ctypes type這一列的類型名都可以作爲工廠函數,通過接受一個正確的可選“類型-值”來創建ctypes對象:

>>> from ctypes import *
>>> c_int()
c_long(0)
>>> c_wchar_p("Hello,World")
c_wchar_p(2731200093040)
>>> c_ushort(-3)
c_ushort(65533)
>>> c_float(6)
c_float(6.0)
>>> c_float('a')
TypeError: must be real number, not str

ctypes對象的初始化值必須與Python type對應,而決定要用到什麼類型的ctypes對象,則根據C type而定。

探討:爲什麼不是c_init(0)?

>>> c_int()
c_long(0)

爲什麼不是c_int(0)?不妨找一找蛛絲馬跡:

>>> import sys
>>> sys.version
'3.6.5 |Anaconda, Inc.| (default, Mar 29 2018, 13:32:41) [MSC v.1900 64 bit (AMD64)]'
>>> sys.platform
'win32'
>>> sys.maxsize
9223372036854775807
>>> 2**63-1
9223372036854775807
>>> sys.maxsize.bit_length()
63
>>> import struct
>>> bit=struct.calcsize('P')*8
>>> bit
64
>>> import platform
>>> platform.architecture()
('64bit', 'WindowsPE')
>>> import ctypes
>>> c_long==c_int
True
>>>

或者,更直接些(c代碼):

#include <stdio.h>
#include<stdlib.h>
int main()
{
      printf("size of int %d,sizeof long %d\n", sizeof(int), sizeof(long));
      system("pause");
}
size of int 4,sizeof long 4
請按任意鍵繼續. . .

ctypes源碼(片段):

if _calcsize("i") == _calcsize("l"):
    # if int and long have the same size, make c_int an alias for c_long
    c_int = c_long
    c_uint = c_ulong
else:
    class c_int(_SimpleCData):
        _type_ = "i"
    _check_size(c_int)
    class c_uint(_SimpleCData):
        _type_ = "I"
    _check_size(c_uint)

通過多種方式得知(這裏要感謝網絡上各位大神提供平臺查詢方法參考),CPU、windows系統和python都是64位,應用程序平臺win32,然而之所以“c_long==c_int”結果爲True,主要還是因爲無論windows系統是32位還是64位,sizeof(int)=sizeof(long)=4,這是一種編譯器在具體定義int和long時的內在規定(這裏要感謝羣中大神指點)。“long與int:標準只規定long不小於int的長度,int不小於short的長度”(《C和指針》)。

總之,c_int是c_long的別名,它們實際上是同一類型。

3.3 更新ctypes數據類型對象

>>> i=c_int(42)
>>> i
c_long(42)
>>> i.value
42
>>> i.value=-99
>>> i
c_long(-99)
>>>

只要改變obj.value,就可以更新對象的值。

對指針類型(如c_char_pc_wchar_pc_void_p)實例賦一個新value,更新的是指針所指向的內存塊而不是內存塊的內容(因爲python的bytes對象是不可變的)。

>>> s='Hello,World'
>>> c_s=c_wchar_p(s)
>>> c_s
c_wchar_p(2126155553624)
>>> c_s.value
'Hello,World'
>>> c_s.value='Hi,there'
>>> c_s
c_wchar_p(2126155553672)
>>> c_s.value
'Hi,there'
>>> s
'Hello,World'
>>>

調用函數時要非常小心,不要把它們傳給需要指針指向可變內存的函數。如果需要一個可寫內存塊,則要用ctypescreate_string_buffer()函數,它將以不同方式創建可變內存塊。

通過raw屬性訪問或修改buffer對象當前內存塊內容。

>>> p=create_string_buffer(3) #創建一個3字節buffer並初始化爲NUL
>>> print(sizeof(p),repr(p.raw))
3 b'\x00\x00\x00'
>>> p=create_string_buffer(b'Hello')#創建包含NUL字符串的buffer
>>> print(sizeof(p),repr(p.raw))
6 b'Hello\x00'

NUL是一個1字節的字符b’\x00’。NUL字符串是末尾有NUL的字符串。

通過value屬性把buffer對象當作NUL字符串訪問。

>>> p.value
b'Hello'

rawvalue的區別:raw所見即所得,value所見即NUL

>>> p=create_string_buffer(b'Hello',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'

 

>>> p=create_string_buffer(b'Hello',10)

>>> print(sizeof(p),repr(p.raw))

10 b'Hello\x00\x00\x00\x00\x00'

>>> p.raw=b'Hi'

>>> print(sizeof(p),repr(p.raw))

10 b'Hillo\x00\x00\x00\x00\x00'

create_string_buffer()函數已經替換掉ctypes早期發行版的c_buffer()(仍可作爲別名使用)和c_string()。若要創建包含unicode字符(國標,一字符二字節,c語言的wchar_t類型)的可變內存塊,請使用create_unicode_buffer()函數。

>>> p=create_unicode_buffer(u'Hello',10)
>>> print(sizeof(p),repr(p.value))
20 'Hello'
>>> p.value=u'123'
>>> print(sizeof(p),repr(p.value))
20 '123'

4、函數調用與ctypes參數

4.1 ctypes參數

函數printf將內容輸出到實際的標準輸出設備,而不是sys.stdout,所以下面這個例子只能運行在控制檯命令提示符下,而不是IDLE或PythonWin:

>>> libc=cdll.msvcrt
>>> printf=libc.printf
>>> printf(b"Hello,%s\n",b"World!")
13
>>> p=c_char_p(b'Hello,%s\n')
>>> p2=c_char_p(b'World!')
>>> printf(p,p2)
13
>>> printf(b'%d bottles of beer\n',42)
19
>>> printf(b'%f bottles of beer\n',42.5)
ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2

在IDLE中執行上述語句無實際輸出,將其保存到文件console.py並在控制檯(Git Bash)下運行:

$ python console.py
ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
Hello,World!
Hello,World!
42 bottles of beer

正如之前所說,所有整數、字符串str、字節串bytes之外的python類型對象都要被對應ctypes封裝才能轉換成所需的c數據類型傳遞給函數(控制檯下python):

>>> printf(b"An int %d, a double %f\n", 1234, c_double(3.14))
An int 1234, a double 3.140000
31
>>> printf(b'%f bottles of beer\n',c_double(42.5))
42.500000 bottles of beer
26

爲什麼一個42.5要用到c_double而不是c_float呢?

>>> printf(b'%f bottles of beer\n',c_float(42.5))
0.000000 bottles of beer
25

這裏給大家留道課後作業,自行探索。

4.2 自定義參數

_as_parameter_定義ctypes參數轉換以允許自定義類型實例作爲函數參數。調用函數的時候,ctypes會查找_as_parameter_屬性並以之爲函數參數。

_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,只有在查詢_as_parameter_的請求中該屬性纔可用。

>>> class Bottles:
...     def __init__(self,number):
...             self.number=number
...     @property
...     def _as_parameter_(self):
...             return self.number
...
>>> bottles=Bottles(42)
>>> printf(b"%d bottles of beer\n", bottles)
42 bottles of beer
19
>>>

4.3 函數原型

這裏的函數原型主要指函數的參數類型。

argtypes:argtypes屬性指定DLL導出函數所要求的參數類型。argtypes必須是一個c數據類型(對應的ctypes)序列(printf可能不是一個介紹函數原型的好例子,因爲它參數數量不固定,並且參數的類型取決於格式化字符串,但是從另一個方面說,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
>>>

argstypes相當於定義一個格式保護來防止不兼容的參數類型(就好像一個c函數的原型),並試着進行兼容類型轉換:

>>> printf(b"%d %d %d", 1, 2, 3)
ctypes.ArgumentError: argument 2: <class '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_屬性,或該情況下任何想要作爲參數傳遞給函數的對象。再次重申,返回結果應該是整數、字符串、字節串、ctypes實例,或者是具有_as_parameter_屬性的對象。

文件console.py:

from ctypes import *
libc=cdll.msvcrt
printf=libc.printf
class Bottles:
    def __init__(self,number):
        self.number=number
    @property
    def _as_parameter_(self):
        return self.number
    @classmethod
    def from_param(cls,obj):
        if obj.__class__!=cls:
            raise AttributeError('Not a Bottles.')
        return obj
bottles=Bottles(42)
printf.argtypes=[c_char_p,c_char_p,Bottles,c_int]
printf(b'Hi,%s,I have %d bottles.%d ?',b'James',bottles,100)

命令行運行:

$ python console.py
Hi,James,I have 42 bottles.100 ?

如果console.py中的Bottles類爲

class Bottles:
    def __init__(self,number):
        self.number=number
    @classmethod
    def from_param(cls,obj):
        if obj.__class__!=cls:
            raise AttributeError('Not a Bottles.')
        return obj.number

也是可以的,輸出結果相同。若是文件最後增加:

printf(b'Hi,%s,I have %d bottles.%d ?',b'James',2,100)

則輸出爲:

ctypes.ArgumentError: argument 3: <class 'AttributeError'>: Not a Bottles.
Hi,James,I have 42 bottles.100 ?

拓展:從原型創建函數

外部函數也可能創建自一個實例化的函數原型。函數原型類似於c語言中的函數原型,不帶執行定義地描述一個函數(返回類型,參數類型,調用約定)。工廠函數必須以外部函數要求的返回類型和參數類型來調用。

ctypes.CFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False):返回的函數原型創建cdecl調用約定函數。函數調用期間將釋放GIL。如果use_errno爲True,ctypes私有的系統errno副本在函數調用前後將和真正的系統errno變量交換值。use_last_error同windows錯誤代碼。

ctypes.WINFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False):返回的函數原型創建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),第一項是字符串(導出函數名),或者是一個小整數(-32768到+32767,導出函數序號),第二項是共享庫實例。

prototype(vtbl_index, name[, paramflags[, iid]]):返回一個調用COM方法的外部函數。vtbl_index是一個非負小整數(虛擬函數表函數索引),name是COM方法名,iid是一個指向擴展錯誤報告中接口標識符的可選指針。

COM方法使用一種特殊調用約定:它要求一個指向COM接口的指針爲第一個參數,除此之外這些參數都由argtypes元組指定。

可選參數paramflags創建的外部函數wrapper比上述特性功用性更強。paramflags必須是一個和argtypes長度相同的元組,元組中每一項都包含更多的參數信息,必須是一個包含1到3項的元組:

第一項是一個包含參數方向標誌組合的整數:1輸入參數;2輸出參數,由外部函數填入一個值;4輸入參數,默認值爲整數0。

第二項可選,是一個字符串(參數名)。如果指定該參數,外部函數可用命名參數調用。

第三項也可選,代表對應參數的默認值。

這個例子展示如何封裝windows的MessageBoxW函數,以讓它支持默認參數和命名參數。它在windows頭文件中聲明如下:

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,'lpText','Hi'),(1,'lpCaption','Hello from ctypes'),(1,'uType',0)
>>> paramflags
((1, 'hWnd', 0), (1, 'lpText', 'Hi'), (1, 'lpCaption', 'Hello from ctypes'), (1, 'uType', 0))
>>> MessageBox=prototype(('MessageBoxW',windll.user32),paramflags)
>>>

現在,外部函數MessageBox能夠用多種方式調用:

>>> MessageBox()
1

>>> MessageBox(lpText="Spam,spam,spam")
1

>>> MessageBox(uType=2,lpText="foo bar")
3
>>> MessageBox(uType=2,lpText="foo bar")
4
>>> MessageBox(uType=2,lpText="foo bar")
5
>>>

第二個例子展示輸出參數的使用。Win32 API 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實例。

>>> r=GetWindowRect(0x00010310)
>>> r
<ctypes.wintypes.RECT object at 0x000001FC15529A48>
>>> print(r.left,r.top,r.right,r.bottom)
69 25 674 628
>>>

輸出參數與errcheck機制組合可做更多輸出處理和錯誤檢測。Win32 GetWindowRect api函數返回一個BOOL值以示調用成功或失敗,因此可對其進行錯誤檢測並在調用失敗時拋出異常:

>>> def errcheck(result, func, args):
      if not result:
             raise WinError()
      return args
>>> GetWindowRect.errcheck=errcheck

爲便於觀察,已以return args前加入print(result,func,args,sep='\n'):

>>> r=GetWindowRect(None)
OSError: [WinError 1400] 無效的窗口句柄。
>>> r=GetWindowRect(0x00010310)
1
<WinFunctionType object at 0x000001FC15510AD8>
(66320, <ctypes.wintypes.RECT object at 0x000001FC159A5AC8>)
>>> r
<ctypes.wintypes.RECT object at 0x000001FC159A5AC8>
>>>

如果errcheck函數原樣返回所接收到的參數元組,ctypes便繼續其對輸出參數的正常處理。如果想要返回一個窗口座標元組而不是RECT實例,那麼可以在函數中獲取RECT字段並返回所要求的結果,而ctypes的正常處理將不再進行:

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

4.4 返回類型

restype:默認情況下函數被假定返回c語言int型數據,如果返回數據爲其他類型則要爲函數對象設置restype屬性。

下面是一個更高級的例子,例中用到的strchr函數需要一個字符串指針和一個字符作爲參數,返回一個字符串指針。

>>> strchr=cdll.msvcrt.strchr
>>> strchr(b'abcdef',ord('d'))
-1860239533
>>> strchr.restype=c_char_p
>>> strchr(b'abcdef',ord('d'))
b'def'
>>> strchr(b'abcdef',ord('x'))
>>>

如果想避免上例中ord(‘x’)的調用,可以爲函數設置argtypes屬性,那麼第二個參數將從python單字符bytes轉換爲c語言char:

>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]
>>> strchr(b'abcdef',b'd')
b'def'
>>> strchr(b'abcdef',b'def')
ctypes.ArgumentError: argument 2: <class 'TypeError'>: wrong type
>>> strchr(b'abcdef',b'x')
>>> strchr(b'abcdef',b'd')
b'def'
>>>

如果外部函數返回整數,那麼還可以用一個python可調用對象(callable對象,例如函數或類)作爲restype屬性(值)。callable對象將以外部函數返回的整數爲參數被調用,其返回結果將作爲函數的最終結果。這對於返回值的錯誤檢查和自動拋出異常非常有幫助。

>>> GetModuleHandle = windll.kernel32.GetModuleHandleA
>>> def ValidHandle(value):
      if value==0:
             raise WinError()
      return value
>>> GetModuleHandle.restype=ValidHandle
>>> GetModuleHandle(None)
480444416
>>> GetModuleHandle('something silly')
OSError: [WinError 126] 找不到指定的模塊。
>>>

WinError()函數將調用Windows API FormatMessage()獲取錯誤代碼對應的字符串,並返回一個異常。WinError()帶有一個可選的錯誤代碼參數,如果沒有,則它會通過GetLastError()來獲取錯誤代碼。

此外,通過爲函數設置errcheck屬性可以使用功能更爲強大的錯誤檢查機制,詳情請參考相關使用手冊。

argtypes和restype只對ctypes導出的DLL函數有效

>>> def func(a,b,c):
      return a.value+b.value+c.contents.value
>>> func.argtypes=[c_int,c_int,POINTER(c_int)]
>>> func.restype=[c_int]
>>> a1=c_int(9)
>>> a2=c_int(10)
>>> a3=c_int(11)
>>> ap3=POINTER(c_int)(c_int(11))
>>> r1=func(a1,a2,ap3)
>>> r1
30
>>> type(r1)
<class 'int'>
>>> r2=func(a1,a2,a3)
AttributeError: 'c_long' object has no attribute 'contents'

4.5 指針參數與引用參數

有時候一個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''
>>> cdll.msvcrt.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.140000104904175 b'Hello'
>>>

cdll.msvcrt.sscanf(b'1 3.14 Hello',b'%d %f %s',pointer(i),pointer(f),s)結果同上。

5、ctypes高級數據類型

5.1 structure(結構)與union(聯合)

與c語言structure、union對應的python類型必須派生自定義在ctypes模塊的Structure和Union基類。每種子類必須定義_fields_屬性,該屬性是一個二元元組列表,各元組包含一個fieldc語言結構體或聯合體的屬性/字段)的名稱及其類型。field類型必須是ctypes類型,比如c_int,或其它任意派生自ctypes的類型:structure,union,array,pointer。

下面是POINT結構的一個簡單例子,包含兩個整數屬性x和y,並介紹如何初始化一個結構:

>>> 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=POINT(1,2,3)
TypeError: too many initializers
>>>

可以定義一個比POINT複雜得多的結構,還可以通過將其他結構作爲一個field類型來包含該結構

下面是一個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
>>>

除上述之外,像RECT這種結構中包含其他結構的複合結構(嵌套結構)有多種實例化方式:

>>> r = RECT(POINT(1, 2), POINT(3, 4))
>>> r = RECT((1, 2), (3, 4))
>>>

類中獲取的field描述能夠提供非常有用的信息,這對調試非常有幫助:

>>> print(POINT.x)
<Field type=c_long, ofs=0, size=4>
>>> print(POINT.y)
<Field type=c_long, ofs=4, size=4>
>>>

警告:ctypes不支持以傳值的形式將帶有bit-field(位域/位字段)的union或structure傳給函數作參數。而這在32位的x86系統上可能行得通,但不被運作在一般情況下的函數庫所保證。帶有bit-field的union或structure應該一直以傳指針的形式傳給函數作參數。

5.2 structure/union內存對齊與字節次序

默認地,structure和union字段(field)在內存中的對齊方式與c編譯器的對齊處理方式是一致的。可以通過在子類定義中指定類屬性_pack_來覆蓋默認對齊方式。_pack_必須是一個正整數,指明字段最大對齊方式。這也是MSVC中#pragma pack(n)指令在做的事。

默認對齊方式:

>>> class POINT(Structure):
      _fields_ = [("x", c_int),
                 ("xbit",c_int64,33),
                 ("y", c_int),
                 ("ybit",c_int64,33),
                 ("z",c_longlong)]
>>> print(POINT.x)
<Field type=c_long, ofs=0, size=4>
>>> print(POINT.xbit)
<Field type=c_longlong, ofs=8:0, bits=33>
>>> print(POINT.y)
<Field type=c_long, ofs=16, size=4>
>>> print(POINT.ybit)
<Field type=c_longlong, ofs=24:0, bits=33>
>>> print(POINT.z)
<Field type=c_longlong, ofs=32, size=8>

指定對齊方式:

>>> class POINT(Structure):
      _fields_ = [("x", c_int),
                 ("xbit",c_int64,33),
                 ("y", c_int),
                 ("ybit",c_int64,33),
                 ("z",c_longlong)]
      _pack_=4    
>>> print(POINT.x)
<Field type=c_long, ofs=0, size=4>
>>> print(POINT.xbit)
<Field type=c_longlong, ofs=4:0, bits=33>
>>> print(POINT.y)
<Field type=c_long, ofs=12, size=4>
>>> print(POINT.ybit)
<Field type=c_longlong, ofs=16:0, bits=33>
>>> print(POINT.z)
<Field type=c_longlong, ofs=24, size=8>

對於structure和union,ctypes使用本地字節次序。要構建一個非本地字節次序結構,就得用到BigEndianStructureLittleEndianStructureBigEndianUnionLittleEndianUnion基類之一進行派生。派生類不能包含指針字段。

注:BigEndianUnion,LittleEndianUnion在ctypes中未實際定義。

大端序、小端序分別指從存儲單元低位地址開始的高位字節優先存放方式、低位字節優先存放方式(字節串類型無區別)。

>>> import sys
>>> sys.byteorder
'little'
>>> class union(Union):
      _fields_=[('x',c_uint32),('x1',c_uint32,8),('pad',c_ulong),
               ('x2',c_uint32,16),('x3',c_uint32,24),
               ('x4',c_uint32,32)
               ]
>>> y=union()
>>> y.x=0xaabbccdd
>>> hex(y.x1)
'0xdd'
>>> hex(y.x2)
'0xccdd'
>>> hex(y.x3)
'0xbbccdd'
>>> hex(y.x4)
'0xaabbccdd'
>>> from struct import *
>>> n=0xaabbccdd
>>> pack('<i',n)
struct.error: argument out of range
>>> pack('<I',n)
b'\xdd\xcc\xbb\xaa'
>>> pack('@I',n)
b'\xdd\xcc\xbb\xaa'
>>> pack('>I',n)
b'\xaa\xbb\xcc\xdd'
>>> pack('!I',n)
b'\xaa\xbb\xcc\xdd'
>>>

現有兩個文件,一個是server.py:

import struct,socket,pickle
from ctypes import *
class D(BigEndianStructure):
    _fields_=[('x',c_uint)]
BUFSIZE=1024
ADDR=("localhost",2046)
recvsocket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
recvsocket.bind(ADDR)
while True:
    print('waiting for the data...')
    data,addr=recvsocket.recvfrom(BUFSIZE)
    print(repr(data))
    dlen=len(data)
    if dlen==58:
        (data1,)=struct.unpack('!58s',data)
        print(repr(data1))
        obj=pickle.loads(data1)
        print(hex(obj.x))
        (data2,)=struct.unpack('@58s',data)
        print(repr(data2))
        obj=pickle.loads(data2)
        print(hex(obj.x))
    if dlen==4:
        (data1,)=struct.unpack('!I',data)
        print(hex(data1))
        (data2,)=struct.unpack('@I',data)
        print(hex(data2))
        break
print('closing...')
recvsocket.close()

另一個是client.py:

import struct,socket
from ctypes import *
import pickle
class D(BigEndianStructure):
    _fields_=[('x',c_uint)]
sdata=D()
sdata.x=0xaabbccdd
BUFSIZE=1024
ADDR=("localhost",2046)
sendsocket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
pdata=pickle.dumps(sdata)
print(repr(pdata))
sendsocket.sendto(pdata,ADDR)
sendsocket.sendto(struct.pack('!I',sdata.x),ADDR)
sendsocket.close()

先運行server,再運行client,服務器端輸出結果如下:

waiting for the data...
b'\x80\x03c_ctypes\n_unpickle\nq\x00c__main__\nD\nq\x01}q\x02C\x04\xaa\xbb\xcc\xddq\x03\x86q\x04\x86q\x05Rq\x06.'
b'\x80\x03c_ctypes\n_unpickle\nq\x00c__main__\nD\nq\x01}q\x02C\x04\xaa\xbb\xcc\xddq\x03\x86q\x04\x86q\x05Rq\x06.'
0xaabbccdd
b'\x80\x03c_ctypes\n_unpickle\nq\x00c__main__\nD\nq\x01}q\x02C\x04\xaa\xbb\xcc\xddq\x03\x86q\x04\x86q\x05Rq\x06.'
0xaabbccdd
waiting for the data...
b'\xaa\xbb\xcc\xdd'
0xaabbccdd
0xddccbbaa
closing...

本例主要考查客戶端sdata.x=0xaabbccdd在服務器端的輸出來理解大端序和小端序的區別,考查依據是隻有當服務器端的unpack次序和客戶端數據實際存放次序一致時才能輸出正確結果。再次重申,大、小端序只對多字節類型的數據有效。客戶端實例對象sdata被序列化成字節串後發住服務器,所以,服務器端原始data,以及分別解析爲大端序、小端序的data1、data2輸出結果都相同,data1和data2通過pickle.loads還原的obj.x也相同。客戶端第二次將sdata.x pack爲網絡序(一般爲大端序)的c類型unsigned int後發送給服務器,很容易看到解析爲大端序的結果0xaabbccdd與客戶端sdata.x一致。

5.3 位字段

前面在介紹structure/union對齊的時候_fields_中出現過("xbit",c_int64,33)這樣的元組。這實際上在定義一個位字段。可以創建帶位字段(bit field,位域)的structure/union,只有整型字段纔有位字段,由_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>
>>> obj=Int(0xaa,0xbb)
>>> sizeof(obj)
4
>>> class Int(Structure):
      _fields_ = [("first_16", c_int, 16),
             ("second_16", c_int64, 16)]
>>> print(Int.first_16)
<Field type=c_long, ofs=0:0, bits=16>
>>> print(Int.second_16)
<Field type=c_longlong, ofs=8:0, bits=16>
>>>

5.4 array

array是一種序列,包含固定數量的同一類型實例對象。強烈建議用帶一個正整數的類型繁衍的方式創建array類型

>>> class POINT(Structure):
      _fields_ = [("x", c_int),
                 ("y", c_int)]
>>> TenPointsArrayType = POINT * 10
>>> class MyStruct(Structure):
      _fields_ = [("a", c_int),
                    ("b", c_float),
                    ("point_array", POINT * 4)]   
>>> print(len(MyStruct().point_array))
4
>>>

如果TenPointsArrayType可視爲一種顯式數組類型,那麼“POINT * 4”則是一種匿名數組類型。array實例化也很簡單:

>>> arr=TenPointsArrayType()
>>> for pt in arr:
      print(pt.x,pt.y)

因爲TenPointsArrayType初始化時內容爲0,所以輸出結果全爲0。初始化時可以指定相應類型的初始化對象:

>>> TenIntegers = c_int * 10
>>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> ii
<__main__.c_long_Array_10 object at 0x00000146DBC08BC8>
>>> for i in ii:print(i,end=' ')
1 2 3 4 5 6 7 8 9 10
>>>

5.5 pointer

調用ctypes的pointer()可以創建一個pointer實例對象:

>>> i = c_int(42)
>>> pi=pointer(i)
>>>

pointer實例對象有一個contents屬性,它返回一個pointer所指向的對象,上面這個例子就是i:

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

注意,ctypes沒有OOR(原始對象返回,機制),每次從contents屬性獲取一個新構建的等價對象

>>> pi.contents is i
False
>>> pi.contents is pi.contents
False
>>>

pointer對象的contents賦另一個c_int實例將導致pointer對象指向新實例所在的內存位置

>>> i = c_int(99)
>>> pi.contents=i
>>> pi.contents
c_long(99)
>>>

pointer實例還可以被整數索引:

>>> pi[0]
99
>>>

給索引賦值會改變pointer指向對象的值:

>>> i
c_long(99)
>>> pi[0]=22
>>> i
c_long(22)
>>>

一個完整的例子:

>>> PI=POINTER(c_int)
>>> a=c_int(90)
>>> b=PI(a)
>>> b.contents=c_int(900)
>>> a
c_long(90)
>>> b=PI(a)
>>> b.contents
c_long(90)
>>> b.contents.value
90
>>> b.contents.value=900
>>> a
c_long(900)
>>> b[0]
900
>>> b[0]=890
>>> a
c_long(890)
>>>

使用pointer對象時,可以用不同於0的索引,但必須清楚爲什麼這麼做,就像在c中強制性訪問或改變內存位置一樣。一般來說如果從c函數獲取到一個等價pointer對象,並且知道pointer對象實際上指向一個array而不是單一對象,那麼就只能使用這個特性(非0索引訪問)。

表象之下,pointer()函數遠不止簡單地創建pointer實例,首先得建立pointer類型,這是調用POINTER()函數完成的。POINTER()接受任意ctypes類型爲參數,並返回一個新類型:

>>> PI=POINTER(c_int)
>>> PI
<class '__main__.LP_c_long'>
>>> PI(42)
TypeError: expected c_long instead of int
>>> PI(c_int(42))
<__main__.LP_c_long object at 0x000001FD2FB97A48>
>>>

調用pointer()的時候如果沒有給定初始化對象則會創建一個NULL(空) pointer。NULL指針對象等效False布爾值:

>>> null_ptr=POINTER(c_int)()
>>> bool(null_ptr)
False
>>>

ctypes在解引用pointer對象(去掉引用形式的引用,獲取所指對象)的時候會做NULL檢查(解引用一個非法的非NULL指針對象會致python崩潰):

>>> null_ptr[0]
ValueError: NULL pointer access
>>> null_ptr[0]=1234
ValueError: NULL pointer access
>>>

5.6 類型轉換

通常情況下ctypes會做嚴格類型檢查。這意味着如果POINTER(c_int)出現在一個函數argtypes列表或作爲一個structure定義的字段成員類型,函數參數或結構字段只能接受與該類型完全相同的實例對象。實際上,在ctypes接受其他對象之處存在一些例外。例如,傳遞相容的array實例來替代pointer實例。所以,對於POINTER(c_int),ctypes接受一個對等的c_int型array:

>>> class Bar(Structure):
      _fields_ = [("count", c_int), ("values", POINTER(c_int))]     
>>> bar=Bar()
>>> bar.values=POINTER(c_int)(c_int(90))
>>> bar.values
<__main__.LP_c_long object at 0x000001FD2FB97B48>
>>> bar.values.contents.value
90
>>> bar.values=90
TypeError: expected LP_c_long instance, got int
>>> bar.count=3
>>> bar.values=(c_int*3)(1,2,3)
>>> for i in range(bar.count):
      print(bar.values[i],end=" ")
1 2 3
>>>

這也就是說,array實例和pointer實例是相容的,可以相互轉換。

此外,如果一個函數參數顯式地在argtypes中聲明爲pointer類型,比如POINTER(c_int),那麼可以把pointer所指類型的實例對象(本例中爲c_int實例)傳給函數作參數。ctypes將會自動地調用byref()進行必要轉換,POINTERpointer也是相容的。

>>> bar=Bar()
>>> arr=(c_int*3)(1,2,3)
>>> ptr=pointer(c_int(90))
>>> PTR=POINTER(c_int)(c_int(90))
>>> bar.values=arr
>>> bar.values=ptr
>>> bar.values=PTR
>>> bar.values=90
TypeError: expected LP_c_long instance, got int
>>>

要設置一個POINTER類型字段爲NULL,可以直接賦值None:

>>> bar.values=None
>>>

在c語言中,一種類型可以轉換成另一種類型。對於類型不相容的實例,ctypes提供一個與c類型轉換作用相同的cast()函數進行類型轉換。之前定義的Bar結構values字段接受一個POINTER(c_int)對象(及其相容對象),而不是其他類型的對象:

>>> bar.values=(c_byte*4)()
TypeError: incompatible types, c_byte_Array_4 instance instead of LP_c_long instance
>>>

這種情況cast()函數就很有用。

cast()函數可用於將一種ctypes類型實例轉換成另一種ctypes數據類型的pointer對象。cast()有兩個參數,一個參數是能轉換成某種類型pointer的ctypes對象,一個參數是pointer類型。cast()返回一個和第一個參數引用內存塊相同的第二個參數指定類型的實例。

>>> a=(c_byte*4)()
>>> cast(a,POINTER(c_int))
<__main__.LP_c_long object at 0x000001FD2FFDF248>
>>>

所以,cast()可用來爲Bar結構的values字段賦值:

>>> bar=Bar()
>>> bts=(c_ubyte*4)(0xdd,0xcc,0xbb,0xaa)
>>> bar.values=cast(bts,POINTER(c_int))
>>> bar.values.contents.value
-1430532899
>>> hex(cast(bar.values,POINTER(c_uint)).contents.value)
'0xaabbccdd'
>>>

5.7 不完全類型

不完全類型指尚未實際定義成員的structureunionarrayc語言中,它們可以通過聲明在前而在定義在後的方式來指定:

struct cell;
...
struct cell {
char* name;
struct cell *next;
};

如下所示,直接將它們轉換成ctypes代碼,但實際無效:

>>> class cell(Structure):
      _fields_ = [("name", c_char_p),
                  ("next", POINTER(cell))]
NameError: name 'cell' is not defined
>>>

這是因爲在目前class語句中新類cell不可用。解決方案是,先定義cell類,再在class語句後面設置_fields_屬性(只能設置一次):

>>> class cell(Structure):pass
>>> cell._fields_ = [("name", c_char_p),
                  ("next", POINTER(cell))]
>>> cell._fields_ = [("name", c_wchar_p),
                  ("next", POINTER(cell))]
AttributeError: _fields_ is final

下面來試一試。創建兩個cell實例,讓它們互相指向彼此,然後按指向鏈遍歷幾次:

>>> c1=cell()
>>> c1.name='白天'.encode('utf-8')
>>> c2=cell()
>>> c2.name=b'night'
>>> c1.next=pointer(c2)
>>> c2.next=pointer(c1)
>>> p=c1
>>> for i in range(8):
      print(p.name.decode('utf-8'),end=' ')
      p=p.next[0]  
白天 night 白天 night 白天 night 白天 night
>>>

5.8 回調函數

ctypes能從python的callable對象創建c函數pointer對象。這類對象有時候被稱爲回調函數(callback function)。

首先,必須爲回調函數創建一個類型。這個類型要能確定函數的調用約定,返回類型,接受的參數類型和數量。這可以通過下面的工廠函數來完成。

工廠函數CFUNCTYPE()創建cdecl調用約定的回調函數類型。windows中,WINFUNCTYPE()工廠函數創建stdcall調用約定的回調函數類型。

這兩個工廠函數調用時,第一個參數爲返回類型,剩下的參數則爲回調函數所要求的參數(數量與類型)。

下面舉一個使用標準c函數庫qsort()的例子,qsort()函數借助一個回調函數對數據項進行排序,在這裏用於對整數array排序:

>>> IntArray=c_int*5
>>> ia=IntArray(5,1,7,33,99)
>>> qsort=cdll.msvcrt.qsort
>>> qsort.restype=None
>>>

調用qsort()的時候必須有以下幾個參數:一個指向待排序數據的pointer(可以是數組),array中項目的數量,各項大小,以及一個回調函數(一個指向用於自定義何謂“比較”的比較函數pointer)。回調函數被調用的時候帶有兩個指向數據項的pointer,並且如果第一項比第二項小必須返回負整數,二者相等必須返回0,第一項比第二項大必須返回正整數。

所以,回調函數接受兩個指向整數的pointer,並返回一個整數。下面就來創建回調函數的類型:

>>> 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 1 5
py_cmp_func 7 5
py_cmp_func 33 5
py_cmp_func 99 5
py_cmp_func 1 99
py_cmp_func 7 99
py_cmp_func 33 99
py_cmp_func 1 33
py_cmp_func 7 33
py_cmp_func 1 7
>>>

現在來真正完成兩項間的比較並返回相應的比較結果:

>>> 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)
>>> ia=IntArray(5,1,7,33,99)
>>> qsort(ia,len(ia),sizeof(c_int),cmp_func)
py_cmp_func 1 5
py_cmp_func 7 5
py_cmp_func 33 7
py_cmp_func 99 33
py_cmp_func 1 5
py_cmp_func 7 5
py_cmp_func 33 7
py_cmp_func 1 5
py_cmp_func 7 5
py_cmp_func 1 5
>>>

最終排序結果:

>>> for i in ia:print(i,end=' ')
1 5 7 33 99
>>>

注意:只要CFUNCTYPE()對象被用於c代碼,就務必要確保對它們的持續引用。ctypes不會而如果程序員也不的話,則它們可能被垃圾回收機制回收,從而在創建回調函數時給予程序致命一擊,類似如下所示:

>>> cmp_func=98
>>> qsort(ia,len(ia),sizeof(c_int),cmp_func)
OSError: exception: access violation writing 0x0000000000000062

同時也要注意到,如果回調函數被創建自python控制之外的線程調用(如通過外部代碼調用回調函數),ctypes會在每次請求調用時創建一個全新虛擬python線程來執行回調函數。這一行爲符合大多數潛在期望和預想,但有一樣例外,用threading.local存放的值將不會在不同回調過程中存活(只在本次調用中是同一對象),甚至即便它們是被同一c線程調用。

5.9 DLL導出變量

一些共享函數庫不僅導出函數,而且也導出變量。python庫的一個例子是Py_OptimizeFlag,其值爲整數0,1或2,這取決於啓動時給定的標誌是-O還是-OO。

ctypes使用所訪問變量對應的ctypes類型的in_dll()類方法來訪問這樣的值。pythonapi是給定的要訪問的python c API預定義符號:

>>> opt_flag = c_int.in_dll(pythonapi, "Py_OptimizeFlag")
>>> opt_flag
c_long(0)
>>>
C:\Users\cyx>python -O
Python 3.6.5 |Anaconda, Inc.| (default, Mar 29 2018, 13:32:41) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from ctypes import *
>>> opt_flag = c_int.in_dll(pythonapi, "Py_OptimizeFlag")
>>> opt_flag
c_long(1)
>>>

下面這個擴展例子示範pointer的使用,該pointer訪問導出自python的PyImport_FrozenModules。

原文檔PyImport_FrozenModules變量參考引用如下:

This pointer is initialized to point to an array of struct_frozen records, terminated by one whose members are all NULL or zero. When a frozen module is imported, it is searched in this table. Third-party code could play tricks with this to provide a dynamically created collection of frozen modules.

所以,對pointer對象PyImport_FrozenModules的處理非常有用。爲限制例子的大小,例中僅展示如何用ctypes讀取PyImport_FrozenModules:

>>> class struct_frozen(Structure):
      _fields_ = [("name", c_char_p),
                 ("code", POINTER(c_ubyte)),
                 ("size", c_int)]
>>>

現在僅是定義struct_frozen數據類型,還要獲取指向PyImport_FrozenModules的pointer:

>>> FrozenTable = POINTER(struct_frozen)
>>> table = FrozenTable.in_dll(pythonapi, "PyImport_FrozenModules")
>>>

由於table是一個指向struct_frozen數組記錄的pointer,所以可以對它反覆迭代訪問。但因爲pointer沒有大小,所以一定要確保循環能夠終止,否則遲早會因訪問違規或別的原因崩潰。最好是在遇到NULL入口的時候主動終止並跳出循環:

>>> for item in table:
      if item.name is None:
             break
      print(item.name.decode("ascii"), item.size) 
_frozen_importlib 29089
_frozen_importlib_external 38876
__hello__ 139
__phello__ -139
__phello__.spam 139
>>>

事實上,標準python有一個凍結模塊和一個凍結包(size顯示爲負)並不爲人所知,僅用於測試。可以試着導入看看(如import __hello__)。

5.10彩蛋

ctypes中有些實際結果遠超一般預期的“彩蛋”。

5.10.1 交錯的內部關係

>>> 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
>>> RC.a,RC.b=RC.b,RC.a
>>>

結果當然應該是3 4 1 2。想多了。

>>> print(RC.a.x,RC.a.y,RC.b.x,RC.b.y)
3 4 3 4
>>>

竟然是3 4 3 4,發生了什麼?

下面是“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相應緩存中(RC.a)。(因爲RC.a被temp1使用)輪換下來,temp1的內容也發生變化,所以最後“RC.b = temp1”就不會出現所期待的結果。

>>> for i in range(4):exec("A"+str(i)+"=type('A%d',(object,),{})"%i)
>>> a=A0();b=A1();c=A2();d=A3()
>>> a,b=c,d
>>> a is c
True
>>> temp0, temp1 = RC.b, RC.a
>>> temp0 is RC.b
False
>>> temp0.x
3
>>> temp0.x=33
>>> RC.b.x
33
>>>

一定要記住,從structureunionarray獲取子成員對象不要用copy(類似淺複製,“=”語義發生改變),而是獲取一個可訪問根對象內部緩存的封裝(代理)對象。

>>> import copy
>>> print(RC.a.x,RC.a.y,RC.b.x,RC.b.y)
1 2 3 4
>>> class record:
      def __init__(self,RC):
             self.a=copy.deepcopy(RC.a)
             self.b=copy.deepcopy(RC.b)         
>>> rRC=record(RC)
>>> print(rRC.a.x,rRC.a.y,rRC.b.x,rRC.b.y)
1 2 3 4
>>> rRC.a.x=11;rRC.a.y=22
>>> print(rRC.a.x,rRC.a.y,rRC.b.x,rRC.b.y)
11 22 3 4
>>> print(RC.a.x,RC.a.y,RC.b.x,RC.b.y)
1 2 3 4
>>>

5.10.2 是我非我

>>> 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對象以替代所存對象的內容,也就是說,每次訪問內容(value)時都將重新構造一個新的python對象。

6、可變數據類型

這裏的可變主要是指大小可變。ctypes支持可變array和structure。

resize()函數可用於重置一個已存在的ctypes對象內存塊大小,帶有兩個參數,一個是對象,一個是請求的以字節爲單位的內存塊大小。內存塊大小不得小於指定對象的類型對應的大小,否則會拋出ValueError異常:

>>> short_array=(c_short*4)()
>>> sizeof(short_array)
8
>>> resize(short_array,4)
ValueError: minimum size is 8
>>> resize(short_array,32)
>>> sizeof(short_array)
32
>>> len(short_array)
4
>>> sizeof(type(short_array))
8
>>>

這種特性除了好還是好,總之非常好。那麼如何才能訪問到數組中新增的元素呢?由於short_array的類型對外聲稱的仍是4個元素,如果訪問其他元素將出現錯誤:

>>> short_array[:]
[0, 0, 0, 0]
>>> short_array[7]
IndexError: invalid index
>>>

通過ctypes配合可變數據類型使用的另一種方式是利用python的動態特性,並根據實際情況在已知所需內存大小後定義或重新定義數據類型:

>>> short_array=(c_short*16)()

還有一種比較危險的方式:

>>> short_arr=(c_short*15)()
>>> short_arr[:]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> resize(short_arr,40)
>>> a=POINTER(c_short)(short_arr)
>>> length=sizeof(short_arr)//sizeof(c_short)
>>> for i in range(length):a[i]=i
>>> short_arr[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> for i in range(length):print(a[i],end=' ')
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
>>>

本教程翻譯、整理、註解自官網文檔,更多版本、平臺、使用指南等細節,請參考:

https://docs.python.org/release/3.6.5/library/ctypes.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章