Skip to content

Commit 1fc6508

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 952fad8 commit 1fc6508

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
@@ -503,12 +507,59 @@ class Test3B(Test3A):
503507
('more_data', c_float * 2),
504508
]
505509

510+
class Test3C1(Structure):
511+
_fields_ = [
512+
("data", c_double * 4)
513+
]
514+
515+
class DataType4(Array):
516+
_type_ = c_double
517+
_length_ = 4
518+
519+
class Test3C2(Structure):
520+
_fields_ = [
521+
("data", DataType4)
522+
]
523+
524+
class Test3C3(Structure):
525+
_fields_ = [
526+
("x", c_double),
527+
("y", c_double),
528+
("z", c_double),
529+
("t", c_double)
530+
]
531+
532+
class Test3D1(Structure):
533+
_fields_ = [
534+
("data", c_double * 5)
535+
]
536+
537+
class DataType5(Array):
538+
_type_ = c_double
539+
_length_ = 5
540+
541+
class Test3D2(Structure):
542+
_fields_ = [
543+
("data", DataType5)
544+
]
545+
546+
class Test3D3(Structure):
547+
_fields_ = [
548+
("x", c_double),
549+
("y", c_double),
550+
("z", c_double),
551+
("t", c_double),
552+
("u", c_double)
553+
]
554+
555+
# Load the shared library
556+
dll = CDLL(_ctypes_test.__file__)
557+
506558
s = Test2()
507559
expected = 0
508560
for i in range(16):
509561
s.data[i] = i
510562
expected += i
511-
dll = CDLL(_ctypes_test.__file__)
512563
func = dll._testfunc_array_in_struct1
513564
func.restype = c_int
514565
func.argtypes = (Test2,)
@@ -549,6 +600,78 @@ class Test3B(Test3A):
549600
self.assertAlmostEqual(s.more_data[0], -3.0, places=6)
550601
self.assertAlmostEqual(s.more_data[1], -2.0, places=6)
551602

603+
# Tests for struct Test3C
604+
expected = (1.0, 2.0, 3.0, 4.0)
605+
func = dll._testfunc_array_in_struct_set_defaults_3C
606+
func.restype = Test3C1
607+
result = func()
608+
# check the default values have been set properly
609+
self.assertEqual(
610+
(result.data[0],
611+
result.data[1],
612+
result.data[2],
613+
result.data[3]),
614+
expected
615+
)
616+
617+
func = dll._testfunc_array_in_struct_set_defaults_3C
618+
func.restype = Test3C2
619+
result = func()
620+
# check the default values have been set properly
621+
self.assertEqual(
622+
(result.data[0],
623+
result.data[1],
624+
result.data[2],
625+
result.data[3]),
626+
expected
627+
)
628+
629+
func = dll._testfunc_array_in_struct_set_defaults_3C
630+
func.restype = Test3C3
631+
result = func()
632+
# check the default values have been set properly
633+
self.assertEqual((result.x, result.y, result.z, result.t), expected)
634+
635+
# Tests for struct Test3D
636+
expected = (1.0, 2.0, 3.0, 4.0, 5.0)
637+
func = dll._testfunc_array_in_struct_set_defaults_3D
638+
func.restype = Test3D1
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 = Test3D2
652+
result = func()
653+
# check the default values have been set properly
654+
self.assertEqual(
655+
(result.data[0],
656+
result.data[1],
657+
result.data[2],
658+
result.data[3],
659+
result.data[4]),
660+
expected
661+
)
662+
663+
func = dll._testfunc_array_in_struct_set_defaults_3D
664+
func.restype = Test3D3
665+
result = func()
666+
# check the default values have been set properly
667+
self.assertEqual(
668+
(result.x,
669+
result.y,
670+
result.z,
671+
result.t,
672+
result.u),
673+
expected)
674+
552675
def test_38368(self):
553676
class U(Union):
554677
_fields_ = [

Modules/_ctypes/_ctypes_test.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,42 @@ _testfunc_array_in_struct2a(Test3B in)
135135
return result;
136136
}
137137

138+
/*
139+
* See gh-110190. structs containing arrays of up to four floating point types
140+
* (max 32 bytes) are passed in registers on Arm.
141+
*/
142+
143+
typedef struct {
144+
double data[4];
145+
} Test3C;
146+
147+
EXPORT(Test3C)
148+
_testfunc_array_in_struct_set_defaults_3C(void)
149+
{
150+
Test3C s;
151+
s.data[0] = 1.0;
152+
s.data[1] = 2.0;
153+
s.data[2] = 3.0;
154+
s.data[3] = 4.0;
155+
return s;
156+
}
157+
158+
typedef struct {
159+
double data[5];
160+
} Test3D;
161+
162+
EXPORT(Test3D)
163+
_testfunc_array_in_struct_set_defaults_3D(void)
164+
{
165+
Test3D 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+
s.data[4] = 5.0;
171+
return s;
172+
}
173+
138174
typedef union {
139175
long a_long;
140176
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)