Skip to content

Commit 2af2c97

Browse files
authored
Implement _layout_ on ctypes structs (#1937)
1 parent ce434b5 commit 2af2c97

File tree

2 files changed

+79
-10
lines changed

2 files changed

+79
-10
lines changed

src/core/IronPython.Modules/_ctypes/StructType.cs

+28-8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ public static partial class CTypes {
3333
/// </summary>
3434
[PythonType, PythonHidden]
3535
public class StructType : PythonType, INativeType {
36+
37+
private enum LayoutKind { Msvc, Gcc }
38+
3639
[DisallowNull]
3740
internal Field[]? _fields; // not null after type construction completes
3841
private int? _size, _alignment, _pack;
@@ -252,6 +255,7 @@ internal static PythonType MakeSystemType(Type underlyingSystemType) {
252255
return PythonType.SetPythonType(underlyingSystemType, new StructType(underlyingSystemType));
253256
}
254257

258+
255259
[MemberNotNull(nameof(_fields), nameof(_size), nameof(_alignment))]
256260
private void SetFields(object? fields) {
257261
lock (this) {
@@ -263,11 +267,12 @@ private void SetFields(object? fields) {
263267
List<Field> allFields = GetBaseSizeAlignmentAndFields(out int size, out int alignment);
264268

265269
IList<string>? anonFields = GetAnonymousFields(this);
270+
LayoutKind layout = GetStructLayout(this);
266271

267272
foreach (object fieldDef in fieldDefList) {
268273
GetFieldInfo(this, fieldDef, out string fieldName, out INativeType cdata, out bitCount);
269274

270-
int fieldOffset = UpdateSizeAndAlignment(cdata, bitCount, ref lastType, ref size, ref alignment, ref curBitCount);
275+
int fieldOffset = UpdateSizeAndAlignment(cdata, bitCount, layout, ref lastType, ref size, ref alignment, ref curBitCount);
271276

272277
var newField = new Field(fieldName, cdata, fieldOffset, allFields.Count, bitCount, curBitCount - bitCount);
273278
allFields.Add(newField);
@@ -292,6 +297,7 @@ private void SetFields(object? fields) {
292297
}
293298
}
294299

300+
295301
internal static void CheckAnonymousFields(List<Field> allFields, IList<string>? anonFields) {
296302
if (anonFields != null) {
297303
foreach (string s in anonFields) {
@@ -365,9 +371,10 @@ private List<Field> GetBaseSizeAlignmentAndFields(out int size, out int alignmen
365371
foreach (PythonType pt in BaseTypes) {
366372
if (pt is StructType st) {
367373
st.EnsureFinal();
374+
LayoutKind layout = GetStructLayout(st);
368375
foreach (Field f in st._fields) {
369376
allFields.Add(f);
370-
UpdateSizeAndAlignment(f.NativeType, f.BitCount, ref lastType, ref size, ref alignment, ref totalBitCount);
377+
UpdateSizeAndAlignment(f.NativeType, f.BitCount, layout, ref lastType, ref size, ref alignment, ref totalBitCount);
371378

372379
if (f.NativeType == this) {
373380
throw StructureCannotContainSelf();
@@ -404,7 +411,7 @@ private List<Field> GetBaseSizeAlignmentAndFields(out int size, out int alignmen
404411
/// On return, the count is updated with the number of occupied bits.</param>
405412
/// <returns>
406413
/// The offset of the processed field within the struct. If the processed field was a bitfield, this is the offset of its container unit.</returns>
407-
private int UpdateSizeAndAlignment(INativeType cdata, int? bitCount, ref INativeType? lastType, ref int size, ref int alignment, ref int? totalBitCount) {
414+
private int UpdateSizeAndAlignment(INativeType cdata, int? bitCount, LayoutKind layout, ref INativeType? lastType, ref int size, ref int alignment, ref int? totalBitCount) {
408415
int fieldOffset;
409416
if (bitCount != null) {
410417
// process a bitfield
@@ -413,7 +420,7 @@ private int UpdateSizeAndAlignment(INativeType cdata, int? bitCount, ref INative
413420

414421
if (_pack != null) throw new NotImplementedException("pack with bitfields"); // TODO: implement
415422

416-
if (UseMsvcBitfieldAlignmentRules) {
423+
if (layout is LayoutKind.Msvc) {
417424
if (totalBitCount != null) { // there is already a bitfield container open
418425
// under the MSVC rules, only bitfields of type that has the same size/alignment, are packed into the same container unit
419426
if (lastType!.Size != cdata.Size || lastType.Alignment != cdata.Alignment) {
@@ -443,7 +450,7 @@ private int UpdateSizeAndAlignment(INativeType cdata, int? bitCount, ref INative
443450
totalBitCount = bitCount;
444451
lastType = cdata;
445452
}
446-
} else { // GCC bitfield alignment rules
453+
} else if (layout is LayoutKind.Gcc) {
447454
// under the GCC rules, all bitfields are packed into the same container unit or an overlapping container unit of a different type,
448455
// as long as they fit and match the alignment
449456
int containerOffset = AlignBack(size, cdata.Alignment); // TODO: _pack
@@ -460,6 +467,8 @@ private int UpdateSizeAndAlignment(INativeType cdata, int? bitCount, ref INative
460467
fieldOffset = size = containerOffset;
461468
totalBitCount = containerBitCount + bitCount;
462469
lastType = cdata;
470+
} else {
471+
throw new InvalidOperationException("unknown layout kind");
463472
}
464473
alignment = Math.Max(alignment, lastType!.Alignment); // TODO: _pack
465474
} else {
@@ -500,6 +509,20 @@ internal void EnsureFinal() {
500509
}
501510
}
502511

512+
513+
private static LayoutKind GetStructLayout(PythonType type) {
514+
if (type.TryGetBoundAttr(type.Context.SharedContext, type, "_layout_", out object layout) && layout is not null) {
515+
if (Converter.TryConvertToString(layout, out string? layoutName)) {
516+
if (layoutName.StartsWith("ms", StringComparison.Ordinal)) return LayoutKind.Msvc;
517+
if (layoutName.StartsWith("gcc", StringComparison.Ordinal)) return LayoutKind.Gcc;
518+
}
519+
throw PythonOps.ValueError("unknown _layout_: {0}", layout);
520+
}
521+
// default layout for structs is platform dependent
522+
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? LayoutKind.Msvc : LayoutKind.Gcc;
523+
}
524+
525+
503526
/// <summary>
504527
/// If our size/alignment hasn't been initialized then grabs the size/alignment
505528
/// from all of our base classes. If later new _fields_ are added we'll be
@@ -525,9 +548,6 @@ private void EnsureSizeAndAlignment() {
525548

526549
private static int AlignBack(int length, int size)
527550
=> length & ~(size - 1);
528-
529-
private static bool UseMsvcBitfieldAlignmentRules
530-
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
531551
}
532552
}
533553
}

tests/suite/modules/type_related/test_ctypes.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,12 @@ class Test(Structure):
507507
]
508508

509509
self.check_bitfield(Test.a, c_longlong, 0, 0, 20)
510-
if is_posix and (is_cli or sys.version_info < (3, 14)): # CPython 3.14 implements MSVC behaviour (bug)
510+
if is_posix and (is_cli or sys.version_info < (3, 14)): # CPython 3.14 implements MSVC behaviour when _layout_ is not set
511511
if is_cli: # GCC-compliant results
512512
self.check_bitfield(Test.b, c_short, 2, 4, 2)
513513
self.check_bitfield(Test.c, c_short, 2, 6, 15)
514514
self.assertEqual(sizeof(Test), 5)
515-
else: # bug in CPython
515+
else: # https://github.com/python/cpython/issues/131747
516516
self.check_bitfield(Test.b, c_short, 6, 20, 2)
517517
self.check_bitfield(Test.c, c_short, 6, 22, 15)
518518
self.assertEqual(sizeof(Test), 8)
@@ -905,6 +905,55 @@ class Test(Structure):
905905
self.assertEqual(sizeof(Test), 16)
906906

907907

908+
@unittest.skipUnless(is_cli or sys.version_info >= (3, 14), '_layout_ not supported before CPython 3.14')
909+
def test_bitfield_mixed_H3_layout_msvc(self):
910+
class Test(Structure):
911+
_layout_ = 'ms'
912+
_fields_ = [
913+
("a", c_longlong, 52),
914+
("b", c_byte, 5),
915+
]
916+
917+
self.check_bitfield(Test.a, c_longlong, 0, 0, 52)
918+
self.check_bitfield(Test.b, c_byte, 8, 0, 5)
919+
self.assertEqual(sizeof(Test), 16)
920+
921+
922+
@unittest.skipUnless(is_cli or sys.version_info >= (3, 14), '_layout_ not supported before CPython 3.14')
923+
def test_bitfield_mixed_H3_layout_gcc(self):
924+
class Test(Structure):
925+
_layout_ = 'gcc-sysv'
926+
_fields_ = [
927+
("a", c_longlong, 52),
928+
("b", c_byte, 5),
929+
]
930+
931+
self.check_bitfield(Test.a, c_longlong, 0, 0, 52)
932+
self.check_bitfield(Test.b, c_byte, 7, 0, 5)
933+
self.assertEqual(sizeof(Test), 8)
934+
935+
936+
@unittest.skipUnless(is_cli or sys.version_info >= (3, 14), '_layout_ not supported before CPython 3.14')
937+
def test_bitfield_mixed_H3_layout_default(self):
938+
class Test(Structure):
939+
_layout_ = None
940+
_fields_ = [
941+
("a", c_longlong, 52),
942+
("b", c_byte, 5),
943+
]
944+
self.assertEqual(sizeof(Test), 8 if is_posix else 16)
945+
946+
947+
@unittest.skipUnless(is_cli or sys.version_info >= (3, 14), '_layout_ not supported before CPython 3.14')
948+
def test_bitfield_mixed_H3_layout_invalid(self):
949+
with self.assertRaises(ValueError) as context:
950+
class Test(Structure):
951+
_layout_ = "invalid"
952+
_fields_ = []
953+
954+
self.assertIn("unknown _layout_", str(context.exception))
955+
956+
908957
@unittest.skipIf(is_posix, 'Windows specific test')
909958
def test_loadlibrary_error(self):
910959
with self.assertRaises(OSError) as cm:

0 commit comments

Comments
 (0)