Skip to content

Commit dd6df6b

Browse files
committed
feat: add support for alignment in structs
1 parent ee22756 commit dd6df6b

7 files changed

Lines changed: 448 additions & 2 deletions

File tree

SKILL.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ from libdestruct import (
4343
tagged_union, # tagged union field descriptor
4444
offset, # explicit field offset
4545
size_of, # get size in bytes of any type/instance/field
46+
alignment_of, # get natural alignment of any type/instance
4647
)
4748
```
4849

@@ -245,6 +246,32 @@ class message_t(struct):
245246

246247
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`.
247248

249+
### Struct Alignment
250+
251+
```python
252+
# Default: packed (no padding)
253+
class packed_t(struct):
254+
a: c_char
255+
b: c_int
256+
# size: 5
257+
258+
# Aligned: natural C alignment with padding
259+
class aligned_t(struct):
260+
_aligned_ = True
261+
a: c_char
262+
b: c_int
263+
# size: 8 (1 + 3 padding + 4)
264+
265+
alignment_of(c_int) # 4
266+
alignment_of(aligned_t) # 4 (max member alignment)
267+
268+
# Custom alignment width
269+
class wide_t(struct):
270+
_aligned_ = 16
271+
a: c_int
272+
# size: 16, alignment: 16
273+
```
274+
248275
### Explicit Field Offsets
249276

250277
```python

docs/advanced/alignment.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Struct Alignment
2+
3+
By default, libdestruct structs are **packed** — fields are placed sequentially with no padding, like C structs with `__attribute__((packed))`.
4+
5+
You can opt into natural alignment (matching standard C struct layout) by setting `_aligned_ = True` on your struct:
6+
7+
## Enabling Alignment
8+
9+
```python
10+
from libdestruct import struct, c_char, c_int, c_long, size_of
11+
12+
class packed_t(struct):
13+
a: c_char
14+
b: c_int
15+
16+
size_of(packed_t) # 5 (1 + 4, no padding)
17+
18+
class aligned_t(struct):
19+
_aligned_ = True
20+
a: c_char
21+
b: c_int
22+
23+
size_of(aligned_t) # 8 (1 + 3 padding + 4)
24+
```
25+
26+
## Alignment Rules
27+
28+
When `_aligned_ = True`:
29+
30+
1. **Field alignment**: Each field is placed at an offset that is a multiple of its natural alignment (1 for `c_char`, 2 for `c_short`, 4 for `c_int`/`c_float`, 8 for `c_long`/`c_double`/`ptr`).
31+
2. **Tail padding**: The struct's total size is rounded up to a multiple of the struct's alignment (the maximum alignment of any member).
32+
33+
```python
34+
class mixed_t(struct):
35+
_aligned_ = True
36+
a: c_char # offset 0, size 1
37+
b: c_short # offset 2 (aligned to 2), size 2
38+
c: c_char # offset 4, size 1
39+
d: c_int # offset 8 (aligned to 4), size 4
40+
e: c_char # offset 12, size 1
41+
f: c_long # offset 16 (aligned to 8), size 8
42+
43+
size_of(mixed_t) # 24 (padded to 8-byte boundary)
44+
```
45+
46+
## Reading Aligned Structs
47+
48+
```python
49+
import struct as pystruct
50+
from libdestruct import inflater
51+
52+
class header_t(struct):
53+
_aligned_ = True
54+
flags: c_char
55+
size: c_int
56+
57+
# flags at offset 0, 3 bytes padding, size at offset 4
58+
memory = pystruct.pack("<b", 0x01) + b"\x00" * 3 + pystruct.pack("<i", 1024)
59+
header = header_t.from_bytes(memory)
60+
61+
print(header.flags.value) # 1
62+
print(header.size.value) # 1024
63+
```
64+
65+
## Nested Aligned Structs
66+
67+
Alignment is respected for nested structs too. A nested struct's alignment equals the maximum alignment of its own members:
68+
69+
```python
70+
class inner_t(struct):
71+
_aligned_ = True
72+
a: c_char
73+
b: c_int
74+
75+
class outer_t(struct):
76+
_aligned_ = True
77+
x: c_char
78+
inner: inner_t # aligned to 4 (inner's max member alignment)
79+
80+
size_of(inner_t) # 8
81+
size_of(outer_t) # 12 (1 + 3 padding + 8)
82+
```
83+
84+
## Custom Alignment Width
85+
86+
Set `_aligned_` to an integer to enforce a minimum alignment boundary. Fields still use natural alignment, but the struct's total size is padded to the specified boundary:
87+
88+
```python
89+
class wide_t(struct):
90+
_aligned_ = 16
91+
a: c_int
92+
93+
size_of(wide_t) # 16 (4 bytes data, padded to 16-byte boundary)
94+
alignment_of(wide_t) # 16
95+
```
96+
97+
`_aligned_ = True` is equivalent to using the maximum natural member alignment (up to 8).
98+
99+
## Interaction with Explicit Offsets
100+
101+
When a field has an explicit `offset()`, alignment does **not** override the specified position. Alignment resumes for subsequent fields without explicit offsets:
102+
103+
```python
104+
from libdestruct import offset
105+
106+
class s_t(struct):
107+
_aligned_ = True
108+
a: c_char
109+
b: c_int = offset(3) # placed at offset 3, not rounded to 4
110+
c: c_int # aligned normally after b
111+
112+
size_of(s_t) # 7 (3 + 4)
113+
```
114+
115+
## alignment_of()
116+
117+
Use `alignment_of()` to query the alignment requirement of any type:
118+
119+
```python
120+
from libdestruct import alignment_of, c_int, c_long
121+
122+
alignment_of(c_int) # 4
123+
alignment_of(c_long) # 8
124+
alignment_of(aligned_t) # max member alignment
125+
alignment_of(packed_t) # 1 (packed structs have alignment 1)
126+
```

libdestruct/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
from libdestruct.common.ptr.ptr import ptr
2121
from libdestruct.common.struct import ptr_to, ptr_to_self, struct
2222
from libdestruct.common.union import tagged_union, union, union_of
23-
from libdestruct.common.utils import size_of
23+
from libdestruct.common.utils import alignment_of, size_of
2424
from libdestruct.libdestruct import inflate, inflater
2525

2626
__all__ = [
2727
"Resolver",
28+
"alignment_of",
2829
"array",
2930
"array_of",
3031
"bitfield_of",

libdestruct/common/struct/struct_impl.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from libdestruct.common.obj import obj
1919
from libdestruct.common.struct import struct
2020
from libdestruct.common.type_registry import TypeRegistry
21-
from libdestruct.common.utils import iterate_annotation_chain, size_of
21+
from libdestruct.common.utils import _align_offset, alignment_of, iterate_annotation_chain, size_of
2222

2323

2424
class struct_impl(struct):
@@ -79,8 +79,12 @@ def _inflate_struct_attributes(
7979
) -> None:
8080
current_offset = 0
8181
bf_tracker = BitfieldTracker()
82+
aligned = getattr(reference_type, "_aligned_", False)
8283

8384
for name, annotation, reference in iterate_annotation_chain(reference_type, terminate_at=struct):
85+
if name == "_aligned_":
86+
continue
87+
8488
resolved_type, bitfield_field, explicit_offset = self._resolve_field(
8589
name, annotation, reference, inflater, reference_type,
8690
)
@@ -97,6 +101,8 @@ def _inflate_struct_attributes(
97101
current_offset += offset_delta
98102
else:
99103
current_offset += bf_tracker.flush()
104+
if aligned and explicit_offset is None:
105+
current_offset = _align_offset(current_offset, alignment_of(resolved_type))
100106
result = resolved_type(resolver.relative_from_own(current_offset, 0))
101107
current_offset += size_of(result)
102108

@@ -154,11 +160,17 @@ def _resolve_field(
154160
def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
155161
"""Compute the size of the struct."""
156162
size = 0
163+
max_alignment = 1
157164
bf_tracker = BitfieldTracker()
165+
aligned = getattr(reference_type, "_aligned_", False)
158166

159167
for name, annotation, reference in iterate_annotation_chain(reference_type, terminate_at=struct):
168+
if name == "_aligned_":
169+
continue
170+
160171
bitfield_field = None
161172
attribute = None
173+
has_explicit_offset = False
162174

163175
if name in reference.__dict__:
164176
attrs = getattr(reference, name)
@@ -174,6 +186,7 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
174186
elif isinstance(attr, Field):
175187
attribute = cls._inflater.inflater_for((attr, annotation), (None, cls))(None)
176188
elif isinstance(attr, OffsetAttribute):
189+
has_explicit_offset = True
177190
offset = attr.offset
178191
if offset < size:
179192
raise ValueError("Offset must be greater than the current size.")
@@ -190,10 +203,21 @@ def compute_own_size(cls: type[struct_impl], reference_type: type) -> None:
190203
size += bf_tracker.compute_size(bitfield_field)
191204
else:
192205
size += bf_tracker.flush()
206+
if aligned and not has_explicit_offset:
207+
field_align = alignment_of(attribute)
208+
max_alignment = max(max_alignment, field_align)
209+
size = _align_offset(size, field_align)
193210
size += size_of(attribute)
194211

195212
size += bf_tracker.flush()
213+
214+
if aligned:
215+
if isinstance(aligned, int) and aligned is not True:
216+
max_alignment = max(max_alignment, aligned)
217+
size = _align_offset(size, max_alignment)
218+
196219
cls.size = size
220+
cls.alignment = max_alignment if aligned else 1
197221

198222
@property
199223
def address(self: struct_impl) -> int:

libdestruct/common/utils.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import contextlib
910
import sys
1011
from types import MethodType
1112
from typing import TYPE_CHECKING, Any, ForwardRef
@@ -56,6 +57,58 @@ def size_of(item_or_inflater: obj | callable[[Resolver], obj]) -> int:
5657
raise ValueError(f"Cannot determine the size of {item_or_inflater}")
5758

5859

60+
def alignment_of(item: obj | type[obj]) -> int:
61+
"""Return the natural alignment of a type or instance.
62+
63+
For primitive types, alignment equals their size (1, 2, 4, or 8).
64+
For struct types, alignment is computed as the max of member alignments.
65+
For packed structs (the default), alignment is 1.
66+
"""
67+
# For uninflated struct types, trigger inflation first so alignment is computed
68+
if isinstance(item, type) and not hasattr(item, "size") and not hasattr(item, "_type_impl"):
69+
with contextlib.suppress(ValueError, TypeError):
70+
size_of(item)
71+
72+
# Struct types with computed alignment
73+
if isinstance(item, type) and hasattr(item, "_type_impl"):
74+
impl = item._type_impl
75+
if hasattr(impl, "alignment"):
76+
return impl.alignment
77+
78+
# Explicit alignment attribute (struct_impl instances, arrays, etc.)
79+
if not isinstance(item, type) and hasattr(item, "alignment") and isinstance(item.alignment, int):
80+
return item.alignment
81+
if isinstance(item, type) and "alignment" in item.__dict__ and isinstance(item.__dict__["alignment"], int):
82+
return item.__dict__["alignment"]
83+
84+
# Field descriptors
85+
if isinstance(item, Field):
86+
return _alignment_from_size(item.get_size())
87+
if is_field_bound_method(item):
88+
return _alignment_from_size(item.__self__.get_size())
89+
90+
# Derive from size for power-of-2 sized types
91+
try:
92+
s = size_of(item)
93+
return _alignment_from_size(s)
94+
except (ValueError, TypeError):
95+
return 1
96+
97+
98+
def _alignment_from_size(s: int) -> int:
99+
"""Derive alignment from size: return size if it's a power of 2 and <= 8, else 1."""
100+
max_alignment = 8
101+
if s > 0 and (s & (s - 1)) == 0 and s <= max_alignment:
102+
return s
103+
return 1
104+
105+
106+
def _align_offset(offset: int, alignment: int) -> int:
107+
"""Round up offset to the next multiple of alignment."""
108+
remainder = offset % alignment
109+
return offset + (alignment - remainder) if remainder else offset
110+
111+
59112
def _resolve_annotation(annotation: Any, defining_class: type) -> Any:
60113
"""Resolve a string annotation to its actual type.
61114

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ nav:
6767
- Forward References: advanced/forward_refs.md
6868
- Hex Dump: advanced/hexdump.md
6969
- Unions: advanced/tagged_unions.md
70+
- Struct Alignment: advanced/alignment.md
7071
- Field Offsets: advanced/offset.md

0 commit comments

Comments
 (0)