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.
This commit is contained in:
Diego Russo 2023-12-05 15:07:50 +00:00 committed by GitHub
parent e7e1116a78
commit bc68f4a4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 193 additions and 20 deletions

View file

@ -2,7 +2,7 @@ import _ctypes_test
import struct import struct
import sys import sys
import unittest import unittest
from ctypes import (CDLL, Structure, Union, POINTER, sizeof, byref, alignment, from ctypes import (CDLL, Array, Structure, Union, POINTER, sizeof, byref, alignment,
c_void_p, c_char, c_wchar, c_byte, c_ubyte, c_void_p, c_char, c_wchar, c_byte, c_ubyte,
c_uint8, c_uint16, c_uint32, c_uint8, c_uint16, c_uint32,
c_short, c_ushort, c_int, c_uint, c_short, c_ushort, c_int, c_uint,
@ -494,12 +494,59 @@ class StructureTestCase(unittest.TestCase):
('more_data', c_float * 2), ('more_data', c_float * 2),
] ]
class Test3C1(Structure):
_fields_ = [
("data", c_double * 4)
]
class DataType4(Array):
_type_ = c_double
_length_ = 4
class Test3C2(Structure):
_fields_ = [
("data", DataType4)
]
class Test3C3(Structure):
_fields_ = [
("x", c_double),
("y", c_double),
("z", c_double),
("t", c_double)
]
class Test3D1(Structure):
_fields_ = [
("data", c_double * 5)
]
class DataType5(Array):
_type_ = c_double
_length_ = 5
class Test3D2(Structure):
_fields_ = [
("data", DataType5)
]
class Test3D3(Structure):
_fields_ = [
("x", c_double),
("y", c_double),
("z", c_double),
("t", c_double),
("u", c_double)
]
# Load the shared library
dll = CDLL(_ctypes_test.__file__)
s = Test2() s = Test2()
expected = 0 expected = 0
for i in range(16): for i in range(16):
s.data[i] = i s.data[i] = i
expected += i expected += i
dll = CDLL(_ctypes_test.__file__)
func = dll._testfunc_array_in_struct1 func = dll._testfunc_array_in_struct1
func.restype = c_int func.restype = c_int
func.argtypes = (Test2,) func.argtypes = (Test2,)
@ -540,6 +587,78 @@ class StructureTestCase(unittest.TestCase):
self.assertAlmostEqual(s.more_data[0], -3.0, places=6) self.assertAlmostEqual(s.more_data[0], -3.0, places=6)
self.assertAlmostEqual(s.more_data[1], -2.0, places=6) self.assertAlmostEqual(s.more_data[1], -2.0, places=6)
# Tests for struct Test3C
expected = (1.0, 2.0, 3.0, 4.0)
func = dll._testfunc_array_in_struct_set_defaults_3C
func.restype = Test3C1
result = func()
# check the default values have been set properly
self.assertEqual(
(result.data[0],
result.data[1],
result.data[2],
result.data[3]),
expected
)
func = dll._testfunc_array_in_struct_set_defaults_3C
func.restype = Test3C2
result = func()
# check the default values have been set properly
self.assertEqual(
(result.data[0],
result.data[1],
result.data[2],
result.data[3]),
expected
)
func = dll._testfunc_array_in_struct_set_defaults_3C
func.restype = Test3C3
result = func()
# check the default values have been set properly
self.assertEqual((result.x, result.y, result.z, result.t), expected)
# Tests for struct Test3D
expected = (1.0, 2.0, 3.0, 4.0, 5.0)
func = dll._testfunc_array_in_struct_set_defaults_3D
func.restype = Test3D1
result = func()
# check the default values have been set properly
self.assertEqual(
(result.data[0],
result.data[1],
result.data[2],
result.data[3],
result.data[4]),
expected
)
func = dll._testfunc_array_in_struct_set_defaults_3D
func.restype = Test3D2
result = func()
# check the default values have been set properly
self.assertEqual(
(result.data[0],
result.data[1],
result.data[2],
result.data[3],
result.data[4]),
expected
)
func = dll._testfunc_array_in_struct_set_defaults_3D
func.restype = Test3D3
result = func()
# check the default values have been set properly
self.assertEqual(
(result.x,
result.y,
result.z,
result.t,
result.u),
expected)
def test_38368(self): def test_38368(self):
class U(Union): class U(Union):
_fields_ = [ _fields_ = [

View file

@ -0,0 +1 @@
Fix ctypes structs with array on Arm platform by setting ``MAX_STRUCT_SIZE`` to 32 in stgdict. Patch by Diego Russo.

View file

@ -150,6 +150,42 @@ _testfunc_array_in_struct2a(Test3B in)
return result; return result;
} }
/*
* See gh-110190. structs containing arrays of up to four floating point types
* (max 32 bytes) are passed in registers on Arm.
*/
typedef struct {
double data[4];
} Test3C;
EXPORT(Test3C)
_testfunc_array_in_struct_set_defaults_3C(void)
{
Test3C s;
s.data[0] = 1.0;
s.data[1] = 2.0;
s.data[2] = 3.0;
s.data[3] = 4.0;
return s;
}
typedef struct {
double data[5];
} Test3D;
EXPORT(Test3D)
_testfunc_array_in_struct_set_defaults_3D(void)
{
Test3D s;
s.data[0] = 1.0;
s.data[1] = 2.0;
s.data[2] = 3.0;
s.data[3] = 4.0;
s.data[4] = 5.0;
return s;
}
typedef union { typedef union {
long a_long; long a_long;
struct { struct {

View file

@ -697,29 +697,43 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
stgdict->align = total_align; stgdict->align = total_align;
stgdict->length = len; /* ADD ffi_ofs? */ stgdict->length = len; /* ADD ffi_ofs? */
/*
* 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.
* By Arm platforms it is meant both 32 and 64-bit.
*/
#if defined(__aarch64__) || defined(__arm__)
# define MAX_STRUCT_SIZE 32
#else
# define MAX_STRUCT_SIZE 16 # define MAX_STRUCT_SIZE 16
#endif
if (arrays_seen && (size <= MAX_STRUCT_SIZE)) { if (arrays_seen && (size <= MAX_STRUCT_SIZE)) {
/* /*
* See bpo-22273. Arrays are normally treated as pointers, which is * See bpo-22273 and gh-110190. Arrays are normally treated as
* fine when an array name is being passed as parameter, but not when * pointers, which is fine when an array name is being passed as
* passing structures by value that contain arrays. On 64-bit Linux, * parameter, but not when passing structures by value that contain
* small structures passed by value are passed in registers, and in * arrays.
* order to do this, libffi needs to know the true type of the array * On x86_64 Linux and Arm platforms, small structures passed by
* members of structs. Treating them as pointers breaks things. * value are passed in registers, and in order to do this, libffi needs
* to know the true type of the array members of structs. Treating them
* as pointers breaks things.
* *
* By small structures, we mean ones that are 16 bytes or less. In that * By small structures, we mean ones that are 16 bytes or less on
* case, there can't be more than 16 elements after unrolling arrays, * x86-64 and 32 bytes or less on Arm. In that case, there can't be
* as we (will) disallow bitfields. So we can collect the true ffi_type * more than 16 or 32 elements after unrolling arrays, as we (will)
* values in a fixed-size local array on the stack and, if any arrays * disallow bitfields. So we can collect the true ffi_type values in
* were seen, replace the ffi_type_pointer.elements with a more * a fixed-size local array on the stack and, if any arrays were seen,
* accurate set, to allow libffi to marshal them into registers * replace the ffi_type_pointer.elements with a more accurate set,
* correctly. It means one more loop over the fields, but if we got * to allow libffi to marshal them into registers correctly.
* here, the structure is small, so there aren't too many of those. * It means one more loop over the fields, but if we got here,
* the structure is small, so there aren't too many of those.
* *
* Although the passing in registers is specific to 64-bit Linux, the * Although the passing in registers is specific to x86_64 Linux
* array-in-struct vs. pointer problem is general. But we restrict the * and Arm platforms, the array-in-struct vs. pointer problem is
* type transformation to small structs nonetheless. * general. But we restrict the type transformation to small structs
* nonetheless.
* *
* Note that although a union may be small in terms of memory usage, it * Note that although a union may be small in terms of memory usage, it
* could contain many overlapping declarations of arrays, e.g. * could contain many overlapping declarations of arrays, e.g.
@ -745,6 +759,9 @@ PyCStructUnionType_update_stgdict(PyObject *type, PyObject *fields, int isStruct
* struct { uint_32 e1; uint_32 e2; ... uint_32 e_4; } f6; * struct { uint_32 e1; uint_32 e2; ... uint_32 e_4; } f6;
* } * }
* *
* The same principle applies for a struct 32 bytes in size like in
* the case of Arm platforms.
*
* So the struct/union needs setting up as follows: all non-array * So the struct/union needs setting up as follows: all non-array
* elements copied across as is, and all array elements replaced with * elements copied across as is, and all array elements replaced with
* an equivalent struct which has as many fields as the array has * an equivalent struct which has as many fields as the array has