Skip to content

Commit ef6b1a6

Browse files
committed
Add memoryview_scoped_release to manage short-lived memoryviews
1 parent e248869 commit ef6b1a6

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

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

417434
.. code-block:: cpp

include/pybind11/pybind11.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,6 +2047,21 @@ class gil_scoped_acquire { };
20472047
class gil_scoped_release { };
20482048
#endif
20492049

2050+
#if PY_VERSION_HEX >= 0x03020000
2051+
/// Release the underlying buffer exposed by the memoryview object when this
2052+
/// object goes out of scope. Any further operation on the view raises a
2053+
/// ValueError.
2054+
///
2055+
/// Only available in Python 3.2+
2056+
class memoryview_scoped_release {
2057+
public:
2058+
explicit memoryview_scoped_release(memoryview view) : m_view(view) {}
2059+
~memoryview_scoped_release() { m_view.attr("release")(); }
2060+
private:
2061+
memoryview m_view;
2062+
};
2063+
#endif
2064+
20502065
error_already_set::~error_already_set() {
20512066
if (m_type) {
20522067
gil_scoped_acquire gil;

include/pybind11/pytypes.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,8 @@ class memoryview : public object {
13641364
This method is meant for providing a ``memoryview`` for C/C++ buffer not
13651365
managed by Python. The caller is responsible for managing the lifetime
13661366
of ``ptr`` and ``format``, which MUST outlive the memoryview constructed
1367-
here.
1367+
here. Consider using ``memoryview_scoped_release`` to manage the lifetime
1368+
for short-lived memoryview objects.
13681369
13691370
See also: Python C API documentation for `PyMemoryView_FromBuffer`_.
13701371
@@ -1418,6 +1419,8 @@ class memoryview : public object {
14181419
This method is meant for providing a ``memoryview`` for C/C++ buffer not
14191420
managed by Python. The caller is responsible for managing the lifetime
14201421
of ``mem``, which MUST outlive the memoryview constructed here.
1422+
Consider using ``memoryview_scoped_release`` to manage the lifetime
1423+
for short-lived memoryview objects.
14211424
14221425
This method is not available in Python 2.
14231426

tests/test_pytypes.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,14 @@ TEST_SUBMODULE(pytypes, m) {
367367
buf, static_cast<ssize_t>(strlen(buf)));
368368
});
369369
#endif
370+
371+
#if PY_VERSION_HEX >= 0x03020000
372+
m.def("test_memoryview_scoped_release", [](py::function f) {
373+
const char* buf = "\x42";
374+
auto view = py::memoryview::from_memory(buf, 1);
375+
py::memoryview_scoped_release release(view);
376+
f(view);
377+
});
378+
#endif
379+
370380
}

tests/test_pytypes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,17 @@ def test_memoryview_from_memory():
345345
assert isinstance(view, memoryview)
346346
assert view.format == 'B'
347347
assert bytes(view) == b'\xff\xe1\xab\x37'
348+
349+
350+
@pytest.mark.skipif(sys.version_info < (3, 2), reason='API not available')
351+
def test_memoryview_scoped_release():
352+
class C:
353+
def fn(self, view):
354+
self.view = view
355+
assert bytes(view) == b'\x42'
356+
c = C()
357+
m.test_memoryview_scoped_release(c.fn)
358+
assert hasattr(c, 'view')
359+
with pytest.raises(ValueError):
360+
bytes(c.view)
361+

0 commit comments

Comments
 (0)