Skip to content

Commit a025396

Browse files
diegorussomgorny
authored andcommitted
[3.11] pythongh-110190: Fix ctypes structs with array on Arm (python#112604) (python#112766)
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. (cherry picked from commit bc68f4a) Signed-off-by: Michał Górny <[email protected]>
1 parent 90c2b65 commit a025396

File tree

3 files changed

+196
-20
lines changed

3 files changed

+196
-20
lines changed

Lib/ctypes/test/test_structures.py

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import platform
22
import sys
33
import unittest
4-
from ctypes import *
54
from ctypes.test import need_symbol
5+
from ctypes import (CDLL, Array, Structure, Union, POINTER, sizeof, byref, alignment,
6+
c_void_p, c_char, c_wchar, c_byte, c_ubyte,
7+
c_uint8, c_uint16, c_uint32,
8+
c_short, c_ushort, c_int, c_uint,
9+
c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double)
610
from struct import calcsize
711
import _ctypes_test
812
from test import support
@@ -508,12 +512,59 @@ class Test3B(Test3A):
508512
('more_data', c_float * 2),
509513
]
510514

515+
class Test3C1(Structure):
516+
_fields_ = [
517+
("data", c_double * 4)
518+
]
519+
520+
class DataType4(Array):
521+
_type_ = c_double
522+
_length_ = 4
523+
524+
class Test3C2(Structure):
525+
_fields_ = [
526+
("data", DataType4)
527+
]
528+
529+
class Test3C3(Structure):
530+
_fields_ = [
531+
("x", c_double),
532+
("y", c_double),
533+
("z", c_double),
534+
("t", c_double)
535+
]
536+
537+
class Test3D1(Structure):
538+
_fields_ = [
539+
("data", c_double * 5)
540+
]
541+
542+
class DataType5(Array):
543+
_type_ = c_double
544+
_length_ = 5
545+
546+
class Test3D2(Structure):
547+
_fields_ = [
548+
("data", DataType5)
549+
]
550+
551+
class Test3D3(Structure):
552+
_fields_ = [
553+
("x", c_double),
554+
("y", c_double),
555+
("z", c_double),
556+
("t", c_double),
557+
("u", c_double)
558+
]
559+
560+
# Load the shared library
561+
dll = CDLL(_ctypes_test.__file__)
562+
511563
s = Test2()
512564
expected = 0
513565
for i in range(16):
514566
s.data[i] = i
515567
expected += i
516-
dll = CDLL(_ctypes_test.__file__)
517568
func = dll._testfunc_array_in_struct1
518569
func.restype = c_int
519570
func.argtypes = (Test2,)
@@ -554,6 +605,78 @@ class Test3B(Test3A):
554605
self.assertAlmostEqual(s.more_data[0], -3.0, places=6)
555606
self.assertAlmostEqual(s.more_data[1], -2.0, places=6)
556607

608+
# Tests for struct Test3C
609+
expected = (1.0, 2.0, 3.0, 4.0)
610+
func = dll._testfunc_array_in_struct_set_defaults_3C
611+
func.restype = Test3C1
612+
result = func()
613+
# check the default values have been set properly
614+
self.assertEqual(
615+
(result.data[0],
616+
result.data[1],
617+
result.data[2],
618+
result.data[3]),
619+
expected
620+
)
621+
622+
func = dll._testfunc_array_in_struct_set_defaults_3C
623+
func.restype = Test3C2
624+
result = func()
625+
# check the default values have been set properly
626+
self.assertEqual(
627+
(result.data[0],
628+
result.data[1],
629+
result.data[2],
630+
result.data[3]),
631+
expected
632+
)
633+
634+
func = dll._testfunc_array_in_struct_set_defaults_3C
635+
func.restype = Test3C3
636+
result = func()
637+
# check the default values have been set properly
638+
self.assertEqual((result.x, result.y, result.z, result.t), expected)
639+
640+
# Tests for struct Test3D
641+
expected = (1.0, 2.0, 3.0, 4.0, 5.0)
642+
func = dll._testfunc_array_in_struct_set_defaults_3D
643+
func.restype = Test3D1
644+
result = func()
645+
# check the default values have been set properly
646+
self.assertEqual(
647+
(result.data[0],
648+
result.data[1],
649+
result.data[2],
650+
result.data[3],
651+
result.data[4]),
652+
expected
653+
)
654+
655+
func = dll._testfunc_array_in_struct_set_defaults_3D
656+
func.restype = Test3D2
657+
result = func()
658+
# check the default values have been set properly
659+
self.assertEqual(
660+
(result.data[0],
661+
result.data[1],
662+
result.data[2],
663+
result.data[3],
664+
result.data[4]),
665+
expected
666+
)
667+
668+
func = dll._testfunc_array_in_struct_set_defaults_3D
669+
func.restype = Test3D3
670+
result = func()
671+
# check the default values have been set properly
672+
self.assertEqual(
673+
(result.x,
674+
result.y,
675+
result.z,
676+
result.t,
677+
result.u),
678+
expected)
679+
557680
def test_38368(self):
558681
class U(Union):
559682
_fields_ = [

Modules/_ctypes/_ctypes_test.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,42 @@ _testfunc_array_in_struct2a(Test3B in)
131131
return result;
132132
}
133133

134+
/*
135+
* See gh-110190. structs containing arrays of up to four floating point types
136+
* (max 32 bytes) are passed in registers on Arm.
137+
*/
138+
139+
typedef struct {
140+
double data[4];
141+
} Test3C;
142+
143+
EXPORT(Test3C)
144+
_testfunc_array_in_struct_set_defaults_3C(void)
145+
{
146+
Test3C s;
147+
s.data[0] = 1.0;
148+
s.data[1] = 2.0;
149+
s.data[2] = 3.0;
150+
s.data[3] = 4.0;
151+
return s;
152+
}
153+
154+
typedef struct {
155+
double data[5];
156+
} Test3D;
157+
158+
EXPORT(Test3D)
159+
_testfunc_array_in_struct_set_defaults_3D(void)
160+
{
161+
Test3D s;
162+
s.data[0] = 1.0;
163+
s.data[1] = 2.0;
164+
s.data[2] = 3.0;
165+
s.data[3] = 4.0;
166+
s.data[4] = 5.0;
167+
return s;
168+
}
169+
134170
typedef union {
135171
long a_long;
136172
struct {

Modules/_ctypes/stgdict.c

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

656-
#define MAX_STRUCT_SIZE 16
656+
/*
657+
* On Arm platforms, structs with at most 4 elements of any floating point
658+
* type values can be passed through registers. If the type is double the
659+
* maximum size of the struct is 32 bytes.
660+
* By Arm platforms it is meant both 32 and 64-bit.
661+
*/
662+
#if defined(__aarch64__) || defined(__arm__)
663+
# define MAX_STRUCT_SIZE 32
664+
#else
665+
# define MAX_STRUCT_SIZE 16
666+
#endif
657667

658668
if (arrays_seen && (size <= MAX_STRUCT_SIZE)) {
659669
/*
660-
* See bpo-22273. Arrays are normally treated as pointers, which is
661-
* fine when an array name is being passed as parameter, but not when
662-
* passing structures by value that contain arrays. On 64-bit Linux,
663-
* small structures passed by value are passed in registers, and in
664-
* order to do this, libffi needs to know the true type of the array
665-
* members of structs. Treating them as pointers breaks things.
670+
* See bpo-22273 and gh-110190. Arrays are normally treated as
671+
* pointers, which is fine when an array name is being passed as
672+
* parameter, but not when passing structures by value that contain
673+
* arrays.
674+
* On x86_64 Linux and Arm platforms, small structures passed by
675+
* value are passed in registers, and in order to do this, libffi needs
676+
* to know the true type of the array members of structs. Treating them
677+
* as pointers breaks things.
666678
*
667-
* By small structures, we mean ones that are 16 bytes or less. In that
668-
* case, there can't be more than 16 elements after unrolling arrays,
669-
* as we (will) disallow bitfields. So we can collect the true ffi_type
670-
* values in a fixed-size local array on the stack and, if any arrays
671-
* were seen, replace the ffi_type_pointer.elements with a more
672-
* accurate set, to allow libffi to marshal them into registers
673-
* correctly. It means one more loop over the fields, but if we got
674-
* here, the structure is small, so there aren't too many of those.
679+
* By small structures, we mean ones that are 16 bytes or less on
680+
* x86-64 and 32 bytes or less on Arm. In that case, there can't be
681+
* more than 16 or 32 elements after unrolling arrays, as we (will)
682+
* disallow bitfields. So we can collect the true ffi_type values in
683+
* a fixed-size local array on the stack and, if any arrays were seen,
684+
* replace the ffi_type_pointer.elements with a more accurate set,
685+
* to allow libffi to marshal them into registers correctly.
686+
* It means one more loop over the fields, but if we got here,
687+
* the structure is small, so there aren't too many of those.
675688
*
676-
* Although the passing in registers is specific to 64-bit Linux, the
677-
* array-in-struct vs. pointer problem is general. But we restrict the
678-
* type transformation to small structs nonetheless.
689+
* Although the passing in registers is specific to x86_64 Linux
690+
* and Arm platforms, the array-in-struct vs. pointer problem is
691+
* general. But we restrict the type transformation to small structs
692+
* nonetheless.
679693
*
680694
* Note that although a union may be small in terms of memory usage, it
681695
* could contain many overlapping declarations of arrays, e.g.
@@ -701,6 +715,9 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
701715
* struct { uint_32 e1; uint_32 e2; ... uint_32 e_4; } f6;
702716
* }
703717
*
718+
* The same principle applies for a struct 32 bytes in size like in
719+
* the case of Arm platforms.
720+
*
704721
* So the struct/union needs setting up as follows: all non-array
705722
* elements copied across as is, and all array elements replaced with
706723
* an equivalent struct which has as many fields as the array has

0 commit comments

Comments
 (0)