Skip to content
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

buffer: tidy up exceptions #1534

Merged
merged 1 commit into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The `auto-initialize` feature is no longer enabled by default. [#1443](https://github.com/PyO3/pyo3/pull/1443)
- Change `PyCFunction::new()` and `PyCFunction::new_with_keywords()` to take `&'static str` arguments rather than implicitly copying (and leaking) them. [#1450](https://github.com/PyO3/pyo3/pull/1450)
- Deprecate `PyModule` methods `call`, `call0`, `call1` and `get`. [#1492](https://github.com/PyO3/pyo3/pull/1492)
- Add length information to `PyBufferError`s raised from `PyBuffer::copy_to_slice` and `PyBuffer::copy_from_slice`. [#1534](https://github.com/PyO3/pyo3/pull/1534)
- Automatically provide `-undefined` and `dynamic_lookup` linker arguments on macOS with `extension-module` feature. [#1539](https://github.com/PyO3/pyo3/pull/1539)

### Removed
Expand Down
111 changes: 63 additions & 48 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
// DEALINGS IN THE SOFTWARE.

//! `PyBuffer` implementation
use crate::err::{self, PyResult};
use crate::{exceptions, ffi, AsPyPointer, FromPyObject, PyAny, PyNativeType, Python};
use std::ffi::CStr;
use crate::{
err, exceptions::PyBufferError, ffi, AsPyPointer, FromPyObject, PyAny, PyNativeType, PyResult,
Python,
};
use std::marker::PhantomData;
use std::os::raw;
use std::pin::Pin;
use std::{cell, mem, ptr, slice};
use std::{ffi::CStr, fmt::Debug};

/// Allows access to the underlying buffer used by a python object such as `bytes`, `bytearray` or `array.array`.
// use Pin<Box> because Python expects that the Py_buffer struct has a stable memory address
Expand All @@ -35,6 +37,24 @@ pub struct PyBuffer<T>(Pin<Box<ffi::Py_buffer>>, PhantomData<T>);
unsafe impl<T> Send for PyBuffer<T> {}
unsafe impl<T> Sync for PyBuffer<T> {}

impl<T> Debug for PyBuffer<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("PyBuffer")
.field("buf", &self.0.buf)
.field("obj", &self.0.obj)
.field("len", &self.0.len)
.field("itemsize", &self.0.itemsize)
.field("readonly", &self.0.readonly)
.field("ndim", &self.0.ndim)
.field("format", &self.0.format)
.field("shape", &self.0.shape)
.field("strides", &self.0.strides)
.field("suboffsets", &self.0.suboffsets)
.field("internal", &self.0.internal)
.finish()
}
}

#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ElementType {
SignedInteger { bytes: usize },
Expand Down Expand Up @@ -147,19 +167,6 @@ pub unsafe trait Element: Copy {
fn is_compatible_format(format: &CStr) -> bool;
}

fn validate(b: &ffi::Py_buffer) -> PyResult<()> {
// shape and stride information must be provided when we use PyBUF_FULL_RO
if b.shape.is_null() {
return Err(exceptions::PyBufferError::new_err("Shape is Null"));
}
if b.strides.is_null() {
return Err(exceptions::PyBufferError::new_err(
"PyBuffer: Strides is Null",
));
}
Ok(())
}

impl<'source, T: Element> FromPyObject<'source> for PyBuffer<T> {
fn extract(obj: &PyAny) -> PyResult<PyBuffer<T>> {
Self::get(obj)
Expand All @@ -169,25 +176,37 @@ impl<'source, T: Element> FromPyObject<'source> for PyBuffer<T> {
impl<T: Element> PyBuffer<T> {
/// Get the underlying buffer from the specified python object.
pub fn get(obj: &PyAny) -> PyResult<PyBuffer<T>> {
unsafe {
let mut buf = Box::pin(ffi::Py_buffer::new());
// TODO: use nightly API Box::new_uninit() once stable
let mut buf = Box::new(mem::MaybeUninit::uninit());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take this approach, then you can remove Py_buffer::new().
Though I'm a bit doubtful about how effective MaybeUninit is (I mean that this is effective only when we raise an error, but then buf is immediately dropped...).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future I would like to remove Py_buffer::new(), but I think to do so right now would be inconvenient for users because PyBufferProtocol basically forces you to handle the raw ffi pointer (which probably points to uninitialized memory which C created).

I chose MaybeUninit here because that's pretty much identical to what C would do: we create a slab of memory and pass its location to PyObject_GetBuffer to initialize the contents.

let buf: Box<ffi::Py_buffer> = unsafe {
err::error_on_minusone(
obj.py(),
ffi::PyObject_GetBuffer(obj.as_ptr(), &mut *buf, ffi::PyBUF_FULL_RO),
ffi::PyObject_GetBuffer(obj.as_ptr(), buf.as_mut_ptr(), ffi::PyBUF_FULL_RO),
)?;
validate(&buf)?;
let buf = PyBuffer(buf, PhantomData);
// Type Check
if mem::size_of::<T>() == buf.item_size()
&& (buf.0.buf as usize) % mem::align_of::<T>() == 0
&& T::is_compatible_format(buf.format())
{
Ok(buf)
} else {
Err(exceptions::PyBufferError::new_err(
"Incompatible type as buffer",
))
}
// Safety: buf is initialized by PyObject_GetBuffer.
// TODO: use nightly API Box::assume_init() once stable
mem::transmute(buf)
};
// Create PyBuffer immediately so that if validation checks fail, the PyBuffer::drop code
// will call PyBuffer_Release (thus avoiding any leaks).
let buf = PyBuffer(Pin::from(buf), PhantomData);

if buf.0.shape.is_null() {
Err(PyBufferError::new_err("shape is null"))
} else if buf.0.strides.is_null() {
Err(PyBufferError::new_err("strides is null"))
} else if mem::size_of::<T>() != buf.item_size() || !T::is_compatible_format(buf.format()) {
Err(PyBufferError::new_err(format!(
"buffer contents are not compatible with {}",
std::any::type_name::<T>()
)))
} else if buf.0.buf.align_offset(mem::align_of::<T>()) != 0 {
Err(PyBufferError::new_err(format!(
"buffer contents are insufficiently aligned for {}",
std::any::type_name::<T>()
)))
} else {
Ok(buf)
}
}

Expand Down Expand Up @@ -441,9 +460,11 @@ impl<T: Element> PyBuffer<T> {

fn copy_to_slice_impl(&self, py: Python, target: &mut [T], fort: u8) -> PyResult<()> {
if mem::size_of_val(target) != self.len_bytes() {
return Err(exceptions::PyBufferError::new_err(
"Slice length does not match buffer length.",
));
return Err(PyBufferError::new_err(format!(
"slice to copy to (of length {}) does not match buffer length of {}",
target.len(),
self.item_count()
)));
}
unsafe {
err::error_on_minusone(
Expand Down Expand Up @@ -525,12 +546,13 @@ impl<T: Element> PyBuffer<T> {

fn copy_from_slice_impl(&self, py: Python, source: &[T], fort: u8) -> PyResult<()> {
if self.readonly() {
return buffer_readonly_error();
}
if mem::size_of_val(source) != self.len_bytes() {
return Err(exceptions::PyBufferError::new_err(
"Slice length does not match buffer length.",
));
return Err(PyBufferError::new_err("cannot write to read-only buffer"));
} else if mem::size_of_val(source) != self.len_bytes() {
return Err(PyBufferError::new_err(format!(
"slice to copy from (of length {}) does not match buffer length of {}",
source.len(),
self.item_count()
)));
}
unsafe {
err::error_on_minusone(
Expand Down Expand Up @@ -562,13 +584,6 @@ impl<T: Element> PyBuffer<T> {
}
}

#[inline(always)]
fn buffer_readonly_error() -> PyResult<()> {
Err(exceptions::PyBufferError::new_err(
"Cannot write to read-only buffer.",
))
}

impl<T> Drop for PyBuffer<T> {
fn drop(&mut self) {
Python::with_gil(|_| unsafe { ffi::PyBuffer_Release(&mut *self.0) });
Expand Down
145 changes: 145 additions & 0 deletions tests/test_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#![cfg(not(Py_LIMITED_API))]

use pyo3::{
buffer::PyBuffer, class::PyBufferProtocol, exceptions::PyBufferError, ffi, prelude::*,
AsPyPointer,
};
use std::{
ffi::CStr,
os::raw::{c_int, c_void},
ptr,
};

#[macro_use]
mod common;

enum TestGetBufferError {
NullShape,
NullStrides,
IncorrectItemSize,
IncorrectFormat,
IncorrectAlignment,
}

#[pyclass]
struct TestBufferErrors {
buf: Vec<u32>,
error: Option<TestGetBufferError>,
}

#[pyproto]
impl PyBufferProtocol for TestBufferErrors {
fn bf_getbuffer(slf: PyRefMut<Self>, view: *mut ffi::Py_buffer, flags: c_int) -> PyResult<()> {
if view.is_null() {
return Err(PyBufferError::new_err("View is null"));
}

if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE {
return Err(PyBufferError::new_err("Object is not writable"));
}

unsafe {
(*view).obj = slf.as_ptr();
ffi::Py_INCREF((*view).obj);
}

let bytes = &slf.buf;

unsafe {
(*view).buf = bytes.as_ptr() as *mut c_void;
(*view).len = bytes.len() as isize;
(*view).readonly = 1;
(*view).itemsize = std::mem::size_of::<u32>() as isize;

let msg = CStr::from_bytes_with_nul(b"I\0").unwrap();
(*view).format = msg.as_ptr() as *mut _;

(*view).ndim = 1;
(*view).shape = (&((*view).len)) as *const _ as *mut _;

(*view).strides = &((*view).itemsize) as *const _ as *mut _;

(*view).suboffsets = ptr::null_mut();
(*view).internal = ptr::null_mut();

if let Some(err) = &slf.error {
use TestGetBufferError::*;
match err {
NullShape => {
(*view).shape = std::ptr::null_mut();
}
NullStrides => {
(*view).strides = std::ptr::null_mut();
}
IncorrectItemSize => {
(*view).itemsize += 1;
}
IncorrectFormat => {
(*view).format = CStr::from_bytes_with_nul(b"B\0").unwrap().as_ptr() as _;
}
IncorrectAlignment => (*view).buf = (*view).buf.add(1),
}
}
}

Ok(())
}

fn bf_releasebuffer(_slf: PyRefMut<Self>, _view: *mut ffi::Py_buffer) {}
}

#[test]
fn test_get_buffer_errors() {
Python::with_gil(|py| {
let instance = Py::new(
py,
TestBufferErrors {
buf: vec![0, 1, 2, 3],
error: None,
},
)
.unwrap();

assert!(PyBuffer::<u32>::get(instance.as_ref(py)).is_ok());

instance.borrow_mut(py).error = Some(TestGetBufferError::NullShape);
assert_eq!(
PyBuffer::<u32>::get(instance.as_ref(py))
.unwrap_err()
.to_string(),
"BufferError: shape is null"
);

instance.borrow_mut(py).error = Some(TestGetBufferError::NullStrides);
assert_eq!(
PyBuffer::<u32>::get(instance.as_ref(py))
.unwrap_err()
.to_string(),
"BufferError: strides is null"
);

instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectItemSize);
assert_eq!(
PyBuffer::<u32>::get(instance.as_ref(py))
.unwrap_err()
.to_string(),
"BufferError: buffer contents are not compatible with u32"
);

instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectFormat);
assert_eq!(
PyBuffer::<u32>::get(instance.as_ref(py))
.unwrap_err()
.to_string(),
"BufferError: buffer contents are not compatible with u32"
);

instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectAlignment);
assert_eq!(
PyBuffer::<u32>::get(instance.as_ref(py))
.unwrap_err()
.to_string(),
"BufferError: buffer contents are insufficiently aligned for u32"
);
});
}