From 6b37e6648c70d5d66fdf7260f3d138a99a88a02f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 24 Oct 2025 13:27:11 +0100 Subject: [PATCH 1/4] Force alignment of empty `bytearray` and `array.array` buffers This ensures the buffers used by the empty `bytearray` and `array.array` are aligned the same as a pointer returned by the allocator. This is a more convenient default for interop with other languages that have stricter requirements of type-safe buffers (e.g. Rust's `&[T]` type) even when empty. --- Misc/ACKS | 1 + .../2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 3 +++ Modules/arraymodule.c | 2 +- Objects/bytearrayobject.c | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst 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..e1ba0146db5787 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -0,0 +1,3 @@ +The empty instances of :func:`bytearray` and :func:`array.array` now produce +buffers that are aligned for any data type. This is a convenience for +interoperation with other languages. 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 */ From 1fea8e5ca08e5e62a4e10c799f73c1529b0b9667 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:28:53 +0000 Subject: [PATCH 2/4] Add tests of buffer pointer alignment --- Lib/test/support/__init__.py | 36 ++++++++++++++++++++++++++++++++++++ Lib/test/test_array.py | 16 ++++++++++++++++ Lib/test/test_builtin.py | 9 +++++++++ 3 files changed, 61 insertions(+) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 098bdcc0542b90..d0f34582dee32b 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -5,6 +5,7 @@ import annotationlib import contextlib +import ctypes import functools import inspect import logging @@ -69,6 +70,7 @@ "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", "reset_code", + "ctypes_py_buffer", ] @@ -3183,3 +3185,37 @@ def linked_to_musl(): return _linked_to_musl _linked_to_musl = tuple(map(int, version.split('.'))) return _linked_to_musl + + +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), + ] + + +@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. + """ + 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..41560696e089e8 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -8,6 +8,8 @@ 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 ctypes import weakref import pickle import operator @@ -67,6 +69,20 @@ def test_empty(self): a += a self.assertEqual(len(a), 0) + def test_empty_alignment(self): + # gh-140557: pointer alignment of empty allocation + 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..070d7b8f1354fe 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -4,6 +4,7 @@ import builtins import collections import contextlib +import ctypes import decimal import fractions import gc @@ -38,6 +39,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 +2406,13 @@ 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 + 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) From b7eed2bb88c0c569a62d771b151ae8a03554c72c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:35:39 +0000 Subject: [PATCH 3/4] Make NEWS more concise --- .../2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index e1ba0146db5787..cceebc7ae49513 100644 --- 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 @@ -1,3 +1,2 @@ -The empty instances of :func:`bytearray` and :func:`array.array` now produce -buffers that are aligned for any data type. This is a convenience for -interoperation with other languages. +:func:`bytearray` and :func:`array.array` now default to aligned buffers when +empty. Unaligned buffers can still be created by slicing. From 9d454de46dcae33514c208c6e2eab799e9dd62b0 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:58:02 +0000 Subject: [PATCH 4/4] Avoid `ctypes` import on unsupported platforms --- Lib/test/support/__init__.py | 39 ++++++++++++++++++++++-------------- Lib/test/test_array.py | 2 +- Lib/test/test_builtin.py | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 517786b89e97e8..4d7a3cbff5567d 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -5,7 +5,6 @@ import annotationlib import contextlib -import ctypes import functools import inspect import logging @@ -3187,20 +3186,25 @@ def linked_to_musl(): return _linked_to_musl -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), - ] +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 @@ -3210,7 +3214,12 @@ def ctypes_py_buffer(ob, flags=inspect.BufferFlags.SIMPLE): `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), diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 41560696e089e8..420186fb8bf1b3 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -9,7 +9,6 @@ from test.support import os_helper from test.support import _2G from test.support import ctypes_py_buffer -import ctypes import weakref import pickle import operator @@ -71,6 +70,7 @@ def test_empty(self): 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", diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 070d7b8f1354fe..e585a5852175bd 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -4,7 +4,6 @@ import builtins import collections import contextlib -import ctypes import decimal import fractions import gc @@ -2408,6 +2407,7 @@ def 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: