diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index 53ec8c1a3c..beb93efc36 100644 --- a/docs/advanced/pycpp/numpy.rst +++ b/docs/advanced/pycpp/numpy.rst @@ -419,6 +419,23 @@ managed by Python. The user is responsible for managing the lifetime of the buffer. Using a ``memoryview`` created in this way after deleting the buffer in C++ side results in undefined behavior. +To prevent undefined behavior, you can call the ``release`` function on a +``memoryview``. After ``release`` is called, any further operation on the view +will raise a ``ValueError``. For short lived buffers, consider using +``memoryview_scoped_release`` to release the memoryview: + +.. code-block:: cpp + + { + auto view = py::memoryview::from_memory(buffer, size); + py::memoryview_scoped_release release(view); + + some_function(view); + } + + // operations on the memoryview after this scope exits will raise a + // ValueError exception + We can also use ``memoryview::from_memory`` for a simple 1D contiguous buffer: .. code-block:: cpp diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 0c25ca1a23..c00ed9a89e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2181,6 +2181,21 @@ void print(Args &&...args) { detail::print(c.args(), c.kwargs()); } +#if PY_VERSION_HEX >= 0x03020000 +/// Release the underlying buffer exposed by the memoryview object when this +/// object goes out of scope. Any further operation on the view raises a +/// ValueError. +/// +/// Only available in Python 3.2+ +class memoryview_scoped_release { +public: + explicit memoryview_scoped_release(memoryview view) : m_view(std::move(view)) {} + ~memoryview_scoped_release() { m_view.attr("release")(); } +private: + memoryview m_view; +}; +#endif + error_already_set::~error_already_set() { if (m_type) { gil_scoped_acquire gil; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index b483fb323c..9eeb143644 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -1514,7 +1514,8 @@ class memoryview : public object { This method is meant for providing a ``memoryview`` for C/C++ buffer not managed by Python. The caller is responsible for managing the lifetime of ``ptr`` and ``format``, which MUST outlive the memoryview constructed - here. + here. Consider using ``memoryview_scoped_release`` to manage the lifetime + for short-lived memoryview objects. See also: Python C API documentation for `PyMemoryView_FromBuffer`_. @@ -1568,6 +1569,8 @@ class memoryview : public object { This method is meant for providing a ``memoryview`` for C/C++ buffer not managed by Python. The caller is responsible for managing the lifetime of ``mem``, which MUST outlive the memoryview constructed here. + Consider using ``memoryview_scoped_release`` to manage the lifetime + for short-lived memoryview objects. This method is not available in Python 2. diff --git a/tests/test_pytypes.cpp b/tests/test_pytypes.cpp index d70536d3f0..db471b4862 100644 --- a/tests/test_pytypes.cpp +++ b/tests/test_pytypes.cpp @@ -434,4 +434,14 @@ TEST_SUBMODULE(pytypes, m) { m.def("weakref_from_object", [](const py::object &o) { return py::weakref(o); }); m.def("weakref_from_object_and_function", [](py::object o, py::function f) { return py::weakref(std::move(o), std::move(f)); }); + +#if PY_VERSION_HEX >= 0x03020000 + m.def("test_memoryview_scoped_release", [](const py::function &f) { + const char* buf = "\x42"; + auto view = py::memoryview::from_memory(buf, 1); + py::memoryview_scoped_release release(view); + f(view); + }); +#endif + } diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 66d6d30a0c..b3cbc15e55 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -589,3 +589,17 @@ def callback(wr): del obj pytest.gc_collect() assert callback.called + + +@pytest.mark.skipif(sys.version_info < (3, 2), reason="API not available") +def test_memoryview_scoped_release(): + class C: + def fn(self, view): + self.view = view + assert bytes(view) == b"\x42" + + c = C() + m.test_memoryview_scoped_release(c.fn) + assert hasattr(c, "view") + with pytest.raises(ValueError): + bytes(c.view)