diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 0a50912ff0ea8c..4d7a3cbff5567d 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -68,7 +68,7 @@ "BrokenIter", "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", - "reset_code", "on_github_actions" + "reset_code", "on_github_actions", "ctypes_py_buffer", ] @@ -3184,3 +3184,47 @@ def linked_to_musl(): return _linked_to_musl _linked_to_musl = tuple(map(int, version.split('.'))) return _linked_to_musl + + +try: + import ctypes + + class _py_buffer(ctypes.Structure): + _fields_ = [ + ("buf", ctypes.c_void_p), + ("obj", ctypes.py_object), + ("len", ctypes.c_ssize_t), + ("itemsize", ctypes.c_ssize_t), + ("readonly", ctypes.c_int), + ("ndim", ctypes.c_int), + ("format", ctypes.c_char_p), + ("shape", ctypes.POINTER(ctypes.c_ssize_t)), + ("strides", ctypes.POINTER(ctypes.c_ssize_t)), + ("suboffsets", ctypes.POINTER(ctypes.c_ssize_t)), + ("internal", ctypes.c_void_p), + ] +except ImportError: + _py_buffer = None + + +@contextlib.contextmanager +def ctypes_py_buffer(ob, flags=inspect.BufferFlags.SIMPLE): + """ + Safely acquire a `Py_buffer` as a ctypes struct. + + `ob` must implement the buffer protocol, and the retrieved buffer is + released on exit of the context manager. + + Skips any test using it if `ctypes` is unavailable. + """ + from .import_helper import import_module + + ctypes = import_module("ctypes") + buf = _py_buffer() + ctypes.pythonapi.PyObject_GetBuffer(ctypes.py_object(ob), + ctypes.byref(buf), + ctypes.c_int(flags)) + try: + yield buf + finally: + ctypes.pythonapi.PyBuffer_Release(ctypes.byref(buf)) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..420186fb8bf1b3 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -8,6 +8,7 @@ from test.support import import_helper from test.support import os_helper from test.support import _2G +from test.support import ctypes_py_buffer import weakref import pickle import operator @@ -67,6 +68,21 @@ def test_empty(self): a += a self.assertEqual(len(a), 0) + def test_empty_alignment(self): + # gh-140557: pointer alignment of empty allocation + ctypes = import_helper.import_module("ctypes") + self.enterContext(warnings.catch_warnings()) + warnings.filterwarnings( + "ignore", + message="The 'u' type code is deprecated and " + "will be removed in Python 3.16", + category=DeprecationWarning) + max_align = ctypes.alignment(ctypes.c_longdouble) + for typecode in typecodes: + a = array.array(typecode) + with ctypes_py_buffer(a) as buf: + self.assertEqual(buf.buf % max_align, 0) + # Machine format codes. # diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index fe3e391a7f5ba1..e585a5852175bd 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -38,6 +38,7 @@ from test.support.testcase import ComplexesAreIdenticalMixin from test.support.warnings_helper import check_warnings from test.support import requires_IEEE_754 +from test.support import ctypes_py_buffer from unittest.mock import MagicMock, patch try: import pty, signal @@ -2404,6 +2405,14 @@ def iterator(): yield b'B' self.assertEqual(bytearray(b'A,B'), array.join(iterator())) + def test_bytearray_empty_alignment(self): + # gh-140557: alignment of pointer in empty allocation + ctypes = import_module("ctypes") + max_align = ctypes.alignment(ctypes.c_longdouble) + array = bytearray() + with ctypes_py_buffer(array) as buf: + self.assertEqual(buf.buf % max_align, 0) + def test_construct_singletons(self): for const in None, Ellipsis, NotImplemented: tp = type(const) diff --git a/Misc/ACKS b/Misc/ACKS index f5f15f2eb7ea24..5bfbc38587b58e 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1148,6 +1148,7 @@ Per Lindqvist Eric Lindvall Gregor Lingl Everett Lipman +Jake Lishman Mirko Liss Alexander Liu Hui Liu diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst new file mode 100644 index 00000000000000..cceebc7ae49513 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -0,0 +1,2 @@ +:func:`bytearray` and :func:`array.array` now default to aligned buffers when +empty. Unaligned buffers can still be created by slicing. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..1129314067e15e 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -2655,7 +2655,7 @@ array_ass_subscr(PyObject *op, PyObject *item, PyObject *value) } } -static const void *emptybuf = ""; +static const _Py_ALIGNED_DEF(ALIGNOF_MAX_ALIGN_T, char) emptybuf[] = ""; static int diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index a73bfff340ce48..af01393a763ac2 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -18,7 +18,7 @@ class bytearray "PyByteArrayObject *" "&PyByteArray_Type" /*[clinic end generated code: output=da39a3ee5e6b4b0d input=5535b77c37a119e0]*/ /* For PyByteArray_AS_STRING(). */ -char _PyByteArray_empty_string[] = ""; +_Py_ALIGNED_DEF(ALIGNOF_MAX_ALIGN_T, char) _PyByteArray_empty_string[] = ""; /* Helpers */