diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41e21dc129..607f612a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,17 @@ jobs: - runs-on: ubuntu-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON + # Keep one PyPy 7 job while that runtime remains supported. - runs-on: ubuntu-latest python-version: 'pypy-3.11-v7.3.23' cmake-args: -DCMAKE_CXX_STANDARD=17 + - runs-on: ubuntu-latest + python-version: 'pypy-3.11-v8.0.1' + cmake-args: -DCMAKE_CXX_STANDARD=17 + # Keep 8.0.0 to test the deallocation and memoryview workarounds (PR #6197). + - runs-on: ubuntu-latest + python-version: 'pypy-3.12-v8.0.0' + cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: ubuntu-latest python-version: 'graalpy-25.0' cmake-args: -DCMAKE_CXX_STANDARD=20 @@ -138,7 +146,7 @@ jobs: python-version: '3.15' cmake-args: -DCMAKE_CXX_STANDARD=17 - runs-on: macos-latest - python-version: 'pypy-3.11-v7.3.23' + python-version: 'pypy-3.12-v8.0.1' - runs-on: macos-latest python-version: 'graalpy-25.0' - runs-on: macos-latest @@ -172,6 +180,9 @@ jobs: - runs-on: windows-latest python-version: 'pypy3.11' cmake-args: -DCMAKE_CXX_STANDARD=20 + - runs-on: windows-latest + python-version: 'pypy3.12' + cmake-args: -DCMAKE_CXX_STANDARD=17 # The setup-python action currently doesn't have graalpy for windows # See https://github.com/actions/setup-python/pull/880 diff --git a/include/pybind11/detail/class-inl.h b/include/pybind11/detail/class-inl.h index 57d12fee44..5cbaee50c3 100644 --- a/include/pybind11/detail/class-inl.h +++ b/include/pybind11/detail/class-inl.h @@ -407,6 +407,20 @@ PYBIND11_INLINE void clear_patients(PyObject *self) { } } +PYBIND11_INLINE PyObject **instance_dict_ptr(PyObject *self) { +#if defined(PYPY_VERSION_NUM) && (PYPY_VERSION_NUM & 0xFFFF0000) == 0x08000000 + // PyPy 8.0.0's _PyObject_GetDictPtr() can abort when called from tp_dealloc. + // The bug was fixed in 8.0.1, but all 8.0.x releases share an extension ABI, + // so an extension built against a fixed release can still run on 8.0.0. + // Keep this workaround for the entire 8.0.x series. + auto offset = Py_TYPE(self)->tp_dictoffset; + return offset > 0 ? reinterpret_cast(reinterpret_cast(self) + offset) + : nullptr; +#else + return _PyObject_GetDictPtr(self); +#endif +} + PYBIND11_INLINE void clear_instance(PyObject *self) { auto *instance = reinterpret_cast(self); @@ -436,7 +450,7 @@ PYBIND11_INLINE void clear_instance(PyObject *self) { PyObject_ClearWeakRefs(self); } - PyObject **dict_ptr = _PyObject_GetDictPtr(self); + PyObject **dict_ptr = instance_dict_ptr(self); if (dict_ptr) { Py_CLEAR(*dict_ptr); } @@ -525,7 +539,7 @@ extern "C" PYBIND11_INLINE int pybind11_traverse(PyObject *self, visitproc visit return ret; } #else - PyObject *&dict = *_PyObject_GetDictPtr(self); + PyObject *&dict = *instance_dict_ptr(self); Py_VISIT(dict); #endif // https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse @@ -537,7 +551,7 @@ extern "C" PYBIND11_INLINE int pybind11_clear(PyObject *self) { #if PY_VERSION_HEX >= 0x030D0000 PyObject_ClearManagedDict(self); #else - PyObject *&dict = *_PyObject_GetDictPtr(self); + PyObject *&dict = *instance_dict_ptr(self); Py_CLEAR(dict); #endif return 0; diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 74e5ef0593..8716223d68 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -122,6 +122,9 @@ void add_patient(PyObject *nurse, PyObject *patient); void clear_patients(PyObject *self); +/// Returns the address of the instance `__dict__` slot, or nullptr if there is none. +PyObject **instance_dict_ptr(PyObject *self); + /// Clears all internal data from the instance and removes it from registered instances in /// preparation for deallocation. void clear_instance(PyObject *self); diff --git a/include/pybind11/pytypes-inl.h b/include/pybind11/pytypes-inl.h index f2eea1070f..f603c0b673 100644 --- a/include/pybind11/pytypes-inl.h +++ b/include/pybind11/pytypes-inl.h @@ -265,8 +265,21 @@ PYBIND11_INLINE memoryview memoryview::from_buffer(void *ptr, view.itemsize = itemsize; view.format = const_cast(format); view.ndim = static_cast(ndim); +#if defined(PYPY_VERSION) + // PyPy (at least up to 8.0.0) only copies shape and strides that point to its inline arrays. + if (ndim > PyBUF_MAX_NDIM) { + pybind11_fail("memoryview: too many dimensions"); + } + for (size_t i = 0; i < ndim; ++i) { + view._shape[i] = (*shape)[i]; + view._strides[i] = (*strides)[i]; + } + view.shape = view._shape; + view.strides = view._strides; +#else view.shape = shape->data(); view.strides = strides->data(); +#endif view.suboffsets = nullptr; view.internal = nullptr; PyObject *obj = PyMemoryView_FromBuffer(&view); diff --git a/tests/test_class.py b/tests/test_class.py index 638053b494..81bf67e6f7 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -622,6 +622,8 @@ class PyNonFinalFinalChild(m.IsNonFinalFinal): def test_exception_rvalue_abort(): with pytest.raises(RuntimeError): m.PyPrintDestructor().throw_something() + # The destructor calls Python; on PyPy, a later GC at an arbitrary point can abort + pytest.gc_collect() # https://github.com/pybind/pybind11/issues/1568 diff --git a/tests/test_multiple_inheritance.py b/tests/test_multiple_inheritance.py index e413d32fa0..10932b5dd8 100644 --- a/tests/test_multiple_inheritance.py +++ b/tests/test_multiple_inheritance.py @@ -266,9 +266,13 @@ def test_mi_static_properties(): def test_mi_dynamic_attributes(): """Mixing bases with and without dynamic attribute support""" - for d in (m.VanillaDictMix1(), m.VanillaDictMix2()): + for cls in (m.VanillaDictMix1, m.VanillaDictMix2): + d = cls() d.dynamic = 1 assert d.dynamic == 1 + del d + # The dict must be cleared without crashing (PyPy 3.12) + pytest.gc_collect() def test_mi_unaligned_base(): diff --git a/tools/check_inl_headers.py b/tools/check_inl_headers.py index 98e5d458bc..271f5e49df 100755 --- a/tools/check_inl_headers.py +++ b/tools/check_inl_headers.py @@ -25,6 +25,7 @@ "PYBIND11_HAS_STRING_VIEW", "PYBIND11_PRECOMPILED", "PYPY_VERSION", + "PYPY_VERSION_NUM", "PY_MAJOR_VERSION", "PY_MINOR_VERSION", "PY_VERSION_HEX",