3.2.2.2. Struct#
A struct describes a finite collection of named fields. In contrast to a sequence, a struct
utilizes Python classes as its model. The annotation feature in Python enables the definition of
custom types as annotations, enabling this special struct class to create a model solely based on
class annotations. Additionally, it generates a dataclass of the provided model, offering a
standardized string representation.
Several differences exist between a Sequence and a
Struct, with the most significant ones highlighted below:
Sequence |
Struct |
|
|---|---|---|
Model Type |
dict |
type |
Inheritance |
No |
Yes |
Attribute Access |
|
|
Unpacked Type (also needed to pack) |
dict [*] |
instance of model |
Documentation |
No |
Yes |
As evident from the comparison, the Struct class introduces new features such as
inheritance and documentation support. It’s crucial to note that inheritance uses
struct types exclusively.
The Sequence class implements a specific process for creating an internal representation
of the given model. The Struct class enhances this process by handling default values, replacing
types for documentation purposes, or removing annotation fields directly from the model. Additionally,
this class adds __struct__ to the model afterward.
Implementation Note
If you decide to use the annotation feature from the __future__ module, it is necessary to
enable S_EVAL_ANNOTATIONS since it “Stringizes” all annotations. inspect then
evaluates all strings, introducing a potential security risk. Exercise with caution when evaluating code!
Specifying structs is as simple as defining Python Classes:
>>> @struct
... class BaseFormat:
... magic: b"MAGIC"
... a: uint8
...
Internally, a representation with all required fields and their corresponding names is
created. As b"MAGIC" or uint8 are instances of types, the type replacement
for documentation purposes should be enabled, as shown in Special Methods for Struct-Like objects.
As described above, this class introduces an easy-to-use inheritance system using the method resolution order of Python:
>>> @struct
... class Format(BaseFormat):
... b: uint32
... c: uint16
...
>>> list(Format.__struct__.get_members())
['magic', 'a', 'b', 'c']
Inherited fields are copied into the derived structure as new field wrappers.
Mutating derived field metadata, for example changing byte order on
Format.__struct__, will not mutate the base structure. If several base
classes provide the same field name, Python MRO precedence decides which field
is imported. Fields declared directly on the child class override inherited
fields in-place, keeping the binary layout position stable.
Byte order and architecture configured on the child structure apply to fields declared by the child. Inherited field wrappers keep their own explicit or base-derived configuration unless the child overrides the field declaration.
If a base field has a default value and the child adds required fields, the child fields are made keyword-only so the binary field order can remain unchanged while still satisfying dataclass constructor rules.
Inherited fields can be removed from the derived binary layout explicitly with
RemoveField:
>>> @struct
... class Child(BaseFormat):
... magic: RemoveField()
... b: uint32
...
This removes magic from Child only; BaseFormat remains unchanged.
Programmers Note
As the Struct class is a direct subclass of Sequence, nesting is supported
by default. That means, so-called anonymous inner structs can be defined within a class
definition.
>>> @struct
... class Format:
... a: uint32
... b: {"c": uint8}
...
It is not recommended to use this technique as the inner structs can’t be used anywhere else.
Anonymous inner union definitions are tricky and are not officially supported yet. There are
workarounds to that problem, which are discussed in the API documentation of Sequence.