]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-131747: ctypes: Deprecate _pack_ implicitly setting _layout_ = 'ms' (GH-133205)
authorPetr Viktorin <encukou@gmail.com>
Mon, 5 May 2025 13:32:06 +0000 (15:32 +0200)
committerGitHub <noreply@github.com>
Mon, 5 May 2025 13:32:06 +0000 (15:32 +0200)
On non-Windows, warn when _pack_ implicitly changes default _layout_
to 'ms'.

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
14 files changed:
Doc/deprecations/index.rst
Doc/deprecations/pending-removal-in-3.19.rst [new file with mode: 0644]
Doc/library/ctypes.rst
Doc/whatsnew/3.14.rst
Lib/ctypes/_layout.py
Lib/test/test_ctypes/test_aligned_structures.py
Lib/test/test_ctypes/test_bitfields.py
Lib/test/test_ctypes/test_byteswap.py
Lib/test/test_ctypes/test_generated_structs.py
Lib/test/test_ctypes/test_pep3118.py
Lib/test/test_ctypes/test_structunion.py
Lib/test/test_ctypes/test_structures.py
Lib/test/test_ctypes/test_unaligned_structures.py
Misc/NEWS.d/next/C_API/2025-04-28-18-26-37.gh-issue-131747.2AiQ9n.rst [new file with mode: 0644]

index bb78f7b36071b80682764d59a02d4e04480f4622..d064f2bec42c229225dfeccd73bdd234cab2632c 100644 (file)
@@ -7,6 +7,8 @@ Deprecations
 
 .. include:: pending-removal-in-3.17.rst
 
+.. include:: pending-removal-in-3.19.rst
+
 .. include:: pending-removal-in-future.rst
 
 C API deprecations
diff --git a/Doc/deprecations/pending-removal-in-3.19.rst b/Doc/deprecations/pending-removal-in-3.19.rst
new file mode 100644 (file)
index 0000000..3936f63
--- /dev/null
@@ -0,0 +1,8 @@
+Pending removal in Python 3.19
+------------------------------
+
+* :mod:`ctypes`:
+
+  * Implicitly switching to the MSVC-compatible struct layout by setting
+    :attr:`~ctypes.Structure._pack_` but not :attr:`~ctypes.Structure._layout_`
+    on non-Windows platforms.
index 2825590400c70b336248684627311103ea8a232e..1b78b33b69f8da7c8da9dc0f1b33326b7876b612 100644 (file)
@@ -2754,6 +2754,16 @@ fields, or any other data types containing pointer type fields.
       when :attr:`_fields_` is assigned, otherwise it will have no effect.
       Setting this attribute to 0 is the same as not setting it at all.
 
+      This is only implemented for the MSVC-compatible memory layout.
+
+      .. deprecated-removed:: next 3.19
+
+         For historical reasons, if :attr:`!_pack_` is non-zero,
+         the MSVC-compatible layout will be used by default.
+         On non-Windows platforms, this default is deprecated and is slated to
+         become an error in Python 3.19.
+         If it is intended, set :attr:`~Structure._layout_` to ``'ms'``
+         explicitly.
 
    .. attribute:: _align_
 
@@ -2782,12 +2792,15 @@ fields, or any other data types containing pointer type fields.
       Currently the default will be:
 
       - On Windows: ``"ms"``
-      - When :attr:`~Structure._pack_` is specified: ``"ms"``
+      - When :attr:`~Structure._pack_` is specified: ``"ms"``.
+        (This is deprecated; see :attr:`~Structure._pack_` documentation.)
       - Otherwise: ``"gcc-sysv"``
 
       :attr:`!_layout_` must already be defined when
       :attr:`~Structure._fields_` is assigned, otherwise it will have no effect.
 
+      .. versionadded:: next
+
    .. attribute:: _anonymous_
 
       An optional sequence that lists the names of unnamed (anonymous) fields.
index 330140010160f6960dd282c7c8594697c9bb588f..d75ba9ab97373c6b2551d058f80715590c75c52e 100644 (file)
@@ -1874,6 +1874,12 @@ Deprecated
   :func:`codecs.open` is now deprecated. Use :func:`open` instead.
   (Contributed by Inada Naoki in :gh:`133036`.)
 
+* :mod:`ctypes`:
+  On non-Windows platforms, setting :attr:`.Structure._pack_` to use a
+  MSVC-compatible default memory layout is deprecated in favor of setting
+  :attr:`.Structure._layout_` to ``'ms'``.
+  (Contributed by Petr Viktorin in :gh:`131747`.)
+
 * :mod:`ctypes`:
   Calling :func:`ctypes.POINTER` on a string is deprecated.
   Use :ref:`ctypes-incomplete-types` for self-referential structures.
@@ -1948,6 +1954,8 @@ Deprecated
 
 .. include:: ../deprecations/pending-removal-in-3.17.rst
 
+.. include:: ../deprecations/pending-removal-in-3.19.rst
+
 .. include:: ../deprecations/pending-removal-in-future.rst
 
 Removed
index 0719e72cfed312cd53da92227ed17f92a3e5b2b0..2048ccb6a1c93fff4613c9b9cbc13c110be0cb87 100644 (file)
@@ -5,6 +5,7 @@ may change at any time.
 """
 
 import sys
+import warnings
 
 from _ctypes import CField, buffer_info
 import ctypes
@@ -66,9 +67,26 @@ def get_layout(cls, input_fields, is_struct, base):
 
     # For clarity, variables that count bits have `bit` in their names.
 
+    pack = getattr(cls, '_pack_', None)
+
     layout = getattr(cls, '_layout_', None)
     if layout is None:
-        if sys.platform == 'win32' or getattr(cls, '_pack_', None):
+        if sys.platform == 'win32':
+            gcc_layout = False
+        elif pack:
+            if is_struct:
+                base_type_name = 'Structure'
+            else:
+                base_type_name = 'Union'
+            warnings._deprecated(
+                '_pack_ without _layout_',
+                f"Due to '_pack_', the '{cls.__name__}' {base_type_name} will "
+                + "use memory layout compatible with MSVC (Windows). "
+                + "If this is intended, set _layout_ to 'ms'. "
+                + "The implicit default is deprecated and slated to become "
+                + "an error in Python {remove}.",
+                remove=(3, 19),
+            )
             gcc_layout = False
         else:
             gcc_layout = True
@@ -95,7 +113,6 @@ def get_layout(cls, input_fields, is_struct, base):
     else:
         big_endian = sys.byteorder == 'big'
 
-    pack = getattr(cls, '_pack_', None)
     if pack is not None:
         try:
             pack = int(pack)
index 0c563ab80559a6f7fc481aad8d37433d76758523..50b4d729b9db8adad97413d8d018d34110b5857e 100644 (file)
@@ -316,6 +316,7 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
 
             class Main(sbase):
                 _pack_ = 1
+                _layout_ = "ms"
                 _fields_ = [
                     ("a", c_ubyte),
                     ("b", Inner),
index dc81e752567c421874b8594086b335db659d8f4b..518f838219eba002ccbcb83722f1189a712038d7 100644 (file)
@@ -430,6 +430,7 @@ class BitFieldTest(unittest.TestCase, StructCheckMixin):
     def test_gh_84039(self):
         class Bad(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [
                 ("a0", c_uint8, 1),
                 ("a1", c_uint8, 1),
@@ -443,9 +444,9 @@ class BitFieldTest(unittest.TestCase, StructCheckMixin):
                 ("b1", c_uint16, 12),
             ]
 
-
         class GoodA(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [
                 ("a0", c_uint8, 1),
                 ("a1", c_uint8, 1),
@@ -460,6 +461,7 @@ class BitFieldTest(unittest.TestCase, StructCheckMixin):
 
         class Good(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [
                 ("a", GoodA),
                 ("b0", c_uint16, 4),
@@ -475,6 +477,7 @@ class BitFieldTest(unittest.TestCase, StructCheckMixin):
     def test_gh_73939(self):
         class MyStructure(Structure):
             _pack_      = 1
+            _layout_ = "ms"
             _fields_    = [
                             ("P",       c_uint16),
                             ("L",       c_uint16, 9),
index 9f9904282e451a0e18d05c08aa422402a562fcd0..ea5951603f93245ef616fe3074023a585aaf29ab 100644 (file)
@@ -269,6 +269,7 @@ class Test(unittest.TestCase, StructCheckMixin):
 
         class S(base):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [("b", c_byte),
                         ("h", c_short),
 
@@ -296,6 +297,7 @@ class Test(unittest.TestCase, StructCheckMixin):
 
         class S(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [("b", c_byte),
 
                         ("h", c_short),
index 9a8102219d8769a0b5bb728a48710c6ec74c2384..aa448fad5bbae607960be7d51c0e1332548c195c 100644 (file)
@@ -125,18 +125,21 @@ class Nested(Structure):
 class Packed1(Structure):
     _fields_ = [('a', c_int8), ('b', c_int64)]
     _pack_ = 1
+    _layout_ = 'ms'
 
 
 @register()
 class Packed2(Structure):
     _fields_ = [('a', c_int8), ('b', c_int64)]
     _pack_ = 2
+    _layout_ = 'ms'
 
 
 @register()
 class Packed3(Structure):
     _fields_ = [('a', c_int8), ('b', c_int64)]
     _pack_ = 4
+    _layout_ = 'ms'
 
 
 @register()
@@ -155,6 +158,7 @@ class Packed4(Structure):
 
     _fields_ = [('a', c_int8), ('b', c_int64)]
     _pack_ = 8
+    _layout_ = 'ms'
 
 @register()
 class X86_32EdgeCase(Structure):
@@ -366,6 +370,7 @@ class Example_gh_95496(Structure):
 @register()
 class Example_gh_84039_bad(Structure):
     _pack_ = 1
+    _layout_ = 'ms'
     _fields_ = [("a0", c_uint8, 1),
                 ("a1", c_uint8, 1),
                 ("a2", c_uint8, 1),
@@ -380,6 +385,7 @@ class Example_gh_84039_bad(Structure):
 @register()
 class Example_gh_84039_good_a(Structure):
     _pack_ = 1
+    _layout_ = 'ms'
     _fields_ = [("a0", c_uint8, 1),
                 ("a1", c_uint8, 1),
                 ("a2", c_uint8, 1),
@@ -392,6 +398,7 @@ class Example_gh_84039_good_a(Structure):
 @register()
 class Example_gh_84039_good(Structure):
     _pack_ = 1
+    _layout_ = 'ms'
     _fields_ = [("a", Example_gh_84039_good_a),
                 ("b0", c_uint16, 4),
                 ("b1", c_uint16, 12)]
@@ -399,6 +406,7 @@ class Example_gh_84039_good(Structure):
 @register()
 class Example_gh_73939(Structure):
     _pack_ = 1
+    _layout_ = 'ms'
     _fields_ = [("P", c_uint16),
                 ("L", c_uint16, 9),
                 ("Pro", c_uint16, 1),
@@ -419,6 +427,7 @@ class Example_gh_86098(Structure):
 @register()
 class Example_gh_86098_pack(Structure):
     _pack_ = 1
+    _layout_ = 'ms'
     _fields_ = [("a", c_uint8, 8),
                 ("b", c_uint8, 8),
                 ("c", c_uint32, 16)]
@@ -528,7 +537,7 @@ def dump_ctype(tp, struct_or_union_tag='', variable_name='', semi=''):
             pushes.append(f'#pragma pack(push, {pack})')
             pops.append(f'#pragma pack(pop)')
         layout = getattr(tp, '_layout_', None)
-        if layout == 'ms' or pack:
+        if layout == 'ms':
             # The 'ms_struct' attribute only works on x86 and PowerPC
             requires.add(
                 'defined(MS_WIN32) || ('
index 06b2ccecade44e6308f6ae7bdef87a6eceff3a1c..11a0744f5a8e365dae0c88d37ef53fc9888aa888 100644 (file)
@@ -81,6 +81,7 @@ class Point(Structure):
 
 class PackedPoint(Structure):
     _pack_ = 2
+    _layout_ = 'ms'
     _fields_ = [("x", c_long), ("y", c_long)]
 
 class PointMidPad(Structure):
@@ -88,6 +89,7 @@ class PointMidPad(Structure):
 
 class PackedPointMidPad(Structure):
     _pack_ = 2
+    _layout_ = 'ms'
     _fields_ = [("x", c_byte), ("y", c_uint64)]
 
 class PointEndPad(Structure):
@@ -95,6 +97,7 @@ class PointEndPad(Structure):
 
 class PackedPointEndPad(Structure):
     _pack_ = 2
+    _layout_ = 'ms'
     _fields_ = [("x", c_uint64), ("y", c_byte)]
 
 class Point2(Structure):
index 8d8b7e5e99513220a33715045ffc43d2d92cdc60..5b21d48d99cef7be3405e9e3bcacfda02a7b5a12 100644 (file)
@@ -11,6 +11,8 @@ from ._support import (_CData, PyCStructType, UnionType,
                        Py_TPFLAGS_DISALLOW_INSTANTIATION,
                        Py_TPFLAGS_IMMUTABLETYPE)
 from struct import calcsize
+import contextlib
+from test.support import MS_WINDOWS
 
 
 class StructUnionTestBase:
@@ -335,6 +337,22 @@ class StructUnionTestBase:
         self.assertIn("from_address", dir(type(self.cls)))
         self.assertIn("in_dll", dir(type(self.cls)))
 
+    def test_pack_layout_switch(self):
+        # Setting _pack_ implicitly sets default layout to MSVC;
+        # this is deprecated on non-Windows platforms.
+        if MS_WINDOWS:
+            warn_context = contextlib.nullcontext()
+        else:
+            warn_context = self.assertWarns(DeprecationWarning)
+        with warn_context:
+            class X(self.cls):
+                _pack_ = 1
+                # _layout_ missing
+                _fields_ = [('a', c_int8, 1), ('b', c_int16, 2)]
+
+        # Check MSVC layout (bitfields of different types aren't combined)
+        self.check_sizeof(X, struct_size=3, union_size=2)
+
 
 class StructureTestCase(unittest.TestCase, StructUnionTestBase):
     cls = Structure
index 221319642e8f3b77791e0cf0f4e511611b238e99..92d4851d739d47ba3654b105462ef93b0101d74f 100644 (file)
@@ -25,6 +25,7 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin):
             _fields_ = [("a", c_byte),
                         ("b", c_longlong)]
             _pack_ = 1
+            _layout_ = 'ms'
         self.check_struct(X)
 
         self.assertEqual(sizeof(X), 9)
@@ -34,6 +35,7 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin):
             _fields_ = [("a", c_byte),
                         ("b", c_longlong)]
             _pack_ = 2
+            _layout_ = 'ms'
         self.check_struct(X)
         self.assertEqual(sizeof(X), 10)
         self.assertEqual(X.b.offset, 2)
@@ -45,6 +47,7 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin):
             _fields_ = [("a", c_byte),
                         ("b", c_longlong)]
             _pack_ = 4
+            _layout_ = 'ms'
         self.check_struct(X)
         self.assertEqual(sizeof(X), min(4, longlong_align) + longlong_size)
         self.assertEqual(X.b.offset, min(4, longlong_align))
@@ -53,27 +56,33 @@ class StructureTestCase(unittest.TestCase, StructCheckMixin):
             _fields_ = [("a", c_byte),
                         ("b", c_longlong)]
             _pack_ = 8
+            _layout_ = 'ms'
         self.check_struct(X)
 
         self.assertEqual(sizeof(X), min(8, longlong_align) + longlong_size)
         self.assertEqual(X.b.offset, min(8, longlong_align))
 
-
-        d = {"_fields_": [("a", "b"),
-                          ("b", "q")],
-             "_pack_": -1}
-        self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
+        with self.assertRaises(ValueError):
+            class X(Structure):
+                _fields_ = [("a", "b"), ("b", "q")]
+                _pack_ = -1
+                _layout_ = "ms"
 
     @support.cpython_only
     def test_packed_c_limits(self):
         # Issue 15989
         import _testcapi
-        d = {"_fields_": [("a", c_byte)],
-             "_pack_": _testcapi.INT_MAX + 1}
-        self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
-        d = {"_fields_": [("a", c_byte)],
-             "_pack_": _testcapi.UINT_MAX + 2}
-        self.assertRaises(ValueError, type(Structure), "X", (Structure,), d)
+        with self.assertRaises(ValueError):
+            class X(Structure):
+                _fields_ = [("a", c_byte)]
+                _pack_ = _testcapi.INT_MAX + 1
+                _layout_ = "ms"
+
+        with self.assertRaises(ValueError):
+            class X(Structure):
+                _fields_ = [("a", c_byte)]
+                _pack_ = _testcapi.UINT_MAX + 2
+                _layout_ = "ms"
 
     def test_initializers(self):
         class Person(Structure):
index 58a00597ef5cc43ef56aaa593fab3f4e15ed9579..b5fb4c0df774538e3ef9966c68b28b992598289a 100644 (file)
@@ -19,10 +19,12 @@ for typ in [c_short, c_int, c_long, c_longlong,
             c_ushort, c_uint, c_ulong, c_ulonglong]:
     class X(Structure):
         _pack_ = 1
+        _layout_ = 'ms'
         _fields_ = [("pad", c_byte),
                     ("value", typ)]
     class Y(SwappedStructure):
         _pack_ = 1
+        _layout_ = 'ms'
         _fields_ = [("pad", c_byte),
                     ("value", typ)]
     structures.append(X)
diff --git a/Misc/NEWS.d/next/C_API/2025-04-28-18-26-37.gh-issue-131747.2AiQ9n.rst b/Misc/NEWS.d/next/C_API/2025-04-28-18-26-37.gh-issue-131747.2AiQ9n.rst
new file mode 100644 (file)
index 0000000..999b264
--- /dev/null
@@ -0,0 +1,4 @@
+On non-Windows platforms, deprecate using :attr:`ctypes.Structure._pack_` to
+use a Windows-compatible layout on non-Windows platforms. The layout should
+be specified explicitly by setting :attr:`ctypes.Structure._layout_` to
+``'ms'``.