Python Struct Assign Bitfield with typesafety

38 Views Asked by At

Consider I have the following struct:

import ctypes

class MyStruct(ctypes.BigEndianStructure):
    # ! These typehints are added so that the IDE recognizes the fields of the Structure
    field_size1: ctypes.c_uint8
    field_size7: ctypes.c_uint8
    field_size8: ctypes.c_uint8

    _pack_ = 1
    _fields_ = [
        ("field_size1", ctypes.c_uint8, 1),
        ("field_size7", ctypes.c_uint8, 7),
        ("field_size8", ctypes.c_uint8, 8),
    ]

I am able to assign the bits in this struct from Python types and get the bytes I expect.

s = MyStruct()
s.field_size1 = 1
s.field_size7 = 2
s.field_size8 = 3

I get the mypy linting error:

Incompatible types in assignment (expression has type "int", variable has type "c_uint8") Mypy(assignment)

However, if I try to use the ctype, the program crashes (ie the message below isn't printed):

s = MyStruct()
s.field_size1 = ctypes.c_uint8(1)
print("completed")

Which is what I believe I would expect. Trying to shove the 8bits into a 1bit length wouldn't make sense.

Is there a correct way to use the ctypes types when assigning bitfields to keep both Python and mypy happy without 'type ignores'

1

There are 1 best solutions below

1
Adehad On BEST ANSWER

Thanks to @Mark Tolonen

I was able to better understand my problem and come up with this answer.

These ctypes.Structure objects automatically handle the conversion to native Python types.

With the original example

>> print(type(s.field_size1))
<class 'int'>
>> s.field_size7 = -20
>> print(s.field_size7) # We also get overflow behaviour as we expect from C
206

My confusion stemmed from my actual use case that involved Arrays, as these do not natively get converted. Hence when I was working with my original application I had naively also applied the sample logic to arrays to the other fields.

import ctypes

ARRAY_SIZE = 5
UINT8x5_ARRAY = ctypes.c_uint8 * ARRAY_SIZE


class MyStructWithArray(ctypes.BigEndianStructure):

    _pack_ = 1
    _fields_ = [
        ("field_size1", ctypes.c_uint8, 1),
        ("field_size7", ctypes.c_uint8, 7),
        ("field_size8x5", UINT8x5_ARRAY),
    ]

Now

>> s = MyStructWithArray(1,2,UINT8x5_ARRAY(*list(range(5,5+ARRAY_SIZE))))
>> print(type(s.field_size1))
<class 'int'>
>> print(type(s.field_size8x5))
<class '__main__.c_ubyte_Array_5'>

Now how best to type hint an array is the question I would need to answer.

This also is as straightforward as direct assignment with a tuple, and tuples can have a specific length in the type hint:

import ctypes

ARRAY_SIZE = 5
UINT8x5_ARRAY = ctypes.c_uint8 * ARRAY_SIZE

class MyStructWithArray(ctypes.BigEndianStructure):
    # ! These typehints are added so that the IDE recognizes the fields of the Structure
    field_size1: int
    field_size7: int
    field_size8x5: tuple[int, int, int, int, int]

    _pack_ = 1
    _fields_ = [
        ("field_size1", ctypes.c_uint8, 1),
        ("field_size7", ctypes.c_uint8, 7),
        ("field_size8x5", UINT8x5_ARRAY),
    ]


>> s.field_size8x5 = (0,1,2,3,4)
>> print(list(s.field_size8x5)) # printing is trivial as can also be casted to a native type
[0,1,2,3,4]
>> print(s.field_size8x5[6]) # mypy[misc]: Tuple index out of range