Skip to content

Commit c24aabb

Browse files
committed
Obey contiguity requests for buffer protocol
If a contiguous buffer is requested, and the underlying buffer isn't, then that should raise. This matches NumPy behaviour if you do something like: ``` struct.unpack_from('5d', np.arange(20.0)[::4]) # Raises for contiguity ``` Also, if a buffer is contiguous, then it can masquerade as a less-complex buffer, either by dropping strides, or even pretending to be 1D. This matches NumPy behaviour if you do something like: ``` a = np.full((3, 5), 30.0) struct.unpack_from('15d', a) # --> Produces 1D tuple from 2D buffer. ```
1 parent d150159 commit c24aabb

File tree

3 files changed

+244
-7
lines changed

3 files changed

+244
-7
lines changed

include/pybind11/detail/class.h

+47-7
Original file line numberDiff line numberDiff line change
@@ -601,26 +601,66 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla
601601
set_error(PyExc_BufferError, "Writable buffer requested for readonly storage");
602602
return -1;
603603
}
604+
605+
// Fill in all the information, and then downgrade as requested by the caller, or raise an error if that's not possible.
604606
view->obj = obj;
605-
view->ndim = 1; // See discussion on PR #5407.
606607
view->internal = info;
607608
view->buf = info->ptr;
608609
view->itemsize = info->itemsize;
609610
view->len = view->itemsize;
610611
for (auto s : info->shape) {
611612
view->len *= s;
612613
}
614+
view->ndim = (int) info->ndim;
615+
view->shape = info->shape.data();
616+
view->strides = info->strides.data();
613617
view->readonly = static_cast<int>(info->readonly);
614618
if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) {
615619
view->format = const_cast<char *>(info->format.c_str());
616620
}
617-
if ((flags & PyBUF_ND) == PyBUF_ND) {
618-
view->ndim = (int) info->ndim;
619-
view->shape = info->shape.data();
620-
}
621-
if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) {
622-
view->strides = info->strides.data();
621+
622+
// Note, all contiguity flags imply PyBUF_STRIDES and lower.
623+
if ((flags & PyBUF_C_CONTIGUOUS) == PyBUF_C_CONTIGUOUS) {
624+
if (!PyBuffer_IsContiguous(view, 'C')) {
625+
std::memset(view, 0, sizeof(Py_buffer));
626+
delete info;
627+
set_error(PyExc_BufferError, "C-contiguous buffer requested for discontiguous storage");
628+
return -1;
629+
}
630+
} else if ((flags & PyBUF_F_CONTIGUOUS) == PyBUF_F_CONTIGUOUS) {
631+
if (!PyBuffer_IsContiguous(view, 'F')) {
632+
std::memset(view, 0, sizeof(Py_buffer));
633+
delete info;
634+
set_error(PyExc_BufferError, "Fortran-contiguous buffer requested for discontiguous storage");
635+
return -1;
636+
}
637+
} else if ((flags & PyBUF_ANY_CONTIGUOUS) == PyBUF_ANY_CONTIGUOUS) {
638+
if (!PyBuffer_IsContiguous(view, 'A')) {
639+
std::memset(view, 0, sizeof(Py_buffer));
640+
delete info;
641+
set_error(PyExc_BufferError, "Contiguous buffer requested for discontiguous storage");
642+
return -1;
643+
}
644+
645+
// If no strides are requested, the buffer must be C-contiguous.
646+
// https://docs.python.org/3/c-api/buffer.html#contiguity-requests
647+
} else if ((flags & PyBUF_STRIDES) != PyBUF_STRIDES) {
648+
if (!PyBuffer_IsContiguous(view, 'C')) {
649+
std::memset(view, 0, sizeof(Py_buffer));
650+
delete info;
651+
set_error(PyExc_BufferError, "C-contiguous buffer requested for discontiguous storage");
652+
return -1;
653+
}
654+
655+
view->strides = nullptr;
656+
657+
// Since this is a contiguous buffer, it can also pretend to be 1D.
658+
if ((flags & PyBUF_ND) != PyBUF_ND) {
659+
view->shape = nullptr;
660+
view->ndim = 1;
661+
}
623662
}
663+
624664
Py_INCREF(view->obj);
625665
return 0;
626666
}

tests/test_buffers.cpp

+122
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,125 @@ TEST_SUBMODULE(buffers, m) {
167167
sizeof(float)});
168168
});
169169

170+
// A matrix that uses Fortran storage order.
171+
class FortranMatrix : public Matrix {
172+
public:
173+
FortranMatrix(py::ssize_t rows, py::ssize_t cols) : Matrix(cols, rows) {
174+
print_created(this, std::to_string(rows) + "x" + std::to_string(cols) + " Fortran matrix");
175+
}
176+
177+
float operator()(py::ssize_t i, py::ssize_t j) const {
178+
return Matrix::operator()(j, i);
179+
}
180+
181+
float &operator()(py::ssize_t i, py::ssize_t j) {
182+
return Matrix::operator()(j, i);
183+
}
184+
185+
using Matrix::data;
186+
187+
py::ssize_t rows() const { return Matrix::cols(); }
188+
py::ssize_t cols() const { return Matrix::rows(); }
189+
};
190+
py::class_<FortranMatrix, Matrix>(m, "FortranMatrix", py::buffer_protocol())
191+
.def(py::init<py::ssize_t, py::ssize_t>())
192+
193+
.def("rows", &FortranMatrix::rows)
194+
.def("cols", &FortranMatrix::cols)
195+
196+
/// Bare bones interface
197+
.def("__getitem__",
198+
[](const FortranMatrix &m, std::pair<py::ssize_t, py::ssize_t> i) {
199+
if (i.first >= m.rows() || i.second >= m.cols()) {
200+
throw py::index_error();
201+
}
202+
return m(i.first, i.second);
203+
})
204+
.def("__setitem__",
205+
[](FortranMatrix &m, std::pair<py::ssize_t, py::ssize_t> i, float v) {
206+
if (i.first >= m.rows() || i.second >= m.cols()) {
207+
throw py::index_error();
208+
}
209+
m(i.first, i.second) = v;
210+
})
211+
/// Provide buffer access
212+
.def_buffer([](FortranMatrix &m) -> py::buffer_info {
213+
return py::buffer_info(
214+
m.data(), /* Pointer to buffer */
215+
{m.rows(), m.cols()}, /* Buffer dimensions */
216+
/* Strides (in bytes) for each index */
217+
{sizeof(float), sizeof(float) * size_t(m.rows())});
218+
});
219+
220+
// A matrix that uses a discontiguous underlying memory block.
221+
class DiscontiguousMatrix : public Matrix {
222+
public:
223+
DiscontiguousMatrix(py::ssize_t rows, py::ssize_t cols,
224+
py::ssize_t row_factor, py::ssize_t col_factor)
225+
: Matrix(rows * row_factor, cols * col_factor),
226+
m_row_factor(row_factor), m_col_factor(col_factor)
227+
{
228+
print_created(this,
229+
std::to_string(rows) + "(*" + std::to_string(row_factor) + ")x" +
230+
std::to_string(cols) + "(*" + std::to_string(col_factor) + ") matrix");
231+
}
232+
233+
~DiscontiguousMatrix() {
234+
print_destroyed(this,
235+
std::to_string(rows() / m_row_factor) + "(*" + std::to_string(m_row_factor) + ")x" +
236+
std::to_string(cols() / m_col_factor) + "(*" + std::to_string(m_col_factor) + ") matrix");
237+
}
238+
239+
float operator()(py::ssize_t i, py::ssize_t j) const {
240+
return Matrix::operator()(i * m_row_factor, j * m_col_factor);
241+
}
242+
243+
float &operator()(py::ssize_t i, py::ssize_t j) {
244+
return Matrix::operator()(i * m_row_factor, j * m_col_factor);
245+
}
246+
247+
using Matrix::data;
248+
249+
py::ssize_t rows() const { return Matrix::rows() / m_row_factor; }
250+
py::ssize_t cols() const { return Matrix::cols() / m_col_factor; }
251+
py::ssize_t row_factor() const { return m_row_factor; }
252+
py::ssize_t col_factor() const { return m_col_factor; }
253+
254+
private:
255+
py::ssize_t m_row_factor;
256+
py::ssize_t m_col_factor;
257+
};
258+
py::class_<DiscontiguousMatrix, Matrix>(m, "DiscontiguousMatrix", py::buffer_protocol())
259+
.def(py::init<py::ssize_t, py::ssize_t, py::ssize_t, py::ssize_t>())
260+
261+
.def("rows", &DiscontiguousMatrix::rows)
262+
.def("cols", &DiscontiguousMatrix::cols)
263+
264+
/// Bare bones interface
265+
.def("__getitem__",
266+
[](const DiscontiguousMatrix &m, std::pair<py::ssize_t, py::ssize_t> i) {
267+
if (i.first >= m.rows() || i.second >= m.cols()) {
268+
throw py::index_error();
269+
}
270+
return m(i.first, i.second);
271+
})
272+
.def("__setitem__",
273+
[](DiscontiguousMatrix &m, std::pair<py::ssize_t, py::ssize_t> i, float v) {
274+
if (i.first >= m.rows() || i.second >= m.cols()) {
275+
throw py::index_error();
276+
}
277+
m(i.first, i.second) = v;
278+
})
279+
/// Provide buffer access
280+
.def_buffer([](DiscontiguousMatrix &m) -> py::buffer_info {
281+
return py::buffer_info(
282+
m.data(), /* Pointer to buffer */
283+
{m.rows(), m.cols()}, /* Buffer dimensions */
284+
/* Strides (in bytes) for each index */
285+
{size_t(m.col_factor()) * sizeof(float) * size_t(m.cols()) * size_t(m.row_factor()),
286+
size_t(m.col_factor()) * sizeof(float)});
287+
});
288+
170289
class BrokenMatrix : public Matrix {
171290
public:
172291
BrokenMatrix(py::ssize_t rows, py::ssize_t cols) : Matrix(rows, cols) {}
@@ -274,6 +393,9 @@ TEST_SUBMODULE(buffers, m) {
274393
m.attr("PyBUF_ND") = PyBUF_ND;
275394
m.attr("PyBUF_STRIDES") = PyBUF_STRIDES;
276395
m.attr("PyBUF_INDIRECT") = PyBUF_INDIRECT;
396+
m.attr("PyBUF_C_CONTIGUOUS") = PyBUF_C_CONTIGUOUS;
397+
m.attr("PyBUF_F_CONTIGUOUS") = PyBUF_F_CONTIGUOUS;
398+
m.attr("PyBUF_ANY_CONTIGUOUS") = PyBUF_ANY_CONTIGUOUS;
277399

278400
m.def("get_py_buffer", [](const py::object &object, int flags) {
279401
Py_buffer buffer;

tests/test_buffers.py

+75
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,78 @@ def test_to_pybuffer():
276276
assert info.strides == [4 * info.itemsize, info.itemsize]
277277
assert info.suboffsets is None # Should be filled in here, but we don't use it.
278278
assert not info.readonly
279+
280+
# A Fortran-shaped buffer can only be accessed at PyBUF_STRIDES level or higher.
281+
mat = m.FortranMatrix(5, 4)
282+
info = m.get_py_buffer(mat, m.PyBUF_STRIDES)
283+
assert info.itemsize == ctypes.sizeof(ctypes.c_float)
284+
assert info.len == mat.rows() * mat.cols() * info.itemsize
285+
assert info.ndim == 2
286+
assert info.shape == [5, 4]
287+
assert info.strides == [info.itemsize, 5 * info.itemsize]
288+
assert info.suboffsets is None
289+
assert not info.readonly
290+
info = m.get_py_buffer(mat, m.PyBUF_INDIRECT)
291+
assert info.itemsize == ctypes.sizeof(ctypes.c_float)
292+
assert info.len == mat.rows() * mat.cols() * info.itemsize
293+
assert info.ndim == 2
294+
assert info.shape == [5, 4]
295+
assert info.strides == [info.itemsize, 5 * info.itemsize]
296+
assert info.suboffsets is None # Should be filled in here, but we don't use it.
297+
assert not info.readonly
298+
299+
mat = m.DiscontiguousMatrix(5, 4, 2, 3)
300+
info = m.get_py_buffer(mat, m.PyBUF_STRIDES)
301+
assert info.itemsize == ctypes.sizeof(ctypes.c_float)
302+
assert info.len == mat.rows() * mat.cols() * info.itemsize
303+
assert info.ndim == 2
304+
assert info.shape == [5, 4]
305+
assert info.strides == [2 * 4 * 3 * info.itemsize, 3 * info.itemsize]
306+
assert info.suboffsets is None
307+
assert not info.readonly
308+
309+
310+
def test_to_pybuffer_contiguity():
311+
def check_strides(mat):
312+
# The full block is memset to 0, so fill it with non-zero in real spots.
313+
expected = np.arange(1, mat.rows() * mat.cols() + 1).reshape((mat.rows(), mat.cols()))
314+
for i in range(mat.rows()):
315+
for j in range(mat.cols()):
316+
mat[i, j] = expected[i, j]
317+
# If all strides are correct, the exposed buffer should match the input.
318+
np.testing.assert_array_equal(np.array(mat), expected)
319+
320+
mat = m.Matrix(5, 4)
321+
check_strides(mat)
322+
# Should work in C-contiguous mode, but not Fortran order.
323+
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS)
324+
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS)
325+
with pytest.raises(BufferError):
326+
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS)
327+
328+
mat = m.FortranMatrix(5, 4)
329+
check_strides(mat)
330+
# These flags imply C-contiguity, so won't work.
331+
with pytest.raises(BufferError):
332+
m.get_py_buffer(mat, m.PyBUF_SIMPLE)
333+
with pytest.raises(BufferError):
334+
m.get_py_buffer(mat, m.PyBUF_ND)
335+
# Should work in Fortran-contiguous mode, but not C order.
336+
with pytest.raises(BufferError):
337+
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS)
338+
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS)
339+
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS)
340+
341+
mat = m.DiscontiguousMatrix(5, 4, 2, 3)
342+
check_strides(mat)
343+
# Should never work.
344+
with pytest.raises(BufferError):
345+
m.get_py_buffer(mat, m.PyBUF_SIMPLE)
346+
with pytest.raises(BufferError):
347+
m.get_py_buffer(mat, m.PyBUF_ND)
348+
with pytest.raises(BufferError):
349+
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS)
350+
with pytest.raises(BufferError):
351+
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS)
352+
with pytest.raises(BufferError):
353+
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS)

0 commit comments

Comments
 (0)