Skip to content

Commit e248869

Browse files
authored
Fix undefined memoryview format (#2223)
* Fix undefined memoryview format * Add missing <algorithm> header * Add workaround for py27 array compatibility * Workaround py27 memoryview behavior * Fix memoryview constructor from buffer_info * Workaround PyMemoryView_FromMemory availability in py27 * Fix up memoryview tests * Update memoryview test from buffer to check signedness * Use static factory method to create memoryview * Remove ndim arg from memoryview::frombuffer and add tests * Allow ndim=0 memoryview and documentation fixup * Use void* to align to frombuffer method signature * Add const variants of frombuffer and frommemory * Add memory view section in doc * Fix docs * Add test for null buffer * Workaround py27 nullptr behavior in test * Rename frombuffer to from_buffer
1 parent aa982e1 commit e248869

File tree

6 files changed

+293
-27
lines changed

6 files changed

+293
-27
lines changed

docs/Doxyfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ ALIASES += "endrst=\endverbatim"
1818
QUIET = YES
1919
WARNINGS = YES
2020
WARN_IF_UNDOCUMENTED = NO
21+
PREDEFINED = DOXYGEN_SHOULD_SKIP_THIS \
22+
PY_MAJOR_VERSION=3

docs/advanced/pycpp/numpy.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,45 @@ operation on the C++ side:
384384
385385
py::array a = /* A NumPy array */;
386386
py::array b = a[py::make_tuple(0, py::ellipsis(), 0)];
387+
388+
Memory view
389+
===========
390+
391+
For a case when we simply want to provide a direct accessor to C/C++ buffer
392+
without a concrete class object, we can return a ``memoryview`` object. Suppose
393+
we wish to expose a ``memoryview`` for 2x4 uint8_t array, we can do the
394+
following:
395+
396+
.. code-block:: cpp
397+
398+
const uint8_t buffer[] = {
399+
0, 1, 2, 3,
400+
4, 5, 6, 7
401+
};
402+
m.def("get_memoryview2d", []() {
403+
return py::memoryview::from_buffer(
404+
buffer, // buffer pointer
405+
{ 2, 4 }, // shape (rows, cols)
406+
{ sizeof(uint8_t) * 4, sizeof(uint8_t) } // strides in bytes
407+
);
408+
})
409+
410+
This approach is meant for providing a ``memoryview`` for a C/C++ buffer not
411+
managed by Python. The user is responsible for managing the lifetime of the
412+
buffer. Using a ``memoryview`` created in this way after deleting the buffer in
413+
C++ side results in undefined behavior.
414+
415+
We can also use ``memoryview::from_memory`` for a simple 1D contiguous buffer:
416+
417+
.. code-block:: cpp
418+
419+
m.def("get_memoryview1d", []() {
420+
return py::memoryview::from_memory(
421+
buffer, // buffer pointer
422+
sizeof(uint8_t) * 8 // buffer size
423+
);
424+
})
425+
426+
.. note::
427+
428+
``memoryview::from_memory`` is not available in Python 2.

include/pybind11/buffer_info.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ struct buffer_info {
5454
explicit buffer_info(Py_buffer *view, bool ownview = true)
5555
: buffer_info(view->buf, view->itemsize, view->format, view->ndim,
5656
{view->shape, view->shape + view->ndim}, {view->strides, view->strides + view->ndim}, view->readonly) {
57-
this->view = view;
57+
this->m_view = view;
5858
this->ownview = ownview;
5959
}
6060

@@ -73,24 +73,26 @@ struct buffer_info {
7373
ndim = rhs.ndim;
7474
shape = std::move(rhs.shape);
7575
strides = std::move(rhs.strides);
76-
std::swap(view, rhs.view);
76+
std::swap(m_view, rhs.m_view);
7777
std::swap(ownview, rhs.ownview);
7878
readonly = rhs.readonly;
7979
return *this;
8080
}
8181

8282
~buffer_info() {
83-
if (view && ownview) { PyBuffer_Release(view); delete view; }
83+
if (m_view && ownview) { PyBuffer_Release(m_view); delete m_view; }
8484
}
8585

86+
Py_buffer *view() const { return m_view; }
87+
Py_buffer *&view() { return m_view; }
8688
private:
8789
struct private_ctr_tag { };
8890

8991
buffer_info(private_ctr_tag, void *ptr, ssize_t itemsize, const std::string &format, ssize_t ndim,
9092
detail::any_container<ssize_t> &&shape_in, detail::any_container<ssize_t> &&strides_in, bool readonly)
9193
: buffer_info(ptr, itemsize, format, ndim, std::move(shape_in), std::move(strides_in), readonly) { }
9294

93-
Py_buffer *view = nullptr;
95+
Py_buffer *m_view = nullptr;
9496
bool ownview = false;
9597
};
9698

include/pybind11/pytypes.h

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,35 +1336,139 @@ class buffer : public object {
13361336

13371337
class memoryview : public object {
13381338
public:
1339-
explicit memoryview(const buffer_info& info) {
1340-
static Py_buffer buf { };
1341-
// Py_buffer uses signed sizes, strides and shape!..
1342-
static std::vector<Py_ssize_t> py_strides { };
1343-
static std::vector<Py_ssize_t> py_shape { };
1344-
buf.buf = info.ptr;
1345-
buf.itemsize = info.itemsize;
1346-
buf.format = const_cast<char *>(info.format.c_str());
1347-
buf.ndim = (int) info.ndim;
1348-
buf.len = info.size;
1349-
py_strides.clear();
1350-
py_shape.clear();
1351-
for (size_t i = 0; i < (size_t) info.ndim; ++i) {
1352-
py_strides.push_back(info.strides[i]);
1353-
py_shape.push_back(info.shape[i]);
1354-
}
1355-
buf.strides = py_strides.data();
1356-
buf.shape = py_shape.data();
1357-
buf.suboffsets = nullptr;
1358-
buf.readonly = info.readonly;
1359-
buf.internal = nullptr;
1339+
PYBIND11_OBJECT_CVT(memoryview, object, PyMemoryView_Check, PyMemoryView_FromObject)
13601340

1361-
m_ptr = PyMemoryView_FromBuffer(&buf);
1341+
/** \rst
1342+
Creates ``memoryview`` from ``buffer_info``.
1343+
1344+
``buffer_info`` must be created from ``buffer::request()``. Otherwise
1345+
throws an exception.
1346+
1347+
For creating a ``memoryview`` from objects that support buffer protocol,
1348+
use ``memoryview(const object& obj)`` instead of this constructor.
1349+
\endrst */
1350+
explicit memoryview(const buffer_info& info) {
1351+
if (!info.view())
1352+
pybind11_fail("Prohibited to create memoryview without Py_buffer");
1353+
// Note: PyMemoryView_FromBuffer never increments obj reference.
1354+
m_ptr = (info.view()->obj) ?
1355+
PyMemoryView_FromObject(info.view()->obj) :
1356+
PyMemoryView_FromBuffer(info.view());
13621357
if (!m_ptr)
13631358
pybind11_fail("Unable to create memoryview from buffer descriptor");
13641359
}
13651360

1366-
PYBIND11_OBJECT_CVT(memoryview, object, PyMemoryView_Check, PyMemoryView_FromObject)
1361+
/** \rst
1362+
Creates ``memoryview`` from static buffer.
1363+
1364+
This method is meant for providing a ``memoryview`` for C/C++ buffer not
1365+
managed by Python. The caller is responsible for managing the lifetime
1366+
of ``ptr`` and ``format``, which MUST outlive the memoryview constructed
1367+
here.
1368+
1369+
See also: Python C API documentation for `PyMemoryView_FromBuffer`_.
1370+
1371+
.. _PyMemoryView_FromBuffer: https://docs.python.org/c-api/memoryview.html#c.PyMemoryView_FromBuffer
1372+
1373+
:param ptr: Pointer to the buffer.
1374+
:param itemsize: Byte size of an element.
1375+
:param format: Pointer to the null-terminated format string. For
1376+
homogeneous Buffers, this should be set to
1377+
``format_descriptor<T>::value``.
1378+
:param shape: Shape of the tensor (1 entry per dimension).
1379+
:param strides: Number of bytes between adjacent entries (for each
1380+
per dimension).
1381+
:param readonly: Flag to indicate if the underlying storage may be
1382+
written to.
1383+
\endrst */
1384+
static memoryview from_buffer(
1385+
void *ptr, ssize_t itemsize, const char *format,
1386+
detail::any_container<ssize_t> shape,
1387+
detail::any_container<ssize_t> strides, bool readonly = false);
1388+
1389+
static memoryview from_buffer(
1390+
const void *ptr, ssize_t itemsize, const char *format,
1391+
detail::any_container<ssize_t> shape,
1392+
detail::any_container<ssize_t> strides) {
1393+
return memoryview::from_buffer(
1394+
const_cast<void*>(ptr), itemsize, format, shape, strides, true);
1395+
}
1396+
1397+
template<typename T>
1398+
static memoryview from_buffer(
1399+
T *ptr, detail::any_container<ssize_t> shape,
1400+
detail::any_container<ssize_t> strides, bool readonly = false) {
1401+
return memoryview::from_buffer(
1402+
reinterpret_cast<void*>(ptr), sizeof(T),
1403+
format_descriptor<T>::value, shape, strides, readonly);
1404+
}
1405+
1406+
template<typename T>
1407+
static memoryview from_buffer(
1408+
const T *ptr, detail::any_container<ssize_t> shape,
1409+
detail::any_container<ssize_t> strides) {
1410+
return memoryview::from_buffer(
1411+
const_cast<T*>(ptr), shape, strides, true);
1412+
}
1413+
1414+
#if PY_MAJOR_VERSION >= 3
1415+
/** \rst
1416+
Creates ``memoryview`` from static memory.
1417+
1418+
This method is meant for providing a ``memoryview`` for C/C++ buffer not
1419+
managed by Python. The caller is responsible for managing the lifetime
1420+
of ``mem``, which MUST outlive the memoryview constructed here.
1421+
1422+
This method is not available in Python 2.
1423+
1424+
See also: Python C API documentation for `PyMemoryView_FromBuffer`_.
1425+
1426+
.. _PyMemoryView_FromMemory: https://docs.python.org/c-api/memoryview.html#c.PyMemoryView_FromMemory
1427+
\endrst */
1428+
static memoryview from_memory(void *mem, ssize_t size, bool readonly = false) {
1429+
PyObject* ptr = PyMemoryView_FromMemory(
1430+
reinterpret_cast<char*>(mem), size,
1431+
(readonly) ? PyBUF_READ : PyBUF_WRITE);
1432+
if (!ptr)
1433+
pybind11_fail("Could not allocate memoryview object!");
1434+
return memoryview(object(ptr, stolen_t{}));
1435+
}
1436+
1437+
static memoryview from_memory(const void *mem, ssize_t size) {
1438+
return memoryview::from_memory(const_cast<void*>(mem), size, true);
1439+
}
1440+
#endif
13671441
};
1442+
1443+
#ifndef DOXYGEN_SHOULD_SKIP_THIS
1444+
inline memoryview memoryview::from_buffer(
1445+
void *ptr, ssize_t itemsize, const char* format,
1446+
detail::any_container<ssize_t> shape,
1447+
detail::any_container<ssize_t> strides, bool readonly) {
1448+
size_t ndim = shape->size();
1449+
if (ndim != strides->size())
1450+
pybind11_fail("memoryview: shape length doesn't match strides length");
1451+
ssize_t size = ndim ? 1 : 0;
1452+
for (size_t i = 0; i < ndim; ++i)
1453+
size *= (*shape)[i];
1454+
Py_buffer view;
1455+
view.buf = ptr;
1456+
view.obj = nullptr;
1457+
view.len = size * itemsize;
1458+
view.readonly = static_cast<int>(readonly);
1459+
view.itemsize = itemsize;
1460+
view.format = const_cast<char*>(format);
1461+
view.ndim = static_cast<int>(ndim);
1462+
view.shape = shape->data();
1463+
view.strides = strides->data();
1464+
view.suboffsets = nullptr;
1465+
view.internal = nullptr;
1466+
PyObject* obj = PyMemoryView_FromBuffer(&view);
1467+
if (!obj)
1468+
throw error_already_set();
1469+
return memoryview(object(obj, stolen_t{}));
1470+
}
1471+
#endif // DOXYGEN_SHOULD_SKIP_THIS
13681472
/// @} pytypes
13691473

13701474
/// \addtogroup python_builtins

tests/test_pytypes.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,53 @@ TEST_SUBMODULE(pytypes, m) {
318318
m.def("test_list_slicing", [](py::list a) {
319319
return a[py::slice(0, -1, 2)];
320320
});
321+
322+
m.def("test_memoryview_object", [](py::buffer b) {
323+
return py::memoryview(b);
324+
});
325+
326+
m.def("test_memoryview_buffer_info", [](py::buffer b) {
327+
return py::memoryview(b.request());
328+
});
329+
330+
m.def("test_memoryview_from_buffer", [](bool is_unsigned) {
331+
static const int16_t si16[] = { 3, 1, 4, 1, 5 };
332+
static const uint16_t ui16[] = { 2, 7, 1, 8 };
333+
if (is_unsigned)
334+
return py::memoryview::from_buffer(
335+
ui16, { 4 }, { sizeof(uint16_t) });
336+
else
337+
return py::memoryview::from_buffer(
338+
si16, { 5 }, { sizeof(int16_t) });
339+
});
340+
341+
m.def("test_memoryview_from_buffer_nativeformat", []() {
342+
static const char* format = "@i";
343+
static const int32_t arr[] = { 4, 7, 5 };
344+
return py::memoryview::from_buffer(
345+
arr, sizeof(int32_t), format, { 3 }, { sizeof(int32_t) });
346+
});
347+
348+
m.def("test_memoryview_from_buffer_empty_shape", []() {
349+
static const char* buf = "";
350+
return py::memoryview::from_buffer(buf, 1, "B", { }, { });
351+
});
352+
353+
m.def("test_memoryview_from_buffer_invalid_strides", []() {
354+
static const char* buf = "\x02\x03\x04";
355+
return py::memoryview::from_buffer(buf, 1, "B", { 3 }, { });
356+
});
357+
358+
m.def("test_memoryview_from_buffer_nullptr", []() {
359+
return py::memoryview::from_buffer(
360+
static_cast<void*>(nullptr), 1, "B", { }, { });
361+
});
362+
363+
#if PY_MAJOR_VERSION >= 3
364+
m.def("test_memoryview_from_memory", []() {
365+
const char* buf = "\xff\xe1\xab\x37";
366+
return py::memoryview::from_memory(
367+
buf, static_cast<ssize_t>(strlen(buf)));
368+
});
369+
#endif
321370
}

tests/test_pytypes.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,70 @@ def test_number_protocol():
278278
def test_list_slicing():
279279
li = list(range(100))
280280
assert li[::2] == m.test_list_slicing(li)
281+
282+
283+
@pytest.mark.parametrize('method, args, fmt, expected_view', [
284+
(m.test_memoryview_object, (b'red',), 'B', b'red'),
285+
(m.test_memoryview_buffer_info, (b'green',), 'B', b'green'),
286+
(m.test_memoryview_from_buffer, (False,), 'h', [3, 1, 4, 1, 5]),
287+
(m.test_memoryview_from_buffer, (True,), 'H', [2, 7, 1, 8]),
288+
(m.test_memoryview_from_buffer_nativeformat, (), '@i', [4, 7, 5]),
289+
])
290+
def test_memoryview(method, args, fmt, expected_view):
291+
view = method(*args)
292+
assert isinstance(view, memoryview)
293+
assert view.format == fmt
294+
if isinstance(expected_view, bytes) or sys.version_info[0] >= 3:
295+
view_as_list = list(view)
296+
else:
297+
# Using max to pick non-zero byte (big-endian vs little-endian).
298+
view_as_list = [max([ord(c) for c in s]) for s in view]
299+
assert view_as_list == list(expected_view)
300+
301+
302+
@pytest.mark.skipif(
303+
not hasattr(sys, 'getrefcount'),
304+
reason='getrefcount is not available')
305+
@pytest.mark.parametrize('method', [
306+
m.test_memoryview_object,
307+
m.test_memoryview_buffer_info,
308+
])
309+
def test_memoryview_refcount(method):
310+
buf = b'\x0a\x0b\x0c\x0d'
311+
ref_before = sys.getrefcount(buf)
312+
view = method(buf)
313+
ref_after = sys.getrefcount(buf)
314+
assert ref_before < ref_after
315+
assert list(view) == list(buf)
316+
317+
318+
def test_memoryview_from_buffer_empty_shape():
319+
view = m.test_memoryview_from_buffer_empty_shape()
320+
assert isinstance(view, memoryview)
321+
assert view.format == 'B'
322+
if sys.version_info.major < 3:
323+
# Python 2 behavior is weird, but Python 3 (the future) is fine.
324+
assert bytes(view).startswith(b'<memory at ')
325+
else:
326+
assert bytes(view) == b''
327+
328+
329+
def test_test_memoryview_from_buffer_invalid_strides():
330+
with pytest.raises(RuntimeError):
331+
m.test_memoryview_from_buffer_invalid_strides()
332+
333+
334+
def test_test_memoryview_from_buffer_nullptr():
335+
if sys.version_info.major < 3:
336+
m.test_memoryview_from_buffer_nullptr()
337+
else:
338+
with pytest.raises(ValueError):
339+
m.test_memoryview_from_buffer_nullptr()
340+
341+
342+
@pytest.mark.skipif(sys.version_info.major < 3, reason='API not available')
343+
def test_memoryview_from_memory():
344+
view = m.test_memoryview_from_memory()
345+
assert isinstance(view, memoryview)
346+
assert view.format == 'B'
347+
assert bytes(view) == b'\xff\xe1\xab\x37'

0 commit comments

Comments
 (0)