Skip to content

Commit 5814f9a

Browse files
committed
Add memoryview_scoped_release to manage short-lived memoryviews
1 parent 98f1bbb commit 5814f9a

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
@@ -2205,6 +2205,21 @@ class gil_scoped_release {
22052205
};
22062206
#endif
22072207

2208+
#if PY_VERSION_HEX >= 0x03020000
2209+
/// Release the underlying buffer exposed by the memoryview object when this
2210+
/// object goes out of scope. Any further operation on the view raises a
2211+
/// ValueError.
2212+
///
2213+
/// Only available in Python 3.2+
2214+
class memoryview_scoped_release {
2215+
public:
2216+
explicit memoryview_scoped_release(memoryview view) : m_view(view) {}
2217+
~memoryview_scoped_release() { m_view.attr("release")(); }
2218+
private:
2219+
memoryview m_view;
2220+
};
2221+
#endif
2222+
22082223
error_already_set::~error_already_set() {
22092224
if (m_type) {
22102225
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
@@ -413,4 +413,14 @@ TEST_SUBMODULE(pytypes, m) {
413413

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

tests/test_pytypes.py

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

0 commit comments

Comments
 (0)