Search code examples
pythonctypes

Redundant prototype parameter specification in ctypes


The ctypes function prototype specification says

The first item is an integer containing a combination of direction flags for the parameter: [...]

4: Input parameter which defaults to the integer zero.

and shortly after,

The optional third item is the default value for this parameter.

Why does "type 4" exist when the exact same thing, seemingly, can be specified by writing 0 in the third tuple element? Are they indeed equivalent? Why would one be preferred over the other?

In fact, there's some evidence that they aren't equivalent: if I define WlanRegisterNotification like

proto = ctypes.WINFUNCTYPE(
    ctypes.wintypes.DWORD,

    ctypes.wintypes.HANDLE,
    ctypes.wintypes.DWORD,
    ctypes.wintypes.BOOL,
    WLAN_NOTIFICATION_CALLBACK,
    ctypes.wintypes.LPVOID,
    ctypes.wintypes.LPVOID,
    ctypes.POINTER(ctypes.wintypes.DWORD),
)
fun = proto(
    ('WlanRegisterNotification', wlanapi),
    (
        (IN, 'hClientHandle'),
        (IN, 'dwNotifSource'),
        (IN, 'bIgnoreDuplicate'),
        (IN | DEFAULT_ZERO, 'funcCallback'),
        (IN | DEFAULT_ZERO, 'pCallbackContext'),
        (IN | DEFAULT_ZERO, 'pReserved'),
        (OUT, 'pdwPrevNotifSource'),
    ),
)

It behaves nonsensically when passed values for the first four parameters:

TypeError: call takes exactly 3 arguments (4 given)

Why should it assume that there are exactly three arguments, when arguments four, five and six are given defaults (but should accept explicit values)? Removing DEFAULT_ZERO and adding , None solves the problem, but is not a satisfying answer.


Solution

  • DEFAULT_ZERO seems to mean "don't pass a parameter...use a default as input", so it only makes sense to use for pReserved. The other two parameters need valid defaults. Note that int(0) is not a valid default for a callback, and None raises expected WinFunctionType instance instead of NoneType, so I passed a default null instance of the notification callback type.

    Functional example:

    import ctypes
    import ctypes.wintypes
    
    PWLAN_NOTIFICATION_DATA = ctypes.c_void_p
    WLAN_NOTIFICATION_CALLBACK = ctypes.WINFUNCTYPE(None, PWLAN_NOTIFICATION_DATA, ctypes.wintypes.LPVOID)
    
    null_callback = WLAN_NOTIFICATION_CALLBACK()
    
    # valid callback
    @WLAN_NOTIFICATION_CALLBACK
    def callback(param1, param2):
        print(param1, param2)
    
    IN = 1
    OUT = 2
    DEFAULT_ZERO = 4
    
    wlanapi = ctypes.WinDLL('wlanapi')
    
    proto = ctypes.WINFUNCTYPE(
        ctypes.wintypes.DWORD,
    
        ctypes.wintypes.HANDLE,
        ctypes.wintypes.DWORD,
        ctypes.wintypes.BOOL,
        WLAN_NOTIFICATION_CALLBACK,
        ctypes.wintypes.LPVOID,
        ctypes.wintypes.LPVOID,
        ctypes.POINTER(ctypes.wintypes.DWORD),
    )
    
    fun = proto(
        ('WlanRegisterNotification', wlanapi),
        (
            (IN, 'hClientHandle'),
            (IN, 'dwNotifSource'),
            (IN, 'bIgnoreDuplicate'),
            (IN, 'funcCallback', null_callback),
            (IN, 'pCallbackContext', None),
            (IN | DEFAULT_ZERO, 'pReserved'),
            (OUT, 'pdwPrevNotifSource'),
        ),
    )
    fun.errcheck = lambda result, func, args: (result, args[5])
    print(fun(0,0,0))
    print(fun(0,0,0,callback))
    print(fun(0,0,0,callback,None))
    print(fun(0,0,0,callback,None,None))  # this will fail. pReserved can't be passed.
    

    Output (note that ERROR_INVALID_PARAMETER = 87 since I passed garbage params, but it makes it passed ctypes parameter validation):

    (87, 0)
    (87, 0)
    (87, 0)
    Traceback (most recent call last):
      File "C:\test.py", line 44, in <module>
        print(fun(0,0,0,callback,None,None))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    TypeError: call takes exactly 5 arguments (6 given)
    

    Frankly I avoid paramFlags and use .argtypes and .restype to declare arguments and return type and write wrappers to make the C API exactly the way I want it if needed:

    import ctypes as ct
    import ctypes.wintypes as w
    
    PWLAN_NOTIFICATION_DATA = ct.c_void_p
    WLAN_NOTIFICATION_CALLBACK = ct.WINFUNCTYPE(None, PWLAN_NOTIFICATION_DATA, w.LPVOID)
    
    null_callback = WLAN_NOTIFICATION_CALLBACK()
    
    # valid callback
    @WLAN_NOTIFICATION_CALLBACK
    def callback(param1, param2):
        print(param1, param2)
    
    wlanapi = ct.WinDLL('wlanapi')
    wlanapi.WlanRegisterNotification.argtypes = w.HANDLE, w.DWORD, w.BOOL, WLAN_NOTIFICATION_CALLBACK, w.LPVOID, w.LPVOID, ct.POINTER(w.DWORD)
    wlanapi.WlanRegisterNotification.restype = w.DWORD
    
    def fun(h, src, ignore, cb=null_callback, context=None):
        prev = w.DWORD()
        result = wlanapi.WlanRegisterNotification(h, src, ignore, cb, context, None, ct.byref(prev))
        return result, prev.value
    
    print(fun(0, 0, 0))
    print(fun(0, 0, 0, callback))
    print(fun(0, 0, 0, callback, None))
    print(fun(0, 0, 0, callback, None, None))  # this will fail. pReserved can't be passed.