[plum] Tutorial: Dimensioned Array Structure Member

This tutorial shows how to use the dimmed_member() member definition within a structure definition to associate a dimensions member and an array member to one another. During unpack operations, the association causes the the unpacked dimension member to control the unpacking of the array member. The association allows the dimension member to be left unspecified and have it automatically derived from the array value.

The examples shown on this page require the following setup:

>>> from plum.array import ArrayX
>>> from plum.bigendian import uint8
>>> from plum.structure import dimmed_member, member, Structure

One-Dimensional Arrays

To demonstrate, let’s start by examining the example below. The example shows the simplest use case, an array with a single dimension. Since its a single dimension, let’s choose the array dimension member name to be count with the format as uint8. After the type annotation, use the Member class to define a member to hold the dimensions giving it an integer transform for the fmt.

For the associated array member, use the dimmed_member() function to associate it to the dims member and specify an undimensioned array transform for the array member’s fmt.

The bookend member exists in the example to show that when the structure is unpacked, the dimensions count member restricts the array and leaves the last byte for the bookend:

>>> array = ArrayX(fmt=uint8)  # greedy array
>>>
>>> class Struct1(Structure):
...     count: int = member(fmt=uint8, compute=True)
...     array: list = dimmed_member(dims=count, fmt=array)
...     bookend: int = member(fmt=uint8)
...
>>> Struct1.unpack(b'\x02\x01\x02\x99').dump()
+--------+---------+-------+-------+---------------------+
| Offset | Access  | Value | Bytes | Format              |
+--------+---------+-------+-------+---------------------+
|        |         |       |       | Struct1 (Structure) |
| 0      | count   | 2     | 02    | uint8               |
|        | array   |       |       | List[int]           |
| 1      |   [0]   | 1     | 01    | uint8               |
| 2      |   [1]   | 2     | 02    | uint8               |
| 3      | bookend | 153   | 99    | uint8               |
+--------+---------+-------+-------+---------------------+

The association also allows the size member to be left unspecified when instantiating the structure. In that case, the size member takes on the length of the array:

>>> buffer, dump = Struct1(array=[1, 2], bookend=0x99).ipack_and_dump()
>>> buffer.hex()
'02010299'
>>> print(dump)
+--------+---------+-------+-------+---------------------+
| Offset | Access  | Value | Bytes | Format              |
+--------+---------+-------+-------+---------------------+
|        |         |       |       | Struct1 (Structure) |
| 0      | count   | 2     | 02    | uint8               |
|        | array   |       |       | List[int]           |
| 1      |   [0]   | 1     | 01    | uint8               |
| 2      |   [1]   | 2     | 02    | uint8               |
| 3      | bookend | 153   | 99    | uint8               |
+--------+---------+-------+-------+---------------------+

The association causes the count member to be updated when setting a new array value using the array attribute:

>>> struct1 = Struct1(array=[1, 2], bookend=123)
>>> struct1.count
2
>>>
>>> struct1.array = [1, 2, 3, 4, 5]
>>> struct1.count
5

Multi-Dimensional Arrays

Implementing multiple dimensions only adds slightly more complexity. Instead of supplying a simple integer transform for the dimensions member’s fmt, create a single dimension array subclass to hold the dimensions (e.g. for a two dimensional array, specify dims=(2,), for three dimensions dims=(3,), etc). Everything else remains the same:

>>> dims = ArrayX(fmt=uint8, dims=(2,))
>>>
>>> class Struct2(Structure):
...     dims: list = member(fmt=dims, compute=True)
...     array: list = dimmed_member(dims=dims, fmt=array)
...
>>> Struct2(array=[[1, 2], [3, 4]]).dump()
+--------+---------+-------+-------+---------------------+
| Offset | Access  | Value | Bytes | Format              |
+--------+---------+-------+-------+---------------------+
|        |         |       |       | Struct2 (Structure) |
|        | dims    |       |       | List[int]           |
| 0      |   [0]   | 2     | 02    | uint8               |
| 1      |   [1]   | 2     | 02    | uint8               |
|        | array   |       |       | List[int]           |
|        |   [0]   |       |       |                     |
| 2      |     [0] | 1     | 01    | uint8               |
| 3      |     [1] | 2     | 02    | uint8               |
|        |   [1]   |       |       |                     |
| 4      |     [0] | 3     | 03    | uint8               |
| 5      |     [1] | 4     | 04    | uint8               |
+--------+---------+-------+-------+---------------------+

Ignoring Members

Both member() and dimmed_member() functions support marking the member to be ignored during comparisons:

>>> class Struct1Ignore(Structure):
...     count: int = member(fmt=uint8, compute=True, ignore=True)
...     array: list = dimmed_member(dims=count, fmt=array, ignore=True)
...     bookend: int = member(fmt=uint8)
...
>>> struct1 = Struct1Ignore.unpack(b'\x02\x01\x02\x99')
>>> struct1.dump()
+--------+---------+-------+-------+---------------------------+
| Offset | Access  | Value | Bytes | Format                    |
+--------+---------+-------+-------+---------------------------+
|        |         |       |       | Struct1Ignore (Structure) |
| 0      | count   | 2     | 02    | uint8                     |
|        | array   |       |       | List[int]                 |
| 1      |   [0]   | 1     | 01    | uint8                     |
| 2      |   [1]   | 2     | 02    | uint8                     |
| 3      | bookend | 153   | 99    | uint8                     |
+--------+---------+-------+-------+---------------------------+
>>>
>>> struct1 == Struct1Ignore(array=[], bookend=0x99)
True

Defaulting Array Member

Specifying a default value for the array in its member definition allows the structure to be instantiated or packed without a value for the array member:

>>> class Struct1(Structure):
...     count: int = member(fmt=uint8, compute=True)
...     array: list = dimmed_member(dims=count, fmt=array, default=(0, 1))
...     bookend: int = member(fmt=uint8)
...
>>> struct = Struct1(bookend=0x99)
>>> struct.dump()
+--------+---------+-------+-------+---------------------+
| Offset | Access  | Value | Bytes | Format              |
+--------+---------+-------+-------+---------------------+
|        |         |       |       | Struct1 (Structure) |
| 0      | count   | 2     | 02    | uint8               |
|        | array   |       |       | List[int]           |
| 1      |   [0]   | 0     | 00    | uint8               |
| 2      |   [1]   | 1     | 01    | uint8               |
| 3      | bookend | 153   | 99    | uint8               |
+--------+---------+-------+-------+---------------------+
>>>
>>> struct.ipack()
b'\x02\x00\x01\x99'