Skip to content

Commit 5915b3e

Browse files
committed
docs: add documentation for simple and tagged unions
1 parent 25d7e5b commit 5915b3e

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

SKILL.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ from libdestruct import (
3838
array_of, # fixed-size array field descriptor
3939
enum_of, # enum field descriptor
4040
bitfield_of, # bitfield descriptor
41+
union, # union annotation type
42+
union_of, # plain union field descriptor
43+
tagged_union, # tagged union field descriptor
4144
offset, # explicit field offset
4245
size_of, # get size in bytes of any type/instance/field
4346
)
@@ -217,6 +220,31 @@ class flags_t(struct):
217220

218221
Consecutive bitfields with the same backing type are packed together. The struct above is 4 bytes total, not 16.
219222

223+
### Unions
224+
225+
```python
226+
from libdestruct.common.union import union, union_of, tagged_union
227+
228+
# Plain union — all variants overlaid at the same offset
229+
class packet_t(struct):
230+
data: union = union_of({"i": c_int, "f": c_float, "l": c_long})
231+
232+
pkt = lib.inflate(packet_t, 0)
233+
pkt.data.i.value # interpret as int
234+
pkt.data.f.value # interpret as float (same bytes)
235+
236+
# Tagged union — discriminator selects the active variant
237+
class message_t(struct):
238+
type: c_int
239+
payload: union = tagged_union("type", {
240+
0: c_int,
241+
1: c_float,
242+
2: point_t, # struct variants work too
243+
})
244+
```
245+
246+
The discriminator field must appear before the union. The union size is the max of all variant sizes. Struct variant fields are accessible directly: `msg.payload.x.value`. Use `.variant` to get the raw variant object. Unknown discriminator values raise `ValueError`.
247+
220248
### Explicit Field Offsets
221249

222250
```python

docs/advanced/tagged_unions.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Unions
2+
3+
libdestruct supports both **plain unions** (C-style, all variants overlaid) and **tagged unions** (discriminated, one active variant selected by another field).
4+
5+
## Plain Unions
6+
7+
Use `union_of({"name": type, ...})` to declare a union where all variants share the same memory. Access each interpretation by name:
8+
9+
```python
10+
from libdestruct import struct, c_int, c_float, c_long, inflater
11+
from libdestruct.common.union import union, union_of
12+
13+
class packet_t(struct):
14+
data: union = union_of({
15+
"i": c_int,
16+
"f": c_float,
17+
"l": c_long,
18+
})
19+
```
20+
21+
All variants are inflated at the same offset. Reading one reinterprets the underlying bytes:
22+
23+
```python
24+
import struct as pystruct
25+
26+
memory = pystruct.pack("<f", 3.14) + b"\x00" * 4 # pad to 8 bytes (c_long size)
27+
pkt = packet_t.from_bytes(memory)
28+
29+
print(pkt.data.f.value) # 3.140000104904175
30+
print(pkt.data.i.value) # 1078523331 (same bytes as int)
31+
```
32+
33+
Writing to any variant updates the shared memory:
34+
35+
```python
36+
memory = bytearray(8)
37+
lib = inflater(memory)
38+
pkt = lib.inflate(packet_t, 0)
39+
40+
pkt.data.i.value = 42
41+
# pkt.data.f.value now reflects those same bytes interpreted as float
42+
```
43+
44+
Struct variants work too — their fields are accessible directly:
45+
46+
```python
47+
class point_t(struct):
48+
x: c_int
49+
y: c_int
50+
51+
class data_t(struct):
52+
raw: union = union_of({"val": c_long, "point": point_t})
53+
54+
d = data_t.from_bytes(pystruct.pack("<ii", 10, 20))
55+
print(d.raw.point.x.value) # 10
56+
print(d.raw.point.y.value) # 20
57+
```
58+
59+
## Tagged Unions
60+
61+
libdestruct also supports tagged (discriminated) unions, where the active variant is selected at runtime by another field in the same struct.
62+
63+
## Defining a Tagged Union
64+
65+
Use `tagged_union(discriminator, variants)` to declare a union field in a struct. The `discriminator` is the name of another field whose value selects the variant, and `variants` maps discriminator values to types:
66+
67+
```python
68+
from libdestruct import struct, c_int, c_float, c_long, inflater
69+
from libdestruct.common.union import tagged_union, union
70+
71+
class message_t(struct):
72+
type: c_int
73+
payload: union = tagged_union("type", {
74+
0: c_int,
75+
1: c_float,
76+
2: c_long,
77+
})
78+
```
79+
80+
## Reading Values
81+
82+
The union automatically inflates the correct variant based on the discriminator:
83+
84+
```python
85+
import struct as pystruct
86+
87+
# type=0 selects c_int variant
88+
memory = pystruct.pack("<i", 0) + pystruct.pack("<i", 42) + b"\x00" * 4
89+
msg = message_t.from_bytes(memory)
90+
print(msg.payload.value) # 42
91+
92+
# type=1 selects c_float variant
93+
memory = pystruct.pack("<i", 1) + pystruct.pack("<f", 3.14) + b"\x00" * 4
94+
msg = message_t.from_bytes(memory)
95+
print(msg.payload.value) # 3.140000104904175
96+
```
97+
98+
## Writing Values
99+
100+
You can write to the active variant:
101+
102+
```python
103+
memory = bytearray(12)
104+
lib = inflater(memory)
105+
msg = lib.inflate(message_t, 0)
106+
107+
msg.payload.value = 100
108+
print(msg.payload.value) # 100
109+
```
110+
111+
## Struct Variants
112+
113+
When a variant is a struct type, its fields are accessible directly through the union:
114+
115+
```python
116+
class point_t(struct):
117+
x: c_int
118+
y: c_int
119+
120+
class packet_t(struct):
121+
type: c_int
122+
data: union = tagged_union("type", {
123+
0: c_int,
124+
1: point_t,
125+
})
126+
127+
memory = pystruct.pack("<i", 1) + pystruct.pack("<ii", 10, 20)
128+
pkt = packet_t.from_bytes(memory)
129+
130+
print(pkt.data.x.value) # 10
131+
print(pkt.data.y.value) # 20
132+
```
133+
134+
## Size
135+
136+
The size of a union field is the **maximum** size of all its variants. This ensures the struct layout is correct regardless of which variant is active:
137+
138+
```python
139+
from libdestruct import size_of
140+
141+
class msg_t(struct):
142+
type: c_int # 4 bytes
143+
payload: union = tagged_union("type", {
144+
0: c_int, # 4 bytes
145+
1: c_long, # 8 bytes
146+
})
147+
148+
size_of(msg_t) # 12 (4 + max(4, 8))
149+
```
150+
151+
## Accessing the Variant
152+
153+
Use the `variant` property to get the active variant object directly:
154+
155+
```python
156+
msg = message_t.from_bytes(data)
157+
variant_obj = msg.payload.variant # the raw c_int, c_float, etc.
158+
```
159+
160+
## Error Handling
161+
162+
If the discriminator value doesn't match any variant, a `ValueError` is raised:
163+
164+
```python
165+
class msg_t(struct):
166+
type: c_int
167+
payload: union = tagged_union("type", {0: c_int})
168+
169+
# type=99 has no matching variant
170+
memory = pystruct.pack("<i", 99) + b"\x00" * 4
171+
msg_t.from_bytes(memory) # raises ValueError
172+
```
173+
174+
!!! info
175+
The discriminator field must appear **before** the union field in the struct definition, since fields are inflated in order.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,5 @@ nav:
6666
- C Struct Parser: advanced/c_parser.md
6767
- Forward References: advanced/forward_refs.md
6868
- Hex Dump: advanced/hexdump.md
69+
- Unions: advanced/tagged_unions.md
6970
- Field Offsets: advanced/offset.md

0 commit comments

Comments
 (0)