pythonctypesbit-fields

CTypes bitfield sets whole byte


I have the following structure:

class KeyboardModifiers(Structure):
    _fields_ = [
        ('left_control', c_bool, 1),
        ('right_control', c_bool, 1),
        ('left_shift', c_bool, 1),
        ('right_shift', c_bool, 1),
        ('left_alt', c_bool, 1),
        ('right_alt', c_bool, 1),
        ('left_meta', c_bool, 1),
        ('right_meta', c_bool, 1),
        ('left_super', c_bool, 1),
        ('right_super', c_bool, 1),
        ('left_hyper', c_bool, 1),
        ('right_hyper', c_bool, 1),
    ]

It represents a structure returned by a C function, the fields are properly set and their values returned, issue comes when setting a field. For example, if I were to do something like:

my_keyboard_mods.left_shift = True

The first 8 fields would add be set to True, similarly with the next 8. What seems to happen is that it sets the value for the whole byte not respecting the bitfield. My question is:

  1. I am doing something wrong, so what's wrong?
  2. This is a bug with ctypes, is there a workaround?

Thanks.


Solution

  • It results from using c_bool. I'm not getting the whole byte set to 1, but bit set resulting in a non-zero byte results in a value of 1, not 0xFF as described. It may be an OS or implementation detail. For me it seems to normalize the boolean value to 0/1 based on zero/non-zero value of the byte. For your implementation it may be normalizing to 0/FF. FYI: make sure to post the entire code that reproduces the problem next time.

    from ctypes import *
    
    class KeyboardModifiers(Structure):
        _fields_ = [
            ('left_control', c_bool, 1),
            ('right_control', c_bool, 1),
            ('left_shift', c_bool, 1),
            ('right_shift', c_bool, 1),
            ('left_alt', c_bool, 1),
            ('right_alt', c_bool, 1),
            ('left_meta', c_bool, 1),
            ('right_meta', c_bool, 1),
            ('left_super', c_bool, 1),
            ('right_super', c_bool, 1),
            ('left_hyper', c_bool, 1),
            ('right_hyper', c_bool, 1),
        ]
    
    k = KeyboardModifiers()
    k.left_control = True
    k.left_shift = True
    k.right_hyper = True
    print(bytes(k))
    

    Expected b'\x05\0x08' but got the following output due to c_bool implementation treating non-zero bytes as 1:

    b'\x01\x01'
    

    Use c_ubyte or c_ushort instead. Note that using a type larger than two bytes would make the structure longer than two bytes if size is a concern:

    from ctypes import *
    
    class KeyboardModifiers(Structure):
        _fields_ = [
            ('left_control', c_ubyte, 1),
            ('right_control', c_ubyte, 1),
            ('left_shift', c_ubyte, 1),
            ('right_shift', c_ubyte, 1),
            ('left_alt', c_ubyte, 1),
            ('right_alt', c_ubyte, 1),
            ('left_meta', c_ubyte, 1),
            ('right_meta', c_ubyte, 1),
            ('left_super', c_ubyte, 1),
            ('right_super', c_ubyte, 1),
            ('left_hyper', c_ubyte, 1),
            ('right_hyper', c_ubyte, 1),
        ]
    
    k = KeyboardModifiers()
    k.left_control = True
    k.left_shift = True
    k.right_hyper = True
    print(bytes(k))
    

    Output:

    b'\x05\x08'
    

    Edit: I see now that each field seems to refer to bit 0 of its containing byte, so even though the byte value of the structure only ever has 0 or 1, the values of the field bits do all change to True when displayed. In any case the fix is the same...don't use c_bool:

    from ctypes import *
    
    class KeyboardModifiers(Structure):
        _fields_ = [
            ('left_control', c_bool, 1),
            ('right_control', c_bool, 1),
            ('left_shift', c_bool, 1),
            ('right_shift', c_bool, 1),
            ('left_alt', c_bool, 1),
            ('right_alt', c_bool, 1),
            ('left_meta', c_bool, 1),
            ('right_meta', c_bool, 1),
            ('left_super', c_bool, 1),
            ('right_super', c_bool, 1),
            ('left_hyper', c_bool, 1),
            ('right_hyper', c_bool, 1),
        ]
        def __repr__(self):
            return (f'KeyboardModifiers(\n'
                    f'  left_control={self.left_control},\n'
                    f'  right_control={self.right_control},\n'
                    f'  left_shift={self.left_shift},\n'
                    f'  right_shift={self.right_shift},\n'
                    f'  left_alt={self.left_alt},\n'
                    f'  right_alt={self.right_alt},\n'
                    f'  left_meta={self.left_meta},\n'
                    f'  right_meta={self.right_meta},\n'
                    f'  left_super={self.left_super},\n'
                    f'  right_super={self.right_super},\n'
                    f'  left_hyper={self.left_hyper},\n'
                    f'  right_hyper={self.right_hyper})')
    
    k = KeyboardModifiers()
    k.left_control = True
    print(bytes(k))
    print(k)
    

    Output:

    b'\x01\x00'
    KeyboardModifiers(
      left_control=True,
      right_control=True,
      left_shift=True,
      right_shift=True,
      left_alt=True,
      right_alt=True,
      left_meta=True,
      right_meta=True,
      left_super=False,
      right_super=False,
      left_hyper=False,
      right_hyper=False)