python ctypes issue on different OSes

Question:

I’m trying to convert C function for python 3.6 use.

code as below:

lib = ctypes.WinDLL('ftrScanAPI.dll') # provided by fingerprint scanner
class FTRSCAN_IMAGE_SIZE(ctypes.Structure):
    _fields_ = [
    ("nWidth", ctypes.c_int),
    ("nHeight", ctypes.c_int),
    ("nImageSize", ctypes.c_int)
]

print('Open device and get device handle...')
hDevice = lib.ftrScanOpenDevice()
print('handle is', hDevice)
print('Get image size...')
Image_size = FTRSCAN_IMAGE_SIZE(0, 0, 0)
if lib.ftrScanGetImageSize(hDevice, ctypes.byref(Image_size)):
    print('Get image size succeed...')
    print('  W', Image_size.nWidth)
    print('  H', Image_size.nHeight)
    print('  Size', Image_size.nImageSize)
else:
    print('Get image size failed...')

function definition:

typedef struct FTR_PACKED __FTRSCAN_IMAGE_SIZE {
    int nWidth;
    int nHeight;
    int nImageSize;
} FTRSCAN_IMAGE_SIZE, *PFTRSCAN_IMAGE_SIZE;
FTRHANDLE ftrScanOpenDevice();  # typedef void *  FTRHANDLE;
BOOL ftrScanGetImageSize(FTRHANDLE ftrHandle, 
    PFTR_SCAN_IMAGE_SIZE pImageSize);

But different OSes with the same code seems to have different result:

  • On Windows 7 64 bit
    output1

  • On Windows 10 64 bit
    I don’t print “handle is here”
    output2

    What I’ve tried:
    According to some answers on stack overflow, this may be caused by not assigning function argtypes and restype explicitly, so I tried and failed.

  • Asked By: newman

    ||

    Answers:

    You should show your .argtypes and .restype attempts, but give this a try:

    from ctypes import wintypes as w
    
    class FTRSCAN_IMAGE_SIZE(ctypes.Structure):
        _fields_ = [('nWidth', ctypes.c_int),
                    ('nHeight', ctypes.c_int),
                    ('nImageSize', ctypes.c_int)]
    
    FTRHANDLE = ctypes.c_void_p
    
    lib = ctypes.WinDLL('ftrScanAPI.dll')
    lib.ftrScanOpenDevice.argtypes = None
    lib.ftrScanOpenDevice.restype = FTRHANDLE
    lib.ftrScanGetImageSize.argtypes = FTRHANDLE,ctypes.POINTER(FTRSCAN_IMAGE_SIZE)
    lib.ftrScanGetImageSize.restype = w.BOOL
    

    Check the use of WinDLL vs. CDLL. It won’t matter if your Python is 64-bit, but it makes a difference on 32-bit Python. Use CDLL if the functions use C calling convention (__cdecl) and WinDLL if the functions use __stdcall calling convention. If the header file isn’t clear, the default is usually __cdecl. Edit: from the API link in another answer, it is __stdcall and WinDLL should be used.

    Answered By: Mark Tolonen

    In 99% of the cases, inconsistencies between arguments (and / or return) type inconsistencies are the cause (check [SO]: C function called from Python via ctypes returns incorrect value (@CristiFati’s answer) for more details).

    Always have [Python.Docs]: ctypes – A foreign function library for Python open when working with CTypes.

    I found [GitHub]: erikssm/futronics-fingerprint-reader – (master) futronics-fingerprint-reader/ftrScanAPI.h (I don’t know how different it’s from what you currently have, but things you posted so far seem to match), and I did some changes to your code:

    • Define argtypes and restype for functions

    • Define missing types (for clarity only)

    • Some other insignificant changes (renames)

    • One other thing that I noticed in the above file, is a #pragma pack(push, 1) macro (check [MS.Docs]: pack for more details). For this structure it makes no difference (thanks @AnttiHaapala for the hint), as the 3 int (4 byte) members alignment doesn’t change, but for other structures (with "smaller" member types (e.g. char, short)) you might want to add: _pack_ = 1

    Your modified code (needless to say, I didn’t run it as I don’t have the .dll):

    #!/usr/bin/env python
    
    import ctypes as ct
    from ctypes import wintypes as wt
    
    
    # ...
    
    
    class FTRSCAN_IMAGE_SIZE(ct.Structure):
        # _pack_ = 1
        _fields_ = (
            ("nWidth", ct.c_int),
            ("nHeight", ct.c_int),
            ("nImageSize", ct.c_int),
        )
    
    PFTRSCAN_IMAGE_SIZE = ct.POINTER(FTRSCAN_IMAGE_SIZE)
    FTRHANDLE = ct.c_void_p
    
    lib = ct.WinDLL("ftrScanAPI.dll")  # provided by fingerprint scanner
    
    ftrScanOpenDevice = lib.ftrScanOpenDevice
    ftrScanOpenDevice.argtypes = ()
    ftrScanOpenDevice.restype = FTRHANDLE
    
    ftrScanGetImageSize = lib.ftrScanGetImageSize
    ftrScanGetImageSize.argtypes = (FTRHANDLE, PFTRSCAN_IMAGE_SIZE)
    ftrScanGetImageSize.restype = wt.BOOL
    
    print("Open device and get device handle...")
    h_device = ftrScanOpenDevice()
    print("Handle is", h_device)
    print("Get image size...")
    image_size = FTRSCAN_IMAGE_SIZE(0, 0, 0)
    
    if ftrScanGetImageSize(h_device, ct.byref(image_size)):
        print("Get image size succeed...")
        print("  W", image_size.nWidth)
        print("  H", image_size.nHeight)
        print("  Size", image_size.nImageSize)
    else:
        print("Get image size failed...")
    
    Answered By: CristiFati

    The problem seems to simply be that the device handle is a 64-bit entity (a typedef’d pointer to void). That it works in your Windows 7 was just a fluke, as the upper 33 bits of the handle were correctly zero.

    The return type of all functions in ctypes defaults to 32-bit int. Now in Windows 10 it seems that the 32th bit (sign bit) was set, which causes a sign extension somewhere when the handle-coerced-to-int is pushed on the 64-bit stack for function call. The resulting address has all upper bits set (0xFFFFFFFFA…) which points to kernel space and not in the user space.

    Thus perhaps you can get your code “working” with just

    lib.ftrScanOpenDevice.restype = c_void_p
    

    That is not to say that you shouldn’t define the argument and return types for all functions – you should, otherwise they will follow just the default argument promotion rules of the C language, and work… or totally fail to work, depending on whether the function’s prototype itself is compatible with default argument promotions. For example any function that accepts floats as arguments cannot be called correctly without defining the prototype.