Skip to content

Commit e8ad33b

Browse files
authored
Fix buffer_info for ctypes buffers (#2502) (#2503)
* tests: New test for ctypes buffers (#2502) * fix: fix buffer_info segfault on views with no stride (pybind11#2502) * Explicit conversions in buffer_info to make clang happy (#2502) * Another explicit cast in buffer_info constructor for clang (#2502) * Simpler implementation of buffer_info constructor from Py_buffer. * Move test_ctypes_buffer into test_buffers * Comment on why view->strides may be NULL (and fix some whitespace) * Use c_strides() instead of zero when view->strides is NULL. c_strides and f_strides are moved from numpy.h (py::array) to buffer_info.h (py::detail) so they can be used from the buffer_info Py_buffer constructor. * Increase ctypes buffer test coverage in test_buffers. * Split ctypes tests and skip one which is broken in PyPy2.
1 parent 6bcd220 commit e8ad33b

File tree

4 files changed

+107
-22
lines changed

4 files changed

+107
-22
lines changed

include/pybind11/buffer_info.h

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@
1313

1414
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1515

16+
PYBIND11_NAMESPACE_BEGIN(detail)
17+
18+
// Default, C-style strides
19+
inline std::vector<ssize_t> c_strides(const std::vector<ssize_t> &shape, ssize_t itemsize) {
20+
auto ndim = shape.size();
21+
std::vector<ssize_t> strides(ndim, itemsize);
22+
if (ndim > 0)
23+
for (size_t i = ndim - 1; i > 0; --i)
24+
strides[i - 1] = strides[i] * shape[i];
25+
return strides;
26+
}
27+
28+
// F-style strides; default when constructing an array_t with `ExtraFlags & f_style`
29+
inline std::vector<ssize_t> f_strides(const std::vector<ssize_t> &shape, ssize_t itemsize) {
30+
auto ndim = shape.size();
31+
std::vector<ssize_t> strides(ndim, itemsize);
32+
for (size_t i = 1; i < ndim; ++i)
33+
strides[i] = strides[i - 1] * shape[i - 1];
34+
return strides;
35+
}
36+
37+
PYBIND11_NAMESPACE_END(detail)
38+
1639
/// Information record describing a Python buffer object
1740
struct buffer_info {
1841
void *ptr = nullptr; // Pointer to the underlying storage
@@ -53,7 +76,14 @@ struct buffer_info {
5376

5477
explicit buffer_info(Py_buffer *view, bool ownview = true)
5578
: buffer_info(view->buf, view->itemsize, view->format, view->ndim,
56-
{view->shape, view->shape + view->ndim}, {view->strides, view->strides + view->ndim}, view->readonly) {
79+
{view->shape, view->shape + view->ndim},
80+
/* Though buffer::request() requests PyBUF_STRIDES, ctypes objects
81+
* ignore this flag and return a view with NULL strides.
82+
* When strides are NULL, build them manually. */
83+
view->strides
84+
? std::vector<ssize_t>(view->strides, view->strides + view->ndim)
85+
: detail::c_strides({view->shape, view->shape + view->ndim}, view->itemsize),
86+
view->readonly) {
5787
this->m_view = view;
5888
this->ownview = ownview;
5989
}

include/pybind11/numpy.h

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ class array : public buffer {
564564
const void *ptr = nullptr, handle base = handle()) {
565565

566566
if (strides->empty())
567-
*strides = c_strides(*shape, dt.itemsize());
567+
*strides = detail::c_strides(*shape, dt.itemsize());
568568

569569
auto ndim = shape->size();
570570
if (ndim != strides->size())
@@ -792,25 +792,6 @@ class array : public buffer {
792792
throw std::domain_error("array is not writeable");
793793
}
794794

795-
// Default, C-style strides
796-
static std::vector<ssize_t> c_strides(const std::vector<ssize_t> &shape, ssize_t itemsize) {
797-
auto ndim = shape.size();
798-
std::vector<ssize_t> strides(ndim, itemsize);
799-
if (ndim > 0)
800-
for (size_t i = ndim - 1; i > 0; --i)
801-
strides[i - 1] = strides[i] * shape[i];
802-
return strides;
803-
}
804-
805-
// F-style strides; default when constructing an array_t with `ExtraFlags & f_style`
806-
static std::vector<ssize_t> f_strides(const std::vector<ssize_t> &shape, ssize_t itemsize) {
807-
auto ndim = shape.size();
808-
std::vector<ssize_t> strides(ndim, itemsize);
809-
for (size_t i = 1; i < ndim; ++i)
810-
strides[i] = strides[i - 1] * shape[i - 1];
811-
return strides;
812-
}
813-
814795
template<typename... Ix> void check_dimensions(Ix... index) const {
815796
check_dimensions_impl(ssize_t(0), shape(), ssize_t(index)...);
816797
}
@@ -869,7 +850,9 @@ template <typename T, int ExtraFlags = array::forcecast> class array_t : public
869850

870851
explicit array_t(ShapeContainer shape, const T *ptr = nullptr, handle base = handle())
871852
: array_t(private_ctor{}, std::move(shape),
872-
ExtraFlags & f_style ? f_strides(*shape, itemsize()) : c_strides(*shape, itemsize()),
853+
ExtraFlags & f_style
854+
? detail::f_strides(*shape, itemsize())
855+
: detail::c_strides(*shape, itemsize()),
873856
ptr, base) { }
874857

875858
explicit array_t(ssize_t count, const T *ptr = nullptr, handle base = handle())

tests/test_buffers.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include "pybind11_tests.h"
1111
#include "constructor_stats.h"
12+
#include <pybind11/stl.h>
1213

1314
TEST_SUBMODULE(buffers, m) {
1415
// test_from_python / test_to_python:
@@ -192,4 +193,22 @@ TEST_SUBMODULE(buffers, m) {
192193
.def_readwrite("readonly", &BufferReadOnlySelect::readonly)
193194
.def_buffer(&BufferReadOnlySelect::get_buffer_info);
194195

196+
// Expose buffer_info for testing.
197+
py::class_<py::buffer_info>(m, "buffer_info")
198+
.def(py::init<>())
199+
.def_readonly("itemsize", &py::buffer_info::itemsize)
200+
.def_readonly("size", &py::buffer_info::size)
201+
.def_readonly("format", &py::buffer_info::format)
202+
.def_readonly("ndim", &py::buffer_info::ndim)
203+
.def_readonly("shape", &py::buffer_info::shape)
204+
.def_readonly("strides", &py::buffer_info::strides)
205+
.def_readonly("readonly", &py::buffer_info::readonly)
206+
.def("__repr__", [](py::handle self) {
207+
return py::str("itemsize={0.itemsize!r}, size={0.size!r}, format={0.format!r}, ndim={0.ndim!r}, shape={0.shape!r}, strides={0.strides!r}, readonly={0.readonly!r}").format(self);
208+
})
209+
;
210+
211+
m.def("get_buffer_info", [](py::buffer buffer) {
212+
return buffer.request();
213+
});
195214
}

tests/test_buffers.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import io
33
import struct
4+
import ctypes
45

56
import pytest
67

@@ -107,3 +108,55 @@ def test_selective_readonly_buffer():
107108
memoryview(buf)[0] = b'\0' if env.PY2 else 0
108109
with pytest.raises(TypeError):
109110
io.BytesIO(b'1').readinto(buf)
111+
112+
113+
def test_ctypes_array_1d():
114+
char1d = (ctypes.c_char * 10)()
115+
int1d = (ctypes.c_int * 15)()
116+
long1d = (ctypes.c_long * 7)()
117+
118+
for carray in (char1d, int1d, long1d):
119+
info = m.get_buffer_info(carray)
120+
assert info.itemsize == ctypes.sizeof(carray._type_)
121+
assert info.size == len(carray)
122+
assert info.ndim == 1
123+
assert info.shape == [info.size]
124+
assert info.strides == [info.itemsize]
125+
assert not info.readonly
126+
127+
128+
def test_ctypes_array_2d():
129+
char2d = ((ctypes.c_char * 10) * 4)()
130+
int2d = ((ctypes.c_int * 15) * 3)()
131+
long2d = ((ctypes.c_long * 7) * 2)()
132+
133+
for carray in (char2d, int2d, long2d):
134+
info = m.get_buffer_info(carray)
135+
assert info.itemsize == ctypes.sizeof(carray[0]._type_)
136+
assert info.size == len(carray) * len(carray[0])
137+
assert info.ndim == 2
138+
assert info.shape == [len(carray), len(carray[0])]
139+
assert info.strides == [info.itemsize * len(carray[0]), info.itemsize]
140+
assert not info.readonly
141+
142+
143+
@pytest.mark.skipif(
144+
"env.PYPY and env.PY2", reason="PyPy2 bytes buffer not reported as readonly"
145+
)
146+
def test_ctypes_from_buffer():
147+
test_pystr = b"0123456789"
148+
for pyarray in (test_pystr, bytearray(test_pystr)):
149+
pyinfo = m.get_buffer_info(pyarray)
150+
151+
if pyinfo.readonly:
152+
cbytes = (ctypes.c_char * len(pyarray)).from_buffer_copy(pyarray)
153+
cinfo = m.get_buffer_info(cbytes)
154+
else:
155+
cbytes = (ctypes.c_char * len(pyarray)).from_buffer(pyarray)
156+
cinfo = m.get_buffer_info(cbytes)
157+
158+
assert cinfo.size == pyinfo.size
159+
assert cinfo.ndim == pyinfo.ndim
160+
assert cinfo.shape == pyinfo.shape
161+
assert cinfo.strides == pyinfo.strides
162+
assert not cinfo.readonly

0 commit comments

Comments
 (0)