pythonpython-3.xstructctypes

How to resize variable sized buffer with c types python?


import struct
import ctypes as ct

def read_buffer(data):
    size = struct.unpack('<h', data[64:66])[0]
    class TableData(ct.Structure):
        _fields_ = [('delta', ct.c_float),
                    ('x', ct.c_float)]
        def __repr__(self):
            return f'TableData(delta={self.delta}, x={self.x})'

    class TableParameters(ct.Structure):
        _fields_ = [('parameter1', ct.c_uint16 * 30),
                    ('parameter2', ct.c_float),
                    ('size', ct.c_uint16),
                    ('parameter3', ct.c_uint16),
                    ('parameter4', ct.c_float),
                    ('parameter5', ct.c_float),
                    ('Table', TableData * size)]
        def __repr__(self):
            return (f'TableParameters(parameter1={list(self.parameter1)},'
                    f'parameter2={self.parameter2},'
                    f'size={self.size},'
                    f'parameter3={self.parameter3},'
                    f'parameter4={self.parameter4},'
                    f'parameter5={self.parameter5},'
                    f'Table={list(self.Table)}')

    return TableParameters.from_buffer_copy(data)

data = struct.pack('<30HfHHff4f',*range(30),1.125,2,8,2.5,3.25,4.75,5.5,6.25,7.75)
a = read_buffer(data)
print (a.parameter1[0])
print (a.parameter1[3])
print (a.parameter5)
print ('------------')
print (len(a.Table))
a.size = 10
print (len(a.Table))

Output:

0
3
3.25        
------------
2
2

This is a structure with a variable sized buffer. The field named 'size' is supposed to determine the size of the field named 'Table'. When reading the data, it works fine. But when trying to modify the size of 'Table', it doesn't work or at least it's not as straightforward as one would think. On the output, we can see that the size of 'Table' did not change and there is no way of adding data points to it.


Solution

  • This uses ctypes.resize to increase/decrease the size of the TableParameters structure, and adds some managed properties to make using the structure more seamless. The managed property is needed because the Table array is fixed size. Increasing the size of the structure makes the structure take more memory, but it doesn't increase the size of the Table field, so the property aliases the extra memory to a properly-sized Table array.

    Comments below. Let me know if you need any clarification:

    import struct
    import ctypes as ct
    
    class TableData(ct.Structure):
        _fields_ = [('delta', ct.c_float),
                    ('x', ct.c_float)]
    
        def __repr__(self):
            return f'(TableData(delta={self.delta}, x={self.x})'
    
    class TableParameters(ct.Structure):
        _fields_ = [('parameter1', ct.c_uint16 * 30),
                    ('parameter2', ct.c_float),
                    ('_size', ct.c_uint16),        # internal managed variable
                    ('parameter3', ct.c_uint16),
                    ('parameter4', ct.c_float),
                    ('parameter5', ct.c_float),
                    ('_table', TableData * 0)]     # internal managed variable
    
        def __init__(self, p1=(0,)*30, p2=0.0, p3=0, p4=0.0, p5=0.0, data=None):
            self.parameter1[:] = p1
            self.parameter2 = p2
            self.parameter3 = p3
            self.parameter4 = p4
            self.parameter5 = p5
            if data is None:
                data = []
            self.Table = data  # uses Table.setter property
    
        @property
        def size(self):
            return self._size
    
        @size.setter
        def size(self, value):
            # Re-size the structure so ct.sizeof() represents correct structure size
            self._size = value
            ct.resize(self, ct.sizeof(TableParameters) + ct.sizeof(TableData) * self._size)
            
        @property
        def Table(self):
            # Table property returns the array of TableData created from the
            # extra data at the end of the TableParameters structure.
            # Note that .from_buffer() *shares* the memory...its not a copy
            return (TableData * self.size).from_buffer(self, TableParameters._table.offset)
    
        @Table.setter
        def Table(self, data):
            # 
            self.size = len(data)  # uses size.setter property to change the structure size.
            self.Table[:] = data   # Uses Table property to initialize the data in the re-sized table.
                                   # Recall the returned Table is *shared* memory.
    
        @classmethod
        def readbuffer(cls, source):
            # Helper class method to build a TableParameters from a byte buffer.
            a = TableParameters.from_buffer_copy(source)
            ct.resize(a, ct.sizeof(TableParameters) + ct.sizeof(TableData) * a.size)
            a.Table[:] = (TableData * a.size).from_buffer_copy(source[ct.sizeof(TableParameters):])
            return a
    
        def __eq__(self, other):
            # Helper method to check for equality between two TableParameters.
            return bytes(self) == bytes(other)
    
        def __repr__(self):
            return f'''\
    TableParameters(parameter1={list(self.parameter1)},
                    parameter2={self.parameter2}, parameter3={self.parameter3}, parameter4={self.parameter4}, parameter5={self.parameter5},
                    Table={list(self.Table)})'''
    
    data = struct.pack('<30HfHHff4f',*range(30), 1.125, 2, 8, 2.5, 3.25, 4.75, 5.5, 6.25, 7.75)
    a = TableParameters.readbuffer(data) # build from a bytes object
    # build from a constructor
    b = TableParameters(range(30), 1.125, 8, 2.5, 3.25, data=[(4.75, 5.5),(6.25 , 7.75)])
    print(a)
    print(b)
    print(a == b)
    b.Table = (1,2), (3,4), (5,6)  # change the table
    print(b)
    b.size = 4  # increase the size manually
    print(b)    # Note the last entry is uninitialized after the manual increase
    b.Table[3] = 7,8 # initialize it.
    print(b)
    

    Output:

    TableParameters(parameter1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
                    parameter2=1.125, parameter3=8, parameter4=2.5, parameter5=3.25,
                    Table=[(TableData(delta=4.75, x=5.5), (TableData(delta=6.25, x=7.75)])
    TableParameters(parameter1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
                    parameter2=1.125, parameter3=8, parameter4=2.5, parameter5=3.25,
                    Table=[(TableData(delta=4.75, x=5.5), (TableData(delta=6.25, x=7.75)])
    True
    TableParameters(parameter1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
                    parameter2=1.125, parameter3=8, parameter4=2.5, parameter5=3.25,
                    Table=[(TableData(delta=1.0, x=2.0), (TableData(delta=3.0, x=4.0), (TableData(delta=5.0, x=6.0)])
    TableParameters(parameter1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
                    parameter2=1.125, parameter3=8, parameter4=2.5, parameter5=3.25,
                    Table=[(TableData(delta=1.0, x=2.0), (TableData(delta=3.0, x=4.0), (TableData(delta=5.0, x=6.0), (TableData(delta=7.349474681214527e+28, x=3.1010056193481227e-15)])
    TableParameters(parameter1=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
                    parameter2=1.125, parameter3=8, parameter4=2.5, parameter5=3.25,
                    Table=[(TableData(delta=1.0, x=2.0), (TableData(delta=3.0, x=4.0), (TableData(delta=5.0, x=6.0), (TableData(delta=7.0, x=8.0)])