Skip to content

Commit a6ef3ac

Browse files
committed
Add optional _align_ attribute to ctypes.Structure
1 parent 84d1f76 commit a6ef3ac

File tree

7 files changed

+119
-2
lines changed

7 files changed

+119
-2
lines changed

Doc/library/ctypes.rst

Lines changed: 10 additions & 0 deletions
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 interger 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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from ctypes import Structure, c_char, c_uint32, c_ubyte, alignment
2+
import inspect
3+
import unittest
4+
5+
6+
class TestAlignedStructures(unittest.TestCase):
7+
def test_aligned_string(self):
8+
data = bytearray(
9+
b'\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
10+
b'\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21\x00\x00\x00\x00'
11+
)
12+
13+
class Aligned(Structure):
14+
_align_ = 16
15+
_fields_ = [
16+
('value', c_char * 16),
17+
]
18+
19+
class Main(Structure):
20+
_fields_ = [
21+
('first', c_uint32),
22+
('string', Aligned),
23+
]
24+
25+
d = Main.from_buffer(data)
26+
self.assertEqual(d.first, 7)
27+
self.assertEqual(d.string.value, b'hello world!')
28+
self.assertEqual(
29+
bytes(d.string.__buffer__(inspect.BufferFlags.SIMPLE)),
30+
b'\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21\x00\x00\x00\x00'
31+
)
32+
33+
def test_aligned_structures(self):
34+
data = bytearray(
35+
b'\x01\x00\x01\x00\x07\x00\x00\x00'
36+
)
37+
38+
class SomeBools(Structure):
39+
_align_ = 4
40+
_fields_ = [
41+
("bool1", c_ubyte),
42+
("bool2", c_ubyte),
43+
("bool3", c_ubyte),
44+
]
45+
class Main(Structure):
46+
_fields_ = [
47+
("x", SomeBools),
48+
("y", c_uint32),
49+
]
50+
51+
class SomeBoolsTooBig(Structure):
52+
_align_ = 8
53+
_fields_ = [
54+
("bool1", c_ubyte),
55+
("bool2", c_ubyte),
56+
("bool3", c_ubyte),
57+
]
58+
class MainTooBig(Structure):
59+
_fields_ = [
60+
("x", SomeBoolsTooBig),
61+
("y", c_uint32),
62+
]
63+
d = Main.from_buffer(data)
64+
self.assertEqual(d.y, 7)
65+
self.assertEqual(alignment(SomeBools), 4)
66+
self.assertEqual(d.x.bool1, True)
67+
self.assertEqual(d.x.bool2, False)
68+
self.assertEqual(d.x.bool3, True)
69+
70+
with self.assertRaises(ValueError) as ctx:
71+
MainTooBig.from_buffer(data)
72+
self.assertEqual(
73+
ctx.exception.args[0],
74+
'Buffer size too small (4 instead of at least 8 bytes)'
75+
)
76+
77+
if __name__ == '__main__':
78+
unittest.main()

Modules/_ctypes/stgdict.c

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
379379
int bitofs;
380380
PyObject *tmp;
381381
int pack;
382+
int forced_alignment = 1;
382383
Py_ssize_t ffi_ofs;
383384
int big_endian;
384385
int arrays_seen = 0;
@@ -419,6 +420,28 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
419420
pack = 0;
420421
}
421422

423+
if (PyObject_GetOptionalAttr(type, &_Py_ID(_align_), &tmp) < 0) {
424+
return -1;
425+
}
426+
if (tmp) {
427+
forced_alignment = PyLong_AsInt(tmp);
428+
Py_DECREF(tmp);
429+
if (forced_alignment < 0) {
430+
if (!PyErr_Occurred() ||
431+
PyErr_ExceptionMatches(PyExc_TypeError) ||
432+
PyErr_ExceptionMatches(PyExc_OverflowError))
433+
{
434+
PyErr_SetString(PyExc_ValueError,
435+
"_align_ must be a non-negative integer");
436+
}
437+
return -1;
438+
}
439+
}
440+
else {
441+
/* Setting `_align_ = 0` amounts to using the default alignment */
442+
forced_alignment = 1;
443+
}
444+
422445
len = PySequence_Size(fields);
423446
if (len == -1) {
424447
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
@@ -463,7 +486,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
463486
size = offset = basedict->size;
464487
align = basedict->align;
465488
union_size = 0;
466-
total_align = align ? align : 1;
489+
total_align = align ? align : forced_alignment;
467490
stgdict->ffi_type_pointer.type = FFI_TYPE_STRUCT;
468491
stgdict->ffi_type_pointer.elements = PyMem_New(ffi_type *, basedict->length + len + 1);
469492
if (stgdict->ffi_type_pointer.elements == NULL) {
@@ -483,7 +506,7 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
483506
size = 0;
484507
align = 0;
485508
union_size = 0;
486-
total_align = 1;
509+
total_align = forced_alignment;
487510
stgdict->ffi_type_pointer.type = FFI_TYPE_STRUCT;
488511
stgdict->ffi_type_pointer.elements = PyMem_New(ffi_type *, len + 1);
489512
if (stgdict->ffi_type_pointer.elements == NULL) {

0 commit comments

Comments
 (0)