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 <zintensitydev@gmail.com>
This commit is contained in:
Petr Viktorin 2025-05-05 15:32:06 +02:00 committed by GitHub
parent f554237b8e
commit 59f78d7b06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 115 additions and 16 deletions

View 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

View file

@ -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.

View 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.

View 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

View 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)

View file

@ -316,6 +316,7 @@ class TestAlignedStructures(unittest.TestCase, StructCheckMixin):
class Main(sbase):
_pack_ = 1
_layout_ = "ms"
_fields_ = [
("a", c_ubyte),
("b", Inner),

View 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),

View 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),

View 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) || ('

View 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):

View 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

View 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):

View 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)

View file

@ -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'``.