Skip to content

Commit 5671fa6

Browse files
committed
Add memoryview_scoped_release to manage short-lived memoryviews
1 parent c16da99 commit 5671fa6

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
@@ -417,6 +417,23 @@ managed by Python. The user is responsible for managing the lifetime of the
417417
buffer. Using a ``memoryview`` created in this way after deleting the buffer in
418418
C++ side results in undefined behavior.
419419

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

422439
.. code-block:: cpp

include/pybind11/pybind11.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2174,6 +2174,21 @@ class gil_scoped_acquire { };
21742174
class gil_scoped_release { };
21752175
#endif
21762176

2177+
#if PY_VERSION_HEX >= 0x03020000
2178+
/// Release the underlying buffer exposed by the memoryview object when this
2179+
/// object goes out of scope. Any further operation on the view raises a
2180+
/// ValueError.
2181+
///
2182+
/// Only available in Python 3.2+
2183+
class memoryview_scoped_release {
2184+
public:
2185+
explicit memoryview_scoped_release(memoryview view) : m_view(view) {}
2186+
~memoryview_scoped_release() { m_view.attr("release")(); }
2187+
private:
2188+
memoryview m_view;
2189+
};
2190+
#endif
2191+
21772192
error_already_set::~error_already_set() {
21782193
if (m_type) {
21792194
gil_scoped_acquire gil;

include/pybind11/pytypes.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1421,7 +1421,8 @@ class memoryview : public object {
14211421
This method is meant for providing a ``memoryview`` for C/C++ buffer not
14221422
managed by Python. The caller is responsible for managing the lifetime
14231423
of ``ptr`` and ``format``, which MUST outlive the memoryview constructed
1424-
here.
1424+
here. Consider using ``memoryview_scoped_release`` to manage the lifetime
1425+
for short-lived memoryview objects.
14251426
14261427
See also: Python C API documentation for `PyMemoryView_FromBuffer`_.
14271428
@@ -1475,6 +1476,8 @@ class memoryview : public object {
14751476
This method is meant for providing a ``memoryview`` for C/C++ buffer not
14761477
managed by Python. The caller is responsible for managing the lifetime
14771478
of ``mem``, which MUST outlive the memoryview constructed here.
1479+
Consider using ``memoryview_scoped_release`` to manage the lifetime
1480+
for short-lived memoryview objects.
14781481
14791482
This method is not available in Python 2.
14801483

tests/test_pytypes.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,4 +410,14 @@ TEST_SUBMODULE(pytypes, m) {
410410

411411
// test_builtin_functions
412412
m.def("get_len", [](py::handle h) { return py::len(h); });
413+
414+
#if PY_VERSION_HEX >= 0x03020000
415+
m.def("test_memoryview_scoped_release", [](py::function f) {
416+
const char* buf = "\x42";
417+
auto view = py::memoryview::from_memory(buf, 1);
418+
py::memoryview_scoped_release release(view);
419+
f(view);
420+
});
421+
#endif
422+
413423
}

tests/test_pytypes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,17 @@ def test_builtin_functions():
486486
"object of type 'generator' has no len()",
487487
"'generator' has no length",
488488
] # PyPy
489+
490+
491+
@pytest.mark.skipif(sys.version_info < (3, 2), reason="API not available")
492+
def test_memoryview_scoped_release():
493+
class C:
494+
def fn(self, view):
495+
self.view = view
496+
assert bytes(view) == b"\x42"
497+
498+
c = C()
499+
m.test_memoryview_scoped_release(c.fn)
500+
assert hasattr(c, "view")
501+
with pytest.raises(ValueError):
502+
bytes(c.view)

0 commit comments

Comments
 (0)