Skip to content

Commit 3c1946f

Browse files
committed
Add memoryview_scoped_release to manage short-lived memoryviews
1 parent e58c689 commit 3c1946f

File tree

5 files changed

+60
-1
lines changed

5 files changed

+60
-1
lines changed

docs/advanced/pycpp/numpy.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,23 @@ managed by Python. The user is responsible for managing the lifetime of the
419419
buffer. Using a ``memoryview`` created in this way after deleting the buffer in
420420
C++ side results in undefined behavior.
421421

422+
To prevent undefined behavior, you can call the ``release`` function on a
423+
``memoryview``. After ``release`` is called, any further operation on the view
424+
will raise a ``ValueError``. For short lived buffers, consider using
425+
``memoryview_scoped_release`` to release the memoryview:
426+
427+
.. code-block:: cpp
428+
429+
{
430+
auto view = py::memoryview::from_memory(buffer, size);
431+
py::memoryview_scoped_release release(view);
432+
433+
some_function(view);
434+
}
435+
436+
// operations on the memoryview after this scope exits will raise a
437+
// ValueError exception
438+
422439
We can also use ``memoryview::from_memory`` for a simple 1D contiguous buffer:
423440

424441
.. code-block:: cpp

include/pybind11/pybind11.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2181,6 +2181,21 @@ void print(Args &&...args) {
21812181
detail::print(c.args(), c.kwargs());
21822182
}
21832183

2184+
#if PY_VERSION_HEX >= 0x03020000
2185+
/// Release the underlying buffer exposed by the memoryview object when this
2186+
/// object goes out of scope. Any further operation on the view raises a
2187+
/// ValueError.
2188+
///
2189+
/// Only available in Python 3.2+
2190+
class memoryview_scoped_release {
2191+
public:
2192+
explicit memoryview_scoped_release(memoryview view) : m_view(std::move(view)) {}
2193+
~memoryview_scoped_release() { m_view.attr("release")(); }
2194+
private:
2195+
memoryview m_view;
2196+
};
2197+
#endif
2198+
21842199
error_already_set::~error_already_set() {
21852200
if (m_type) {
21862201
gil_scoped_acquire gil;

include/pybind11/pytypes.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1514,7 +1514,8 @@ class memoryview : public object {
15141514
This method is meant for providing a ``memoryview`` for C/C++ buffer not
15151515
managed by Python. The caller is responsible for managing the lifetime
15161516
of ``ptr`` and ``format``, which MUST outlive the memoryview constructed
1517-
here.
1517+
here. Consider using ``memoryview_scoped_release`` to manage the lifetime
1518+
for short-lived memoryview objects.
15181519
15191520
See also: Python C API documentation for `PyMemoryView_FromBuffer`_.
15201521
@@ -1568,6 +1569,8 @@ class memoryview : public object {
15681569
This method is meant for providing a ``memoryview`` for C/C++ buffer not
15691570
managed by Python. The caller is responsible for managing the lifetime
15701571
of ``mem``, which MUST outlive the memoryview constructed here.
1572+
Consider using ``memoryview_scoped_release`` to manage the lifetime
1573+
for short-lived memoryview objects.
15711574
15721575
This method is not available in Python 2.
15731576

tests/test_pytypes.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,4 +434,14 @@ TEST_SUBMODULE(pytypes, m) {
434434
m.def("weakref_from_object", [](const py::object &o) { return py::weakref(o); });
435435
m.def("weakref_from_object_and_function",
436436
[](py::object o, py::function f) { return py::weakref(std::move(o), std::move(f)); });
437+
438+
#if PY_VERSION_HEX >= 0x03020000
439+
m.def("test_memoryview_scoped_release", [](const py::function f) {
440+
const char* buf = "\x42";
441+
auto view = py::memoryview::from_memory(buf, 1);
442+
py::memoryview_scoped_release release(view);
443+
f(view);
444+
});
445+
#endif
446+
437447
}

tests/test_pytypes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,17 @@ def callback(wr):
589589
del obj
590590
pytest.gc_collect()
591591
assert callback.called
592+
593+
594+
@pytest.mark.skipif(sys.version_info < (3, 2), reason="API not available")
595+
def test_memoryview_scoped_release():
596+
class C:
597+
def fn(self, view):
598+
self.view = view
599+
assert bytes(view) == b"\x42"
600+
601+
c = C()
602+
m.test_memoryview_scoped_release(c.fn)
603+
assert hasattr(c, "view")
604+
with pytest.raises(ValueError):
605+
bytes(c.view)

0 commit comments

Comments
 (0)