https://github.com/python/cpython/commit/59f78d7b0609063d5eb7f2c2744540b47a8398fc
commit: 59f78d7b0609063d5eb7f2c2744540b47a8398fc
branch: main
author: Petr Viktorin <encu...@gmail.com>
committer: encukou <encu...@gmail.com>
date: 2025-05-05T15:32:06+02:00
summary:

gh-131747: ctypes: Deprecate _pack_ implicitly setting _layout_ = 'ms' 
(GH-133205)

On non-Windows, warn when _pack_ implicitly changes default _layout_
to 'ms'.

Co-authored-by: Peter Bierma <zintensity...@gmail.com>

files:
A Doc/deprecations/pending-removal-in-3.19.rst
A Misc/NEWS.d/next/C_API/2025-04-28-18-26-37.gh-issue-131747.2AiQ9n.rst
M Doc/deprecations/index.rst
M Doc/library/ctypes.rst
M Doc/whatsnew/3.14.rst
M Lib/ctypes/_layout.py
M Lib/test/test_ctypes/test_aligned_structures.py
M Lib/test/test_ctypes/test_bitfields.py
M Lib/test/test_ctypes/test_byteswap.py
M Lib/test/test_ctypes/test_generated_structs.py
M Lib/test/test_ctypes/test_pep3118.py
M Lib/test/test_ctypes/test_structunion.py
M Lib/test/test_ctypes/test_structures.py
M Lib/test/test_ctypes/test_unaligned_structures.py

diff --git a/Doc/deprecations/index.rst b/Doc/deprecations/index.rst
index bb78f7b36071b8..d064f2bec42c22 100644
--- a/Doc/deprecations/index.rst
+++ b/Doc/deprecations/index.rst
@@ -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
index 00000000000000..3936f63ca5b5af
--- /dev/null
+++ b/Doc/deprecations/pending-removal-in-3.19.rst
@@ -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.
diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst
index 2825590400c70b..1b78b33b69f8da 100644
--- a/Doc/library/ctypes.rst
+++ b/Doc/library/ctypes.rst
@@ -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.
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 330140010160f6..d75ba9ab97373c 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -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
diff --git a/Lib/ctypes/_layout.py b/Lib/ctypes/_layout.py
index 0719e72cfed312..2048ccb6a1c93f 100644
--- a/Lib/ctypes/_layout.py
+++ b/Lib/ctypes/_layout.py
@@ -5,6 +5,7 @@
 """
 
 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)
diff --git a/Lib/test/test_ctypes/test_aligned_structures.py 
b/Lib/test/test_ctypes/test_aligned_structures.py
index 0c563ab80559a6..50b4d729b9db8a 100644
--- a/Lib/test/test_ctypes/test_aligned_structures.py
+++ b/Lib/test/test_ctypes/test_aligned_structures.py
@@ -316,6 +316,7 @@ class Inner(sbase):
 
             class Main(sbase):
                 _pack_ = 1
+                _layout_ = "ms"
                 _fields_ = [
                     ("a", c_ubyte),
                     ("b", Inner),
diff --git a/Lib/test/test_ctypes/test_bitfields.py 
b/Lib/test/test_ctypes/test_bitfields.py
index dc81e752567c42..518f838219eba0 100644
--- a/Lib/test/test_ctypes/test_bitfields.py
+++ b/Lib/test/test_ctypes/test_bitfields.py
@@ -430,6 +430,7 @@ class TestStruct(Structure):
     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 Bad(Structure):
                 ("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 GoodA(Structure):
 
         class Good(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [
                 ("a", GoodA),
                 ("b0", c_uint16, 4),
@@ -475,6 +477,7 @@ class Good(Structure):
     def test_gh_73939(self):
         class MyStructure(Structure):
             _pack_      = 1
+            _layout_ = "ms"
             _fields_    = [
                             ("P",       c_uint16),
                             ("L",       c_uint16, 9),
diff --git a/Lib/test/test_ctypes/test_byteswap.py 
b/Lib/test/test_ctypes/test_byteswap.py
index 9f9904282e451a..ea5951603f9324 100644
--- a/Lib/test/test_ctypes/test_byteswap.py
+++ b/Lib/test/test_ctypes/test_byteswap.py
@@ -269,6 +269,7 @@ def test_unaligned_nonnative_struct_fields(self):
 
         class S(base):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [("b", c_byte),
                         ("h", c_short),
 
@@ -296,6 +297,7 @@ def test_unaligned_native_struct_fields(self):
 
         class S(Structure):
             _pack_ = 1
+            _layout_ = "ms"
             _fields_ = [("b", c_byte),
 
                         ("h", c_short),
diff --git a/Lib/test/test_ctypes/test_generated_structs.py 
b/Lib/test/test_ctypes/test_generated_structs.py
index 9a8102219d8769..aa448fad5bbae6 100644
--- a/Lib/test/test_ctypes/test_generated_structs.py
+++ b/Lib/test/test_ctypes/test_generated_structs.py
@@ -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 @@ def _maybe_skip():
 
     _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) || ('
diff --git a/Lib/test/test_ctypes/test_pep3118.py 
b/Lib/test/test_ctypes/test_pep3118.py
index 06b2ccecade44e..11a0744f5a8e36 100644
--- a/Lib/test/test_ctypes/test_pep3118.py
+++ b/Lib/test/test_ctypes/test_pep3118.py
@@ -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):
diff --git a/Lib/test/test_ctypes/test_structunion.py 
b/Lib/test/test_ctypes/test_structunion.py
index 8d8b7e5e995132..5b21d48d99cef7 100644
--- a/Lib/test/test_ctypes/test_structunion.py
+++ b/Lib/test/test_ctypes/test_structunion.py
@@ -11,6 +11,8 @@
                        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 @@ def test_methods(self):
         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
diff --git a/Lib/test/test_ctypes/test_structures.py 
b/Lib/test/test_ctypes/test_structures.py
index 221319642e8f3b..92d4851d739d47 100644
--- a/Lib/test/test_ctypes/test_structures.py
+++ b/Lib/test/test_ctypes/test_structures.py
@@ -25,6 +25,7 @@ class X(Structure):
             _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 X(Structure):
             _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 X(Structure):
             _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 X(Structure):
             _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):
diff --git a/Lib/test/test_ctypes/test_unaligned_structures.py 
b/Lib/test/test_ctypes/test_unaligned_structures.py
index 58a00597ef5cc4..b5fb4c0df77453 100644
--- a/Lib/test/test_ctypes/test_unaligned_structures.py
+++ b/Lib/test/test_ctypes/test_unaligned_structures.py
@@ -19,10 +19,12 @@
             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
index 00000000000000..999b2642cb7bc1
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-04-28-18-26-37.gh-issue-131747.2AiQ9n.rst
@@ -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'``.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to