diff --git a/Doc/library/threadsafety.rst b/Doc/library/threadsafety.rst index a529f7803affbc..2ff44b0fabd608 100644 --- a/Doc/library/threadsafety.rst +++ b/Doc/library/threadsafety.rst @@ -604,3 +604,9 @@ underlying object supports it: Resizing or reallocating the underlying object (such as calling :meth:`bytearray.resize`) while a memoryview is exported raises :exc:`BufferError`. This is enforced regardless of threading. + +In the :term:`free-threaded build`, :meth:`~memoryview.release` is +thread-safe. If another thread has an in-flight operation on the memoryview +when it is released, the underlying buffer may remain exported until that +operation completes. Restrictions on the exporter, such as prohibiting +resizing, remain in effect until then. diff --git a/Include/cpython/memoryobject.h b/Include/cpython/memoryobject.h index 961161b70f2058..419e57d1ad56a4 100644 --- a/Include/cpython/memoryobject.h +++ b/Include/cpython/memoryobject.h @@ -11,7 +11,7 @@ typedef struct { PyObject_HEAD int flags; /* state flags */ - Py_ssize_t exports; /* number of direct memoryview exports */ + Py_ssize_t exports; /* registered views and active operations */ Py_buffer master; /* snapshot buffer obtained from the original exporter */ } _PyManagedBufferObject; diff --git a/Lib/test/test_free_threading/test_memoryview.py b/Lib/test/test_free_threading/test_memoryview.py new file mode 100644 index 00000000000000..8493f425d7b86b --- /dev/null +++ b/Lib/test/test_free_threading/test_memoryview.py @@ -0,0 +1,313 @@ +import pickle +import threading +import unittest + +from test.support import threading_helper + +NTHREADS = 8 + +# Fresh objects expose one-time races under ThreadSanitizer. +ROUNDS = 20 +ITERS = 20 + +HASH_DATA_TEMPLATE = bytes(range(256)) +HASH_DATA_REPEAT = 256 + +READ_OPS = [ + lambda mv: mv.tobytes(), + lambda mv: mv.hex(), + lambda mv: mv.tolist(), + lambda mv: mv.cast("B"), + lambda mv: mv.toreadonly(), + lambda mv: memoryview(mv), + lambda mv: bytes(mv), + lambda mv: pickle.PickleBuffer(mv).release(), + lambda mv: mv.obj, + lambda mv: mv.format, + lambda mv: mv.c_contiguous, + lambda mv: mv[0], + lambda mv: mv[0:4], + lambda mv: mv.count(0), + lambda mv: mv.index(0), + lambda mv: mv == mv, + lambda mv: list(mv), + lambda mv: len(mv), +] + + +def run_racy(func, *args): + try: + func(*args) + except (ValueError, BufferError): + pass + + +@threading_helper.requires_working_threading() +class TestMemoryViewRaces(unittest.TestCase): + def assert_exporter_free(self, buf): + buf.append(0) + del buf[-1] + + def test_concurrent_slicing_keeps_export_count(self): + for _ in range(ROUNDS): + mv = memoryview(bytes(64)) + slices = [] + lock = threading.Lock() + + def make_slices(): + local = [mv[0:4] for _ in range(ITERS)] + local += [memoryview(mv) for _ in range(ITERS)] + with lock: + slices.extend(local) + + threading_helper.run_concurrently(make_slices, nthreads=NTHREADS) + del slices + + self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) + + def test_concurrent_release(self): + buf = bytearray(64) + + for _ in range(ROUNDS): + views = [memoryview(buf) for _ in range(NTHREADS)] + + def release(views=views): + for mv in views: + run_racy(mv.release) + + threading_helper.run_concurrently(release, nthreads=NTHREADS) + + self.assert_exporter_free(buf) + + def test_release_races_with_reads(self): + for _ in range(ROUNDS): + buf = bytearray(64) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def reader(): + for _ in range(ITERS): + mv = cell[0] + for op in READ_OPS: + run_racy(op, mv) + + threading_helper.run_concurrently( + [releaser] * (NTHREADS // 2) + [reader] * (NTHREADS // 2), + nthreads=NTHREADS, + ) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_read_keeps_exporter_alive_after_release(self): + size = 1 << 20 + for _ in range(ROUNDS): + exporter = [bytearray(size)] + view = memoryview(exporter[0]) + stale = [] + + def reader(): + try: + for _ in range(4): + data = view.tobytes() + if data.count(0) != len(data): + stale.append(True) + return + except ValueError: + pass + + def releaser(): + view.release() + exporter.clear() + for _ in range(8): + bytearray(b"\xdb" * size) + + threading_helper.run_concurrently([reader, releaser]) + self.assertFalse(stale) + + def test_release_races_with_writes(self): + for _ in range(ROUNDS): + buf = bytearray(8 * NTHREADS) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def writer(slot): + start = slot * 8 + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.__setitem__, start, 1) + run_racy(mv.__setitem__, slice(start, start + 4), b"abcd") + + workers = [releaser] * (NTHREADS // 2) + workers += [lambda s=s: writer(s) for s in range(NTHREADS // 2)] + threading_helper.run_concurrently(workers, nthreads=NTHREADS) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_release_races_with_buffer_exports(self): + for _ in range(ROUNDS): + buf = bytearray(64) + mv = memoryview(buf) + + def exporter(): + for _ in range(ITERS): + run_racy(lambda: pickle.PickleBuffer(mv).release()) + + def releaser(): + for _ in range(ITERS): + run_racy(mv.release) + + threading_helper.run_concurrently( + [exporter] * (NTHREADS - 1) + [releaser], nthreads=NTHREADS + ) + + mv.release() + self.assert_exporter_free(buf) + + def test_release_with_live_export(self): + buf = bytearray(64) + + for _ in range(ROUNDS): + mv = memoryview(buf) + held = pickle.PickleBuffer(mv) + + def release(mv=mv): + try: + mv.release() + except BufferError: + pass + + threading_helper.run_concurrently(release, nthreads=NTHREADS) + + self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) + held.release() + mv.release() + + self.assert_exporter_free(buf) + + def test_compare_two_views_races_with_release(self): + for _ in range(ROUNDS): + buf = bytearray(64) + cell = [memoryview(buf), memoryview(buf)] + lock = threading.Lock() + + def releaser(slot): + for _ in range(ITERS): + mv = cell[slot] + run_racy(mv.release) + with lock: + cell[slot] = memoryview(buf) + + def comparer(): + for _ in range(ITERS): + left, right = cell[0], cell[1] + run_racy(lambda: left == right) + run_racy(lambda: left != right) + + threading_helper.run_concurrently( + [lambda: releaser(0), lambda: releaser(1)] + [comparer] * 6, + nthreads=NTHREADS, + ) + + for mv in cell: + mv.release() + self.assert_exporter_free(buf) + + def test_release_parent_keeps_child_valid(self): + for _ in range(ROUNDS): + buf = bytearray(range(64)) + parent = memoryview(buf) + child = parent[0:32] + + def use_child(): + for _ in range(ITERS): + self.assertEqual(child[0], 0) + child.tobytes() + + def release_parent(): + parent.release() + + threading_helper.run_concurrently( + [release_parent] + [use_child] * (NTHREADS - 1), + nthreads=NTHREADS, + ) + + self.assertEqual(child.tobytes(), bytes(range(32))) + child.release() + self.assert_exporter_free(buf) + + def test_concurrent_iteration(self): + for _ in range(ROUNDS): + buf = bytearray(range(64)) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def iterator(): + for _ in range(ITERS): + run_racy(list, cell[0]) + + threading_helper.run_concurrently( + [releaser] * (NTHREADS // 2) + [iterator] * (NTHREADS // 2), + nthreads=NTHREADS, + ) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_iterator_exhaustion_drops_last_reference(self): + def loop(): + for _ in range(ROUNDS * ITERS): + self.assertEqual(list(iter(memoryview(b"ab"))), [97, 98]) + + threading_helper.run_concurrently(loop, nthreads=NTHREADS) + + def test_concurrent_hash(self): + for _ in range(ROUNDS): + data = HASH_DATA_TEMPLATE * HASH_DATA_REPEAT + mv = memoryview(data) + results = [] + lock = threading.Lock() + + def hasher(): + local = {hash(mv) for _ in range(ITERS)} + with lock: + results.append(local) + + threading_helper.run_concurrently(hasher, nthreads=NTHREADS) + self.assertEqual({h for s in results for h in s}, {hash(data)}) + + def test_concurrent_hash_and_release(self): + for _ in range(ROUNDS): + mv = memoryview(HASH_DATA_TEMPLATE * HASH_DATA_REPEAT) + + def work(mv=mv): + run_racy(hash, mv) + run_racy(mv.release) + + threading_helper.run_concurrently(work, nthreads=NTHREADS) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 3cb8a104faee5e..172b0374cd15f9 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -91,6 +91,13 @@ def test_iter(self): m = self._view(b) self.assertEqual(list(m), [m[i] for i in range(len(m))]) + def test_released_empty_iterator(self): + for tp in self._types: + m = self._view(tp(b"")) + iterator = iter(m) + m.release() + self.assertRaises(StopIteration, next, iterator) + def test_count(self): for tp in self._types: b = tp(self._source) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst new file mode 100644 index 00000000000000..731dd2bb83851c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst @@ -0,0 +1,9 @@ +On the free-threaded build, :class:`memoryview` operations are now thread safe: +an operation that reads the buffer holds the managed buffer for its duration, +so a concurrent :meth:`~memoryview.release` can no longer free the buffer under +it; slicing a memoryview from several threads no longer loses buffer +registrations and raises ``ValueError: operation forbidden on released +memoryview object``; and :func:`hash` of a memoryview is no longer a data race. +As a result :meth:`~memoryview.release` may now return while an operation +started by another thread is still running, see +:ref:`thread-safety-memoryview`. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 0bcd0b6596be77..4125a28bce75d1 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -103,13 +103,69 @@ _PyManagedBuffer_FromObject(PyObject *base, int flags) return (PyObject *)mbuf; } +/* Released flags participate in memoryview_pin()'s seq-cst handshake. */ + +static inline int +flags_get(const int *flags) +{ +#ifdef Py_GIL_DISABLED + return _Py_atomic_load_int(flags); +#else + return *flags; +#endif +} + +static inline int +flags_test_and_set(int *flags, int bit) +{ +#ifdef Py_GIL_DISABLED + int prev = _Py_atomic_load_int_relaxed(flags); + while (!_Py_atomic_compare_exchange_int(flags, &prev, prev | bit)) { + // Compare-exchange updates prev. + } +#else + int prev = *flags; + *flags = prev | bit; +#endif + return (prev & bit) != 0; +} + +static inline int +mv_get_flags(PyMemoryViewObject *mv) +{ + return flags_get(&mv->flags); +} + +static inline int +mv_has_flag(PyMemoryViewObject *mv, int bits) +{ + return (mv_get_flags(mv) & bits) != 0; +} + +static inline int +mv_set_flag(PyMemoryViewObject *mv, int bit) +{ + return flags_test_and_set(&mv->flags, bit); +} + +static inline int +mbuf_has_flag(_PyManagedBufferObject *mbuf, int bits) +{ + return (flags_get(&mbuf->flags) & bits) != 0; +} + +static inline int +mbuf_set_flag(_PyManagedBufferObject *mbuf, int bit) +{ + return flags_test_and_set(&mbuf->flags, bit); +} + static void mbuf_release(_PyManagedBufferObject *self) { - if (self->flags&_Py_MANAGED_BUFFER_RELEASED) + if (mbuf_set_flag(self, _Py_MANAGED_BUFFER_RELEASED)) { return; - - self->flags |= _Py_MANAGED_BUFFER_RELEASED; + } /* PyBuffer_Release() decrements master->obj and sets it to NULL. */ _PyObject_GC_UNTRACK(self); @@ -122,7 +178,7 @@ mbuf_dealloc(PyObject *_self) _PyManagedBufferObject *self = (_PyManagedBufferObject *)_self; assert(self->exports == 0); mbuf_release(self); - if (self->flags&_Py_MANAGED_BUFFER_FREE_FORMAT) + if (mbuf_has_flag(self, _Py_MANAGED_BUFFER_FREE_FORMAT)) PyMem_Free(self->master.format); PyObject_GC_Del(self); } @@ -178,8 +234,8 @@ PyTypeObject _PyManagedBuffer_Type = { /* In the process of breaking reference cycles mbuf_release() can be called before memory_release(). */ #define BASE_INACCESSIBLE(mv) \ - (((PyMemoryViewObject *)mv)->flags&_Py_MEMORYVIEW_RELEASED || \ - ((PyMemoryViewObject *)mv)->mbuf->flags&_Py_MANAGED_BUFFER_RELEASED) + (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RELEASED) || \ + mbuf_has_flag(_PyMemoryView_CAST(mv)->mbuf, _Py_MANAGED_BUFFER_RELEASED)) #define CHECK_RELEASED(mv) \ if (BASE_INACCESSIBLE(mv)) { \ @@ -196,17 +252,17 @@ PyTypeObject _PyManagedBuffer_Type = { } #define CHECK_RESTRICTED(mv) \ - if (((PyMemoryViewObject *)(mv))->flags & _Py_MEMORYVIEW_RESTRICTED) { \ - PyErr_SetString(PyExc_ValueError, \ - "cannot create new view on restricted memoryview"); \ - return NULL; \ + if (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RESTRICTED)) { \ + PyErr_SetString(PyExc_ValueError, \ + "cannot create new view on restricted memoryview"); \ + return NULL; \ } #define CHECK_RESTRICTED_INT(mv) \ - if (((PyMemoryViewObject *)(mv))->flags & _Py_MEMORYVIEW_RESTRICTED) { \ - PyErr_SetString(PyExc_ValueError, \ - "cannot create new view on restricted memoryview"); \ - return -1; \ + if (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RESTRICTED)) { \ + PyErr_SetString(PyExc_ValueError, \ + "cannot create new view on restricted memoryview"); \ + return -1; \ } /* See gh-92888. These macros signal that we need to check the memoryview @@ -214,6 +270,9 @@ PyTypeObject _PyManagedBuffer_Type = { #define CHECK_RELEASED_AGAIN(mv) CHECK_RELEASED(mv) #define CHECK_RELEASED_INT_AGAIN(mv) CHECK_RELEASED_INT(mv) +static inline int memoryview_pin(PyMemoryViewObject *self); +static inline void memoryview_unpin(PyMemoryViewObject *self); + #define CHECK_LIST_OR_TUPLE(v) \ if (!PyList_Check(v) && !PyTuple_Check(v)) { \ PyErr_SetString(PyExc_TypeError, \ @@ -699,7 +758,7 @@ mbuf_add_view(_PyManagedBufferObject *mbuf, const Py_buffer *src) init_flags(mv); mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf); - mbuf->exports++; + FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); return (PyObject *)mv; } @@ -729,7 +788,7 @@ mbuf_add_incomplete_view(_PyManagedBufferObject *mbuf, const Py_buffer *src, init_shared_values(dest, src); mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf); - mbuf->exports++; + FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); return (PyObject *)mv; } @@ -804,9 +863,20 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) if (PyMemoryView_Check(v)) { PyMemoryViewObject *mv = (PyMemoryViewObject *)v; - CHECK_RELEASED(mv); - CHECK_RESTRICTED(mv); - return mbuf_add_view(mv->mbuf, &mv->view); + if (memoryview_pin(mv) < 0) { + return NULL; + } + PyObject *result; + if (mv_has_flag(mv, _Py_MEMORYVIEW_RESTRICTED)) { + PyErr_SetString(PyExc_ValueError, + "cannot create new view on restricted memoryview"); + result = NULL; + } + else { + result = mbuf_add_view(mv->mbuf, &mv->view); + } + memoryview_unpin(mv); + return result; } else if (PyObject_CheckBuffer(v)) { PyObject *ret; @@ -1096,6 +1166,78 @@ get_exports(PyMemoryViewObject *buf) #endif } +static void +mbuf_drop_export(_PyManagedBufferObject *mbuf) +{ +#ifdef Py_GIL_DISABLED + Py_ssize_t prev_exports = _Py_atomic_add_ssize(&mbuf->exports, -1); +#else + Py_ssize_t prev_exports = mbuf->exports--; +#endif + assert(prev_exports > 0); + if (prev_exports == 1) { + mbuf_release(mbuf); + } +} + +/* Pinning and release use this seq-cst handshake: + + pin: increment exports; read released + release: set released; decrement exports + + If pinning misses the released flag, its increment precedes release's + decrement. A pin starting at zero sees the flag and backs out. */ +static inline int +memoryview_pin(PyMemoryViewObject *self) +{ +#ifdef Py_GIL_DISABLED + (void)_Py_atomic_add_ssize(&self->mbuf->exports, 1); + if (BASE_INACCESSIBLE(self)) { + mbuf_drop_export(self->mbuf); + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return -1; + } + return 0; +#else + if (BASE_INACCESSIBLE(self)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return -1; + } + return 0; +#endif +} + +static inline void +memoryview_unpin(PyMemoryViewObject *self) +{ +#ifdef Py_GIL_DISABLED + mbuf_drop_export(self->mbuf); +#endif +} + +/* The lock makes adding an export atomic with memoryview_release_impl(). + The pin handshake cannot be used because release checks exports first. */ +static int +memoryview_add_export(PyMemoryViewObject *self) +{ + int result = 0; + + Py_BEGIN_CRITICAL_SECTION(self); + if (BASE_INACCESSIBLE(self)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + result = -1; + } + else { + FT_ATOMIC_ADD_SSIZE(self->exports, 1); + } + Py_END_CRITICAL_SECTION(); + + return result; +} + /****************************************************************************/ /* Release/GC management */ @@ -1109,14 +1251,11 @@ static void _memory_release(PyMemoryViewObject *self) { assert(get_exports(self) == 0); - if (self->flags & _Py_MEMORYVIEW_RELEASED) + if (mv_set_flag(self, _Py_MEMORYVIEW_RELEASED)) { return; - - self->flags |= _Py_MEMORYVIEW_RELEASED; - assert(self->mbuf->exports > 0); - if (--self->mbuf->exports == 0) { - mbuf_release(self->mbuf); } + + mbuf_drop_export(self->mbuf); } /*[clinic input] @@ -1129,22 +1268,26 @@ static PyObject * memoryview_release_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=d0b7e3ba95b7fcb9 input=bc71d1d51f4a52f0]*/ { + PyObject *result = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); Py_ssize_t exports = get_exports(self); if (exports == 0) { _memory_release(self); - Py_RETURN_NONE; + result = Py_NewRef(Py_None); } - - if (exports > 0) { + else if (exports > 0) { PyErr_Format(PyExc_BufferError, "memoryview has %zd exported buffer%s", exports, - exports==1 ? "" : "s"); - return NULL; + exports == 1 ? "" : "s"); + } + else { + PyErr_SetString(PyExc_SystemError, + "memoryview: negative export count"); } + Py_END_CRITICAL_SECTION(); - PyErr_SetString(PyExc_SystemError, - "memoryview: negative export count"); - return NULL; + return result; } static void @@ -1468,6 +1611,10 @@ zero_in_shape(PyMemoryViewObject *mv) All casts must result in views that will have the exact byte size of the original input. Otherwise, an error is raised. */ +static PyObject * +memoryview_cast_pinned(PyMemoryViewObject *self, PyObject *format, + PyObject *shape, int order); + /*[clinic input] memoryview.cast @@ -1487,6 +1634,18 @@ static PyObject * memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, PyObject *shape, int order) /*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_cast_pinned(self, format, shape, order); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_cast_pinned(PyMemoryViewObject *self, PyObject *format, + PyObject *shape, int order) { PyMemoryViewObject *mv = NULL; Py_ssize_t ndim = 1; @@ -1499,8 +1658,9 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, return NULL; } - if (!MV_C_CONTIGUOUS(self->flags)) { - if (shape || !MV_F_CONTIGUOUS(self->flags)) { + int self_flags = mv_get_flags(self); + if (!MV_C_CONTIGUOUS(self_flags)) { + if (shape || !MV_F_CONTIGUOUS(self_flags)) { PyErr_SetString(PyExc_TypeError, "memoryview: casts are restricted to contiguous views"); return NULL; @@ -1544,6 +1704,9 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, return NULL; } +static PyObject * +memoryview_toreadonly_pinned(PyMemoryViewObject *self); + /*[clinic input] memoryview.toreadonly @@ -1553,6 +1716,17 @@ Return a readonly version of the memoryview. static PyObject * memoryview_toreadonly_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=2c7e056f04c99e62 input=dc06d20f19ba236f]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_toreadonly_pinned(self); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_toreadonly_pinned(PyMemoryViewObject *self) { CHECK_RELEASED(self); CHECK_RESTRICTED(self); @@ -1571,12 +1745,13 @@ memoryview_toreadonly_impl(PyMemoryViewObject *self) /* getbuffer */ /**************************************************************************/ +/* No pin is needed: memoryview_add_export() keeps the buffer alive or fails. */ static int memory_getbuf(PyObject *_self, Py_buffer *view, int flags) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *base = &self->view; - int baseflags = self->flags; + int baseflags = mv_get_flags(self); CHECK_RELEASED_INT(self); CHECK_RESTRICTED_INT(self); @@ -1644,10 +1819,11 @@ memory_getbuf(PyObject *_self, Py_buffer *view, int flags) view->shape = NULL; } + if (memoryview_add_export(self) < 0) { + return -1; + } view->obj = Py_NewRef(self); - FT_ATOMIC_ADD_SSIZE(self->exports, 1); - return 0; } @@ -2322,6 +2498,9 @@ tolist_rec(PyMemoryViewObject *self, const char *ptr, Py_ssize_t ndim, const Py_ /* Return a list representation of the memoryview. Currently only buffers with native format strings are supported. */ +static PyObject * +memoryview_tolist_pinned(PyMemoryViewObject *self); + /*[clinic input] memoryview.tolist @@ -2331,6 +2510,17 @@ Return the data in the buffer as a list of elements. static PyObject * memoryview_tolist_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=a6cda89214fd5a1b input=21e7d0c1860b211a]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_tolist_pinned(self); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_tolist_pinned(PyMemoryViewObject *self) { const Py_buffer *view = &self->view; const char *fmt; @@ -2355,6 +2545,9 @@ memoryview_tolist_impl(PyMemoryViewObject *self) } } +static PyObject * +memoryview_tobytes_pinned(PyMemoryViewObject *self, const char *order); + /*[clinic input] memoryview.tobytes @@ -2373,6 +2566,17 @@ is the same as order='C'. static PyObject * memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) /*[clinic end generated code: output=1288b62560a32a23 input=119c70aa91791dc8]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_tobytes_pinned(self, order); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_tobytes_pinned(PyMemoryViewObject *self, const char *order) { Py_buffer *src = VIEW_ADDR(self); char ord = 'C'; @@ -2407,6 +2611,10 @@ memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) return PyBytesWriter_Finish(writer); } +static PyObject * +memoryview_hex_pinned(PyMemoryViewObject *self, PyObject *sep, + Py_ssize_t bytes_per_sep); + /*[clinic input] memoryview.hex @@ -2434,12 +2642,24 @@ static PyObject * memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, Py_ssize_t bytes_per_sep) /*[clinic end generated code: output=c9bb00c7a8e86056 input=3f1c5d08906e3b70]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_hex_pinned(self, sep, bytes_per_sep); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_hex_pinned(PyMemoryViewObject *self, PyObject *sep, + Py_ssize_t bytes_per_sep) { Py_buffer *src = VIEW_ADDR(self); CHECK_RELEASED(self); - if (MV_C_CONTIGUOUS(self->flags)) { + if (MV_C_CONTIGUOUS(mv_get_flags(self))) { // Prevent 'self' from being freed if computing len(sep) mutates 'self' // in _Py_strhex_with_sep(). // See: https://github.com/python/cpython/issues/143195. @@ -2473,7 +2693,7 @@ static PyObject * memory_repr(PyObject *_self) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (self->flags & _Py_MEMORYVIEW_RELEASED) + if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) return PyUnicode_FromFormat("", self); else return PyUnicode_FromFormat("", self); @@ -2546,9 +2766,9 @@ ptr_from_tuple(const Py_buffer *view, PyObject *tup) /* Return the item at index. In a one-dimensional view, this is an object with the type specified by view->format. Otherwise, the item is a sub-view. - The function is used in memory_subscript() and memory_as_sequence. */ + The function is used in memory_subscript_pinned() and by sq_item. */ static PyObject * -memory_item(PyObject *_self, Py_ssize_t index) +memory_item_pinned(PyObject *_self, Py_ssize_t index) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); @@ -2674,7 +2894,7 @@ is_multiindex(PyObject *key) 0-d memoryview objects can be referenced using mv[...] or mv[()] but not with anything else. */ static PyObject * -memory_subscript(PyObject *_self, PyObject *key) +memory_subscript_pinned(PyObject *_self, PyObject *key) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view; @@ -2704,7 +2924,7 @@ memory_subscript(PyObject *_self, PyObject *key) index = PyNumber_AsSsize_t(key, PyExc_IndexError); if (index == -1 && PyErr_Occurred()) return NULL; - return memory_item((PyObject *)self, index); + return memory_item_pinned((PyObject *)self, index); } else if (PySlice_Check(key)) { CHECK_RESTRICTED(self); @@ -2737,7 +2957,7 @@ memory_subscript(PyObject *_self, PyObject *key) } static int -memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) +memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); @@ -2827,7 +3047,7 @@ memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) return pack_single(self, ptr, value, fmt); } if (PySlice_Check(key) || is_multislice(key)) { - /* Call memory_subscript() to produce a sliced lvalue, then copy + /* Call memory_subscript_pinned() to produce a sliced lvalue, then copy rvalue into lvalue. This is already implemented in _testbuffer.c. */ PyErr_SetString(PyExc_NotImplementedError, "memoryview slice assignments are currently restricted " @@ -2851,6 +3071,42 @@ memory_length(PyObject *_self) return self->view.shape[0]; } +static PyObject * +memory_item(PyObject *_self, Py_ssize_t index) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_item_pinned(_self, index); + memoryview_unpin(self); + return result; +} + +static PyObject * +memory_subscript(PyObject *_self, PyObject *key) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_subscript_pinned(_self, key); + memoryview_unpin(self); + return result; +} + +static int +memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return -1; + } + int result = memory_ass_sub_pinned(_self, key, value); + memoryview_unpin(self); + return result; +} + /* As mapping */ static PyMappingMethods memory_as_mapping = { memory_length, /* mp_length */ @@ -2871,6 +3127,9 @@ static PySequenceMethods memory_as_sequence = { /* Counting */ /****************************************************************************/ +static PyObject * +memoryview_count_pinned(PyMemoryViewObject *self, PyObject *value); + /*[clinic input] memoryview.count @@ -2883,6 +3142,17 @@ Count the number of occurrences of a value. static PyObject * memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) /*[clinic end generated code: output=a15cb19311985063 input=e3036ce1ed7d1823]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_count_pinned(self, value); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_count_pinned(PyMemoryViewObject *self, PyObject *value) { PyObject *iter = PyObject_GetIter(_PyObject_CAST(self)); if (iter == NULL) { @@ -2920,6 +3190,10 @@ memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) /* Lookup */ /**************************************************************************/ +static PyObject * +memoryview_index_pinned(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop); + /*[clinic input] memoryview.index @@ -2937,6 +3211,18 @@ static PyObject * memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, Py_ssize_t start, Py_ssize_t stop) /*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_index_pinned(self, value, start, stop); + memoryview_unpin(self); + return result; +} + +static PyObject * +memoryview_index_pinned(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop) { const Py_buffer *view = &self->view; CHECK_RELEASED(self); @@ -2973,7 +3259,7 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, // entire loop. assert(index < n); - PyObject *item = memory_item(obj, index); + PyObject *item = memory_item_pinned(obj, index); if (item == NULL) { return NULL; } @@ -3216,7 +3502,7 @@ cmp_rec(const char *p, const char *q, } static PyObject * -memory_richcompare(PyObject *v, PyObject *w, int op) +memory_richcompare_pinned(PyObject *v, PyObject *w, int op) { PyObject *res; Py_buffer wbuf, *vv; @@ -3342,15 +3628,44 @@ memory_richcompare(PyObject *v, PyObject *w, int op) return Py_XNewRef(res); } +static PyObject * +memory_richcompare(PyObject *v, PyObject *w, int op) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)v; + if (memoryview_pin(self) < 0) { + /* Released memoryviews still compare by identity. */ + PyErr_Clear(); + return memory_richcompare_pinned(v, w, op); + } + + PyMemoryViewObject *other = NULL; + if (PyMemoryView_Check(w) && w != v) { + other = (PyMemoryViewObject *)w; + if (memoryview_pin(other) < 0) { + PyErr_Clear(); + memoryview_unpin(self); + return memory_richcompare_pinned(v, w, op); + } + } + + PyObject *result = memory_richcompare_pinned(v, w, op); + if (other != NULL) { + memoryview_unpin(other); + } + memoryview_unpin(self); + return result; +} + /**************************************************************************/ /* Hash */ /**************************************************************************/ static Py_hash_t -memory_hash(PyObject *_self) +memory_hash_pinned(PyObject *_self) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (self->hash == -1) { + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->hash); + if (hash == -1) { Py_buffer *view = &self->view; char *mem = view->buf; Py_ssize_t ret; @@ -3381,7 +3696,7 @@ memory_hash(PyObject *_self) } } - if (!MV_C_CONTIGUOUS(self->flags)) { + if (!MV_C_CONTIGUOUS(mv_get_flags(self))) { mem = PyMem_Malloc(view->len); if (mem == NULL) { PyErr_NoMemory(); @@ -3394,13 +3709,30 @@ memory_hash(PyObject *_self) } /* Can't fail */ - self->hash = Py_HashBuffer(mem, view->len); + hash = Py_HashBuffer(mem, view->len); + FT_ATOMIC_STORE_SSIZE_RELAXED(self->hash, hash); if (mem != view->buf) PyMem_Free(mem); } - return self->hash; + return hash; +} + +static Py_hash_t +memory_hash(PyObject *_self) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->hash); + if (hash != -1) { + return hash; + } + if (memoryview_pin(self) < 0) { + return -1; + } + Py_hash_t result = memory_hash_pinned(_self); + memoryview_unpin(self); + return result; } @@ -3433,7 +3765,7 @@ _IntTupleFromSsizet(int len, Py_ssize_t *vals) } static PyObject * -memory_obj_get(PyObject *_self, void *Py_UNUSED(ignored)) +memory_obj_get_pinned(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &self->view; @@ -3454,7 +3786,7 @@ memory_nbytes_get(PyObject *_self, void *Py_UNUSED(ignored)) } static PyObject * -memory_format_get(PyObject *_self, void *Py_UNUSED(ignored)) +memory_format_get_pinned(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); @@ -3514,7 +3846,7 @@ memory_c_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_C_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_C_CONTIGUOUS(mv_get_flags(self))); } static PyObject * @@ -3522,7 +3854,7 @@ memory_f_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_F_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_F_CONTIGUOUS(mv_get_flags(self))); } static PyObject * @@ -3530,9 +3862,27 @@ memory_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_ANY_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_ANY_CONTIGUOUS(mv_get_flags(self))); } +#define MEMORYVIEW_PINNED_GETTER(name) \ + static PyObject * \ + name(PyObject *_self, void *ignored) \ + { \ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; \ + if (memoryview_pin(self) < 0) { \ + return NULL; \ + } \ + PyObject *result = name##_pinned(_self, ignored); \ + memoryview_unpin(self); \ + return result; \ + } + +MEMORYVIEW_PINNED_GETTER(memory_obj_get) +MEMORYVIEW_PINNED_GETTER(memory_format_get) + +#undef MEMORYVIEW_PINNED_GETTER + PyDoc_STRVAR(memory_obj_doc, "The underlying object of the memoryview."); PyDoc_STRVAR(memory_nbytes_doc, @@ -3636,32 +3986,44 @@ static PyObject * memoryiter_next(PyObject *self) { memoryiterobject *it = (memoryiterobject *)self; - PyMemoryViewObject *seq; - seq = it->it_seq; + PyMemoryViewObject *seq = it->it_seq; if (seq == NULL) { return NULL; } - if (it->it_index < it->it_length) { - CHECK_RELEASED(seq); - Py_buffer *view = &(seq->view); - char *ptr = (char *)seq->view.buf; + if (it->it_index >= it->it_length) { + /* Preserve StopIteration for exhausted iterators over released views. */ + it->it_seq = NULL; + Py_DECREF(seq); + return NULL; + } + +#ifdef Py_GIL_DISABLED + /* Keep seq alive if another thread exhausts the iterator. */ + Py_INCREF(seq); +#endif + + PyObject *result = NULL; + if (memoryview_pin(seq) == 0) { + Py_buffer *view = &seq->view; + char *ptr = (char *)view->buf; ptr += view->strides[0] * it->it_index++; ptr = ADJUST_PTR(ptr, view->suboffsets, 0); - if (ptr == NULL) { - return NULL; + if (ptr != NULL) { + result = unpack_single(seq, ptr, it->it_fmt); } - return unpack_single(seq, ptr, it->it_fmt); + memoryview_unpin(seq); } - it->it_seq = NULL; +#ifdef Py_GIL_DISABLED Py_DECREF(seq); - return NULL; +#endif + return result; } static PyObject * -memory_iter(PyObject *seq) +memory_iter_pinned(PyObject *seq) { if (!PyMemoryView_Check(seq)) { PyErr_BadInternalCall(); @@ -3698,6 +4060,18 @@ memory_iter(PyObject *seq) return (PyObject *)it; } +static PyObject * +memory_iter(PyObject *seq) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)seq; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_iter_pinned(seq); + memoryview_unpin(self); + return result; +} + PyTypeObject _PyMemoryIter_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "memory_iterator",