Skip to content

Commit 298bcdc

Browse files
authored
gh-112433: Add optional _align_ attribute to ctypes.Structure (GH-113790)
1 parent f42e112 commit 298bcdc

File tree

8 files changed

+328
-1
lines changed

8 files changed

+328
-1
lines changed

Doc/library/ctypes.rst

+10
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,10 @@ compiler does it. It is possible to override this behavior by specifying a
670670
:attr:`~Structure._pack_` class attribute in the subclass definition.
671671
This must be set to a positive integer and specifies the maximum alignment for the fields.
672672
This is what ``#pragma pack(n)`` also does in MSVC.
673+
It is also possible to set a minimum alignment for how the subclass itself is packed in the
674+
same way ``#pragma align(n)`` works in MSVC.
675+
This can be achieved by specifying a ::attr:`~Structure._align_` class attribute
676+
in the subclass definition.
673677

674678
:mod:`ctypes` uses the native byte order for Structures and Unions. To build
675679
structures with non-native byte order, you can use one of the
@@ -2534,6 +2538,12 @@ fields, or any other data types containing pointer type fields.
25342538
Setting this attribute to 0 is the same as not setting it at all.
25352539

25362540

2541+
.. attribute:: _align_
2542+
2543+
An optional small integer that allows overriding the alignment of
2544+
the structure when being packed or unpacked to/from memory.
2545+
Setting this attribute to 0 is the same as not setting it at all.
2546+
25372547
.. attribute:: _anonymous_
25382548

25392549
An optional sequence that lists the names of unnamed (anonymous) fields.

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ struct _Py_global_strings {
231231
STRUCT_FOR_ID(_abc_impl)
232232
STRUCT_FOR_ID(_abstract_)
233233
STRUCT_FOR_ID(_active)
234+
STRUCT_FOR_ID(_align_)
234235
STRUCT_FOR_ID(_annotation)
235236
STRUCT_FOR_ID(_anonymous_)
236237
STRUCT_FOR_ID(_argtypes_)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
from ctypes import (
2+
c_char, c_uint32, c_uint16, c_ubyte, c_byte, alignment, sizeof,
3+
BigEndianStructure, LittleEndianStructure,
4+
BigEndianUnion, LittleEndianUnion,
5+
)
6+
import struct
7+
import unittest
8+
9+
10+
class TestAlignedStructures(unittest.TestCase):
11+
def test_aligned_string(self):
12+
for base, e in (
13+
(LittleEndianStructure, "<"),
14+
(BigEndianStructure, ">"),
15+
):
16+
data = bytearray(struct.pack(f"{e}i12x16s", 7, b"hello world!"))
17+
class Aligned(base):
18+
_align_ = 16
19+
_fields_ = [
20+
('value', c_char * 12)
21+
]
22+
23+
class Main(base):
24+
_fields_ = [
25+
('first', c_uint32),
26+
('string', Aligned),
27+
]
28+
29+
main = Main.from_buffer(data)
30+
self.assertEqual(main.first, 7)
31+
self.assertEqual(main.string.value, b'hello world!')
32+
self.assertEqual(bytes(main.string), b'hello world!\0\0\0\0')
33+
self.assertEqual(Main.string.offset, 16)
34+
self.assertEqual(Main.string.size, 16)
35+
self.assertEqual(alignment(main.string), 16)
36+
self.assertEqual(alignment(main), 16)
37+
38+
def test_aligned_structures(self):
39+
for base, data in (
40+
(LittleEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
41+
(BigEndianStructure, bytearray(b"\1\0\0\0\1\0\0\0\7\0\0\0")),
42+
):
43+
class SomeBools(base):
44+
_align_ = 4
45+
_fields_ = [
46+
("bool1", c_ubyte),
47+
("bool2", c_ubyte),
48+
]
49+
class Main(base):
50+
_fields_ = [
51+
("x", c_ubyte),
52+
("y", SomeBools),
53+
("z", c_ubyte),
54+
]
55+
56+
main = Main.from_buffer(data)
57+
self.assertEqual(alignment(SomeBools), 4)
58+
self.assertEqual(alignment(main), 4)
59+
self.assertEqual(alignment(main.y), 4)
60+
self.assertEqual(Main.x.size, 1)
61+
self.assertEqual(Main.y.offset, 4)
62+
self.assertEqual(Main.y.size, 4)
63+
self.assertEqual(main.y.bool1, True)
64+
self.assertEqual(main.y.bool2, False)
65+
self.assertEqual(Main.z.offset, 8)
66+
self.assertEqual(main.z, 7)
67+
68+
def test_oversized_structure(self):
69+
data = bytearray(b"\0" * 8)
70+
for base in (LittleEndianStructure, BigEndianStructure):
71+
class SomeBoolsTooBig(base):
72+
_align_ = 8
73+
_fields_ = [
74+
("bool1", c_ubyte),
75+
("bool2", c_ubyte),
76+
("bool3", c_ubyte),
77+
]
78+
class Main(base):
79+
_fields_ = [
80+
("y", SomeBoolsTooBig),
81+
("z", c_uint32),
82+
]
83+
with self.assertRaises(ValueError) as ctx:
84+
Main.from_buffer(data)
85+
self.assertEqual(
86+
ctx.exception.args[0],
87+
'Buffer size too small (4 instead of at least 8 bytes)'
88+
)
89+
90+
def test_aligned_subclasses(self):
91+
for base, e in (
92+
(LittleEndianStructure, "<"),
93+
(BigEndianStructure, ">"),
94+
):
95+
data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
96+
class UnalignedSub(base):
97+
x: c_uint32
98+
_fields_ = [
99+
("x", c_uint32),
100+
]
101+
102+
class AlignedStruct(UnalignedSub):
103+
_align_ = 8
104+
_fields_ = [
105+
("y", c_uint32),
106+
]
107+
108+
class Main(base):
109+
_fields_ = [
110+
("a", c_uint32),
111+
("b", AlignedStruct)
112+
]
113+
114+
main = Main.from_buffer(data)
115+
self.assertEqual(alignment(main.b), 8)
116+
self.assertEqual(alignment(main), 8)
117+
self.assertEqual(sizeof(main.b), 8)
118+
self.assertEqual(sizeof(main), 16)
119+
self.assertEqual(main.a, 1)
120+
self.assertEqual(main.b.x, 3)
121+
self.assertEqual(main.b.y, 4)
122+
self.assertEqual(Main.b.offset, 8)
123+
self.assertEqual(Main.b.size, 8)
124+
125+
def test_aligned_union(self):
126+
for sbase, ubase, e in (
127+
(LittleEndianStructure, LittleEndianUnion, "<"),
128+
(BigEndianStructure, BigEndianUnion, ">"),
129+
):
130+
data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
131+
class AlignedUnion(ubase):
132+
_align_ = 8
133+
_fields_ = [
134+
("a", c_uint32),
135+
("b", c_ubyte * 7),
136+
]
137+
138+
class Main(sbase):
139+
_fields_ = [
140+
("first", c_uint32),
141+
("union", AlignedUnion),
142+
]
143+
144+
main = Main.from_buffer(data)
145+
self.assertEqual(main.first, 1)
146+
self.assertEqual(main.union.a, 3)
147+
self.assertEqual(bytes(main.union.b), data[8:-1])
148+
self.assertEqual(Main.union.offset, 8)
149+
self.assertEqual(Main.union.size, 8)
150+
self.assertEqual(alignment(main.union), 8)
151+
self.assertEqual(alignment(main), 8)
152+
153+
def test_aligned_struct_in_union(self):
154+
for sbase, ubase, e in (
155+
(LittleEndianStructure, LittleEndianUnion, "<"),
156+
(BigEndianStructure, BigEndianUnion, ">"),
157+
):
158+
data = bytearray(struct.pack(f"{e}4i", 1, 2, 3, 4))
159+
class Sub(sbase):
160+
_align_ = 8
161+
_fields_ = [
162+
("x", c_uint32),
163+
("y", c_uint32),
164+
]
165+
166+
class MainUnion(ubase):
167+
_fields_ = [
168+
("a", c_uint32),
169+
("b", Sub),
170+
]
171+
172+
class Main(sbase):
173+
_fields_ = [
174+
("first", c_uint32),
175+
("union", MainUnion),
176+
]
177+
178+
main = Main.from_buffer(data)
179+
self.assertEqual(Main.first.size, 4)
180+
self.assertEqual(alignment(main.union), 8)
181+
self.assertEqual(alignment(main), 8)
182+
self.assertEqual(Main.union.offset, 8)
183+
self.assertEqual(Main.union.size, 8)
184+
self.assertEqual(main.first, 1)
185+
self.assertEqual(main.union.a, 3)
186+
self.assertEqual(main.union.b.x, 3)
187+
self.assertEqual(main.union.b.y, 4)
188+
189+
def test_smaller_aligned_subclassed_union(self):
190+
for sbase, ubase, e in (
191+
(LittleEndianStructure, LittleEndianUnion, "<"),
192+
(BigEndianStructure, BigEndianUnion, ">"),
193+
):
194+
data = bytearray(struct.pack(f"{e}H2xI", 1, 0xD60102D7))
195+
class SubUnion(ubase):
196+
_align_ = 2
197+
_fields_ = [
198+
("unsigned", c_ubyte),
199+
("signed", c_byte),
200+
]
201+
202+
class MainUnion(SubUnion):
203+
_fields_ = [
204+
("num", c_uint32)
205+
]
206+
207+
class Main(sbase):
208+
_fields_ = [
209+
("first", c_uint16),
210+
("union", MainUnion),
211+
]
212+
213+
main = Main.from_buffer(data)
214+
self.assertEqual(main.union.num, 0xD60102D7)
215+
self.assertEqual(main.union.unsigned, data[4])
216+
self.assertEqual(main.union.signed, data[4] - 256)
217+
self.assertEqual(alignment(main), 4)
218+
self.assertEqual(alignment(main.union), 4)
219+
self.assertEqual(Main.union.offset, 4)
220+
self.assertEqual(Main.union.size, 4)
221+
self.assertEqual(Main.first.size, 2)
222+
223+
def test_larger_aligned_subclassed_union(self):
224+
for ubase, e in (
225+
(LittleEndianUnion, "<"),
226+
(BigEndianUnion, ">"),
227+
):
228+
data = bytearray(struct.pack(f"{e}I4x", 0xD60102D6))
229+
class SubUnion(ubase):
230+
_align_ = 8
231+
_fields_ = [
232+
("unsigned", c_ubyte),
233+
("signed", c_byte),
234+
]
235+
236+
class Main(SubUnion):
237+
_fields_ = [
238+
("num", c_uint32)
239+
]
240+
241+
main = Main.from_buffer(data)
242+
self.assertEqual(alignment(main), 8)
243+
self.assertEqual(sizeof(main), 8)
244+
self.assertEqual(main.num, 0xD60102D6)
245+
self.assertEqual(main.unsigned, 0xD6)
246+
self.assertEqual(main.signed, -42)
247+
248+
def test_aligned_packed_structures(self):
249+
for sbase, e in (
250+
(LittleEndianStructure, "<"),
251+
(BigEndianStructure, ">"),
252+
):
253+
data = bytearray(struct.pack(f"{e}B2H4xB", 1, 2, 3, 4))
254+
255+
class Inner(sbase):
256+
_align_ = 8
257+
_fields_ = [
258+
("x", c_uint16),
259+
("y", c_uint16),
260+
]
261+
262+
class Main(sbase):
263+
_pack_ = 1
264+
_fields_ = [
265+
("a", c_ubyte),
266+
("b", Inner),
267+
("c", c_ubyte),
268+
]
269+
270+
main = Main.from_buffer(data)
271+
self.assertEqual(sizeof(main), 10)
272+
self.assertEqual(Main.b.offset, 1)
273+
# Alignment == 8 because _pack_ wins out.
274+
self.assertEqual(alignment(main.b), 8)
275+
# Size is still 8 though since inside this Structure, it will have
276+
# effect.
277+
self.assertEqual(sizeof(main.b), 8)
278+
self.assertEqual(Main.c.offset, 9)
279+
self.assertEqual(main.a, 1)
280+
self.assertEqual(main.b.x, 2)
281+
self.assertEqual(main.b.y, 3)
282+
self.assertEqual(main.c, 4)
283+
284+
285+
if __name__ == '__main__':
286+
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ability to force alignment of :mod:`ctypes.Structure` by way of the new ``_align_`` attribute on the class.

0 commit comments

Comments
 (0)