Skip to content

Commit bc68f4a

Browse files
authored
gh-110190: Fix ctypes structs with array on Arm (#112604)
Set MAX_STRUCT_SIZE to 32 in stgdict.c when on Arm platforms. This because on Arm platforms structs with at most 4 elements of any floating point type values can be passed through registers. If the type is double the maximum size of the struct is 32 bytes. On x86-64 Linux, it's maximum 16 bytes hence we need to differentiate.
1 parent e7e1116 commit bc68f4a

File tree

4 files changed

+193
-20
lines changed

4 files changed

+193
-20
lines changed

Lib/test/test_ctypes/test_structures.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import struct
33
import sys
44
import unittest
5-
from ctypes import (CDLL, Structure, Union, POINTER, sizeof, byref, alignment,
5+
from ctypes import (CDLL, Array, Structure, Union, POINTER, sizeof, byref, alignment,
66
c_void_p, c_char, c_wchar, c_byte, c_ubyte,
77
c_uint8, c_uint16, c_uint32,
88
c_short, c_ushort, c_int, c_uint,
@@ -494,12 +494,59 @@ class Test3B(Test3A):
494494
('more_data', c_float * 2),
495495
]
496496

497+
class Test3C1(Structure):
498+
_fields_ = [
499+
("data", c_double * 4)
500+
]
501+
502+
class DataType4(Array):
503+
_type_ = c_double
504+
_length_ = 4
505+
506+
class Test3C2(Structure):
507+
_fields_ = [
508+
("data", DataType4)
509+
]
510+
511+
class Test3C3(Structure):
512+
_fields_ = [
513+
("x", c_double),
514+
("y", c_double),
515+
("z", c_double),
516+
("t", c_double)
517+
]
518+
519+
class Test3D1(Structure):
520+
_fields_ = [
521+
("data", c_double * 5)
522+
]
523+
524+
class DataType5(Array):
525+
_type_ = c_double
526+
_length_ = 5
527+
528+
class Test3D2(Structure):
529+
_fields_ = [
530+
("data", DataType5)
531+
]
532+
533+
class Test3D3(Structure):
534+
_fields_ = [
535+
("x", c_double),
536+
("y", c_double),
537+
("z", c_double),
538+
("t", c_double),
539+
("u", c_double)
540+
]
541+
542+
# Load the shared library
543+
dll = CDLL(_ctypes_test.__file__)
544+
497545
s = Test2()
498546
expected = 0
499547
for i in range(16):
500548
s.data[i] = i
501549
expected += i
502-
dll = CDLL(_ctypes_test.__file__)
503550
func = dll._testfunc_array_in_struct1
504551
func.restype = c_int
505552
func.argtypes = (Test2,)
@@ -540,6 +587,78 @@ class Test3B(Test3A):
540587
self.assertAlmostEqual(s.more_data[0], -3.0, places=6)
541588
self.assertAlmostEqual(s.more_data[1], -2.0, places=6)
542589

590+
# Tests for struct Test3C
591+
expected = (1.0, 2.0, 3.0, 4.0)
592+
func = dll._testfunc_array_in_struct_set_defaults_3C
593+
func.restype = Test3C1
594+
result = func()
595+
# check the default values have been set properly
596+
self.assertEqual(
597+
(result.data[0],
598+
result.data[1],
599+
result.data[2],
600+
result.data[3]),
601+
expected
602+
)
603+
604+
func = dll._testfunc_array_in_struct_set_defaults_3C
605+
func.restype = Test3C2
606+
result = func()
607+
# check the default values have been set properly
608+
self.assertEqual(
609+
(result.data[0],
610+
result.data[1],
611+
result.data[2],
612+
result.data[3]),
613+
expected
614+
)
615+
616+
func = dll._testfunc_array_in_struct_set_defaults_3C
617+
func.restype = Test3C3
618+
result = func()
619+
# check the default values have been set properly
620+
self.assertEqual((result.x, result.y, result.z, result.t), expected)
621+
622+
# Tests for struct Test3D
623+
expected = (1.0, 2.0, 3.0, 4.0, 5.0)
624+
func = dll._testfunc_array_in_struct_set_defaults_3D
625+
func.restype = Test3D1
626+
result = func()
627+
# check the default values have been set properly
628+
self.assertEqual(
629+
(result.data[0],
630+
result.data[1],
631+
result.data[2],
632+
result.data[3],
633+
result.data[4]),
634+
expected
635+
)
636+
637+
func = dll._testfunc_array_in_struct_set_defaults_3D
638+
func.restype = Test3D2
639+
result = func()
640+
# check the default values have been set properly
641+
self.assertEqual(
642+
(result.data[0],
643+
result.data[1],
644+
result.data[2],
645+
result.data[3],
646+
result.data[4]),
647+
expected
648+
)
649+
650+
func = dll._testfunc_array_in_struct_set_defaults_3D
651+
func.restype = Test3D3
652+
result = func()
653+
# check the default values have been set properly
654+
self.assertEqual(
655+
(result.x,
656+
result.y,
657+
result.z,
658+
result.t,
659+
result.u),
660+
expected)
661+
543662
def test_38368(self):
544663
class U(Union):
545664
_fields_ = [
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix ctypes structs with array on Arm platform by setting ``MAX_STRUCT_SIZE`` to 32 in stgdict. Patch by Diego Russo.

Modules/_ctypes/_ctypes_test.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,42 @@ _testfunc_array_in_struct2a(Test3B in)
150150
return result;
151151
}
152152

153+
/*
154+
* See gh-110190. structs containing arrays of up to four floating point types
155+
* (max 32 bytes) are passed in registers on Arm.
156+
*/
157+
158+
typedef struct {
159+
double data[4];
160+
} Test3C;
161+
162+
EXPORT(Test3C)
163+
_testfunc_array_in_struct_set_defaults_3C(void)
164+
{
165+
Test3C s;
166+
s.data[0] = 1.0;
167+
s.data[1] = 2.0;
168+
s.data[2] = 3.0;
169+
s.data[3] = 4.0;
170+
return s;
171+
}
172+
173+
typedef struct {
174+
double data[5];
175+
} Test3D;
176+
177+
EXPORT(Test3D)
178+
_testfunc_array_in_struct_set_defaults_3D(void)
179+
{
180+
Test3D s;
181+
s.data[0] = 1.0;
182+
s.data[1] = 2.0;
183+
s.data[2] = 3.0;
184+
s.data[3] = 4.0;
185+
s.data[4] = 5.0;
186+
return s;
187+
}
188+
153189
typedef union {
154190
long a_long;
155191
struct {

Modules/_ctypes/stgdict.c

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -697,29 +697,43 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
697697
stgdict->align = total_align;
698698
stgdict->length = len; /* ADD ffi_ofs? */
699699

700-
#define MAX_STRUCT_SIZE 16
700+
/*
701+
* On Arm platforms, structs with at most 4 elements of any floating point
702+
* type values can be passed through registers. If the type is double the
703+
* maximum size of the struct is 32 bytes.
704+
* By Arm platforms it is meant both 32 and 64-bit.
705+
*/
706+
#if defined(__aarch64__) || defined(__arm__)
707+
# define MAX_STRUCT_SIZE 32
708+
#else
709+
# define MAX_STRUCT_SIZE 16
710+
#endif
701711

702712
if (arrays_seen && (size <= MAX_STRUCT_SIZE)) {
703713
/*
704-
* See bpo-22273. Arrays are normally treated as pointers, which is
705-
* fine when an array name is being passed as parameter, but not when
706-
* passing structures by value that contain arrays. On 64-bit Linux,
707-
* small structures passed by value are passed in registers, and in
708-
* order to do this, libffi needs to know the true type of the array
709-
* members of structs. Treating them as pointers breaks things.
714+
* See bpo-22273 and gh-110190. Arrays are normally treated as
715+
* pointers, which is fine when an array name is being passed as
716+
* parameter, but not when passing structures by value that contain
717+
* arrays.
718+
* On x86_64 Linux and Arm platforms, small structures passed by
719+
* value are passed in registers, and in order to do this, libffi needs
720+
* to know the true type of the array members of structs. Treating them
721+
* as pointers breaks things.
710722
*
711-
* By small structures, we mean ones that are 16 bytes or less. In that
712-
* case, there can't be more than 16 elements after unrolling arrays,
713-
* as we (will) disallow bitfields. So we can collect the true ffi_type
714-
* values in a fixed-size local array on the stack and, if any arrays
715-
* were seen, replace the ffi_type_pointer.elements with a more
716-
* accurate set, to allow libffi to marshal them into registers
717-
* correctly. It means one more loop over the fields, but if we got
718-
* here, the structure is small, so there aren't too many of those.
723+
* By small structures, we mean ones that are 16 bytes or less on
724+
* x86-64 and 32 bytes or less on Arm. In that case, there can't be
725+
* more than 16 or 32 elements after unrolling arrays, as we (will)
726+
* disallow bitfields. So we can collect the true ffi_type values in
727+
* a fixed-size local array on the stack and, if any arrays were seen,
728+
* replace the ffi_type_pointer.elements with a more accurate set,
729+
* to allow libffi to marshal them into registers correctly.
730+
* It means one more loop over the fields, but if we got here,
731+
* the structure is small, so there aren't too many of those.
719732
*
720-
* Although the passing in registers is specific to 64-bit Linux, the
721-
* array-in-struct vs. pointer problem is general. But we restrict the
722-
* type transformation to small structs nonetheless.
733+
* Although the passing in registers is specific to x86_64 Linux
734+
* and Arm platforms, the array-in-struct vs. pointer problem is
735+
* general. But we restrict the type transformation to small structs
736+
* nonetheless.
723737
*
724738
* Note that although a union may be small in terms of memory usage, it
725739
* could contain many overlapping declarations of arrays, e.g.
@@ -745,6 +759,9 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
745759
* struct { uint_32 e1; uint_32 e2; ... uint_32 e_4; } f6;
746760
* }
747761
*
762+
* The same principle applies for a struct 32 bytes in size like in
763+
* the case of Arm platforms.
764+
*
748765
* So the struct/union needs setting up as follows: all non-array
749766
* elements copied across as is, and all array elements replaced with
750767
* an equivalent struct which has as many fields as the array has

0 commit comments

Comments
 (0)