-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix buffer protocol implementation #5407
Changes from 3 commits
9f36b54
d150159
d98c86c
ea8c424
83f6ec5
63d6fd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -601,24 +601,70 @@ extern "C" inline int pybind11_getbuffer(PyObject *obj, Py_buffer *view, int fla | |
set_error(PyExc_BufferError, "Writable buffer requested for readonly storage"); | ||
return -1; | ||
} | ||
|
||
// Fill in all the information, and then downgrade as requested by the caller, or raise an | ||
// error if that's not possible. | ||
view->obj = obj; | ||
view->ndim = 1; | ||
view->internal = info; | ||
view->buf = info->ptr; | ||
view->itemsize = info->itemsize; | ||
view->len = view->itemsize; | ||
for (auto s : info->shape) { | ||
view->len *= s; | ||
} | ||
view->ndim = (int) info->ndim; | ||
view->shape = info->shape.data(); | ||
view->strides = info->strides.data(); | ||
view->readonly = static_cast<int>(info->readonly); | ||
if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) { | ||
view->format = const_cast<char *>(info->format.c_str()); | ||
} | ||
if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES) { | ||
view->ndim = (int) info->ndim; | ||
view->strides = info->strides.data(); | ||
view->shape = info->shape.data(); | ||
|
||
// Note, all contiguity flags imply PyBUF_STRIDES and lower. | ||
if ((flags & PyBUF_C_CONTIGUOUS) == PyBUF_C_CONTIGUOUS) { | ||
if (PyBuffer_IsContiguous(view, 'C') == 0) { | ||
std::memset(view, 0, sizeof(Py_buffer)); | ||
delete info; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a follow-on PR: We should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will that work when put into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @QuLogic Is there a chance that you could try this in a follow-on PR, now that this one is merged? |
||
set_error(PyExc_BufferError, | ||
"C-contiguous buffer requested for discontiguous storage"); | ||
return -1; | ||
} | ||
} else if ((flags & PyBUF_F_CONTIGUOUS) == PyBUF_F_CONTIGUOUS) { | ||
if (PyBuffer_IsContiguous(view, 'F') == 0) { | ||
std::memset(view, 0, sizeof(Py_buffer)); | ||
delete info; | ||
set_error(PyExc_BufferError, | ||
"Fortran-contiguous buffer requested for discontiguous storage"); | ||
return -1; | ||
} | ||
} else if ((flags & PyBUF_ANY_CONTIGUOUS) == PyBUF_ANY_CONTIGUOUS) { | ||
if (PyBuffer_IsContiguous(view, 'A') == 0) { | ||
std::memset(view, 0, sizeof(Py_buffer)); | ||
delete info; | ||
set_error(PyExc_BufferError, "Contiguous buffer requested for discontiguous storage"); | ||
return -1; | ||
} | ||
|
||
// If no strides are requested, the buffer must be C-contiguous. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe move this comment to the top of the |
||
// https://docs.python.org/3/c-api/buffer.html#contiguity-requests | ||
} else if ((flags & PyBUF_STRIDES) != PyBUF_STRIDES) { | ||
if (PyBuffer_IsContiguous(view, 'C') == 0) { | ||
std::memset(view, 0, sizeof(Py_buffer)); | ||
delete info; | ||
set_error(PyExc_BufferError, | ||
"C-contiguous buffer requested for discontiguous storage"); | ||
return -1; | ||
} | ||
|
||
view->strides = nullptr; | ||
|
||
// Since this is a contiguous buffer, it can also pretend to be 1D. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsure, maybe better to follow numpy (IIUC?) and make this |
||
if ((flags & PyBUF_ND) != PyBUF_ND) { | ||
view->shape = nullptr; | ||
view->ndim = 1; | ||
} | ||
} | ||
|
||
Py_INCREF(view->obj); | ||
return 0; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -239,3 +239,117 @@ def test_buffer_exception(): | |
memoryview(m.BrokenMatrix(1, 1)) | ||
assert isinstance(excinfo.value.__cause__, RuntimeError) | ||
assert "for context" in str(excinfo.value.__cause__) | ||
|
||
|
||
def test_to_pybuffer(): | ||
mat = m.Matrix(5, 4) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Could you please try that? Roughly:
Motivation behind my suggestion:
(I think you can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No need; it's already at the top of the file. |
||
|
||
info = m.get_py_buffer(mat, m.PyBUF_SIMPLE) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 1 # See discussion on PR #5407. | ||
assert info.shape is None | ||
assert info.strides is None | ||
assert info.suboffsets is None | ||
assert not info.readonly | ||
info = m.get_py_buffer(mat, m.PyBUF_ND) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides is None | ||
assert info.suboffsets is None | ||
assert not info.readonly | ||
info = m.get_py_buffer(mat, m.PyBUF_STRIDES) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides == [4 * info.itemsize, info.itemsize] | ||
assert info.suboffsets is None | ||
assert not info.readonly | ||
info = m.get_py_buffer(mat, m.PyBUF_INDIRECT) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides == [4 * info.itemsize, info.itemsize] | ||
assert info.suboffsets is None # Should be filled in here, but we don't use it. | ||
assert not info.readonly | ||
|
||
# A Fortran-shaped buffer can only be accessed at PyBUF_STRIDES level or higher. | ||
mat = m.FortranMatrix(5, 4) | ||
info = m.get_py_buffer(mat, m.PyBUF_STRIDES) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides == [info.itemsize, 5 * info.itemsize] | ||
assert info.suboffsets is None | ||
assert not info.readonly | ||
info = m.get_py_buffer(mat, m.PyBUF_INDIRECT) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides == [info.itemsize, 5 * info.itemsize] | ||
assert info.suboffsets is None # Should be filled in here, but we don't use it. | ||
assert not info.readonly | ||
|
||
mat = m.DiscontiguousMatrix(5, 4, 2, 3) | ||
info = m.get_py_buffer(mat, m.PyBUF_STRIDES) | ||
assert info.itemsize == ctypes.sizeof(ctypes.c_float) | ||
assert info.len == mat.rows() * mat.cols() * info.itemsize | ||
assert info.ndim == 2 | ||
assert info.shape == [5, 4] | ||
assert info.strides == [2 * 4 * 3 * info.itemsize, 3 * info.itemsize] | ||
assert info.suboffsets is None | ||
assert not info.readonly | ||
|
||
|
||
def test_to_pybuffer_contiguity(): | ||
def check_strides(mat): | ||
# The full block is memset to 0, so fill it with non-zero in real spots. | ||
expected = np.arange(1, mat.rows() * mat.cols() + 1).reshape( | ||
(mat.rows(), mat.cols()) | ||
) | ||
for i in range(mat.rows()): | ||
for j in range(mat.cols()): | ||
mat[i, j] = expected[i, j] | ||
# If all strides are correct, the exposed buffer should match the input. | ||
np.testing.assert_array_equal(np.array(mat), expected) | ||
|
||
mat = m.Matrix(5, 4) | ||
check_strides(mat) | ||
# Should work in C-contiguous mode, but not Fortran order. | ||
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS) | ||
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS) | ||
|
||
mat = m.FortranMatrix(5, 4) | ||
check_strides(mat) | ||
# These flags imply C-contiguity, so won't work. | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_SIMPLE) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_ND) | ||
# Should work in Fortran-contiguous mode, but not C order. | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS) | ||
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS) | ||
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS) | ||
|
||
mat = m.DiscontiguousMatrix(5, 4, 2, 3) | ||
check_strides(mat) | ||
# Should never work. | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_SIMPLE) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_ND) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_C_CONTIGUOUS) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_ANY_CONTIGUOUS) | ||
with pytest.raises(BufferError): | ||
m.get_py_buffer(mat, m.PyBUF_F_CONTIGUOUS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please change this to
static_cast<int>(info->ndim)
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to ask if we should prefer C++ static_cast over C-style casts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, although we're not set up to catch C-style casts automatically.
A battle for another day. In the meantime I'm simply changing C-style casts to C++ casts in code that's touched in some way.