Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -2,6 +2,7 @@
- v0.28.0
- Fix mismatched behavior between `PyArrayLike1` and `PyArrayLike2` when used with floats ([#520](https://github.com/PyO3/rust-numpy/pull/520))
- Add ownership-moving conversions into `PyReadonlyArray` and `PyReadwriteArray` ([#524](https://github.com/PyO3/rust-numpy/pull/524))
- Fix UB when calling `PyArray::as_slice` and `as_slice_mut` on misaligned arrays ([#525](https://github.com/PyO3/rust-numpy/pull/525))

- v0.27.1
- Bump ndarray dependency to v0.17. ([#516](https://github.com/PyO3/rust-numpy/pull/516))
Expand Down
32 changes: 21 additions & 11 deletions src/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::cold;
use crate::convert::{ArrayExt, IntoPyArray, NpyIndex, ToNpyDims, ToPyArray};
use crate::dtype::{Element, PyArrayDescrMethods};
use crate::error::{
BorrowError, DimensionalityError, FromVecError, IgnoreError, NotContiguousError, TypeError,
AsSliceError, BorrowError, DimensionalityError, FromVecError, IgnoreError, TypeError,
DIMENSIONALITY_MISMATCH_ERR, MAX_DIMENSIONALITY_ERR,
};
use crate::npyffi::{self, npy_intp, NPY_ORDER, PY_ARRAY_API};
Expand Down Expand Up @@ -739,15 +739,20 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
/// or concurrently modified by Python or other native code.
///
/// Please consider the safe alternative [`PyReadonlyArray::as_slice`].
unsafe fn as_slice(&self) -> Result<&[T], NotContiguousError>
unsafe fn as_slice(&self) -> Result<&[T], AsSliceError>
where
T: Element,
D: Dimension,
{
if self.is_contiguous() {
Ok(slice::from_raw_parts(self.data(), self.len()))
let len = self.len();
if len == 0 {
// We can still produce a slice over zero objects regardless of whether
// the underlying pointer is aligned or not.
Ok(&[])
} else if self.is_aligned() && self.is_contiguous() {
Ok(slice::from_raw_parts(self.data(), len))
} else {
Err(NotContiguousError)
Err(AsSliceError)
}
}

Expand All @@ -761,15 +766,20 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
///
/// Please consider the safe alternative [`PyReadwriteArray::as_slice_mut`].
#[allow(clippy::mut_from_ref)]
unsafe fn as_slice_mut(&self) -> Result<&mut [T], NotContiguousError>
unsafe fn as_slice_mut(&self) -> Result<&mut [T], AsSliceError>
where
T: Element,
D: Dimension,
{
if self.is_contiguous() {
Ok(slice::from_raw_parts_mut(self.data(), self.len()))
let len = self.len();
if len == 0 {
// We can still produce a slice over zero objects regardless of whether
// the underlying pointer is aligned or not.
Ok(&mut [])
} else if self.is_aligned() && self.is_contiguous() {
Ok(slice::from_raw_parts_mut(self.data(), len))
} else {
Err(NotContiguousError)
Err(AsSliceError)
}
}

Expand Down Expand Up @@ -951,7 +961,7 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> + Sized {
/// })
/// # }
/// ```
fn to_vec(&self) -> Result<Vec<T>, NotContiguousError>
fn to_vec(&self) -> Result<Vec<T>, AsSliceError>
where
T: Element,
D: Dimension;
Expand Down Expand Up @@ -1503,7 +1513,7 @@ impl<'py, T, D> PyArrayMethods<'py, T, D> for Bound<'py, PyArray<T, D>> {
unsafe { self.cast_unchecked() }
}

fn to_vec(&self) -> Result<Vec<T>, NotContiguousError>
fn to_vec(&self) -> Result<Vec<T>, AsSliceError>
where
T: Element,
D: Dimension,
Expand Down
6 changes: 3 additions & 3 deletions src/borrow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ use pyo3::{Borrowed, Bound, CastError, FromPyObject, PyAny, PyResult};
use crate::array::{PyArray, PyArrayMethods};
use crate::convert::NpyIndex;
use crate::dtype::Element;
use crate::error::{BorrowError, NotContiguousError};
use crate::error::{AsSliceError, BorrowError};
use crate::npyffi::flags;
use crate::untyped_array::PyUntypedArrayMethods;

Expand Down Expand Up @@ -268,7 +268,7 @@ where

/// Provide an immutable slice view of the interior of the NumPy array if it is contiguous.
#[inline(always)]
pub fn as_slice(&self) -> Result<&[T], NotContiguousError> {
pub fn as_slice(&self) -> Result<&[T], AsSliceError> {
// SAFETY: Global borrow flags ensure aliasing discipline.
unsafe { self.array.as_slice() }
}
Expand Down Expand Up @@ -511,7 +511,7 @@ where

/// Provide a mutable slice view of the interior of the NumPy array if it is contiguous.
#[inline(always)]
pub fn as_slice_mut(&mut self) -> Result<&mut [T], NotContiguousError> {
pub fn as_slice_mut(&mut self) -> Result<&mut [T], AsSliceError> {
// SAFETY: Global borrow flags ensure aliasing discipline.
unsafe { self.array.as_slice_mut() }
}
Expand Down
12 changes: 5 additions & 7 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,15 @@ impl fmt::Display for FromVecError {

impl_pyerr!(FromVecError);

/// Represents that the given array is not contiguous.
/// Represents that the given array is not compatible with viewing as a Rust slice.
#[derive(Debug)]
pub struct NotContiguousError;

impl fmt::Display for NotContiguousError {
pub struct AsSliceError;
impl fmt::Display for AsSliceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "The given array is not contiguous")
write!(f, "The given array is not contiguous or is misaligned.")
}
}

impl_pyerr!(NotContiguousError);
impl_pyerr!(AsSliceError);

/// Inidcates why borrowing an array failed.
#[derive(Debug)]
Expand Down
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub use crate::borrow::{
};
pub use crate::convert::{IntoPyArray, NpyIndex, ToNpyDims, ToPyArray};
pub use crate::dtype::{dtype, Complex32, Complex64, Element, PyArrayDescr, PyArrayDescrMethods};
pub use crate::error::{BorrowError, FromVecError, NotContiguousError};
pub use crate::error::{AsSliceError, BorrowError, FromVecError};
pub use crate::npyffi::{PY_ARRAY_API, PY_UFUNC_API};
pub use crate::strings::{PyFixedString, PyFixedUnicode};
pub use crate::sum_products::{dot, einsum, inner};
Expand All @@ -130,6 +130,11 @@ pub mod prelude {
pub use crate::untyped_array::PyUntypedArrayMethods;
}

/// Deprecated type alias to [`AsSliceError`]. The new name is preferred because arrays might also
/// fail to view as a slice due to misalignment.
#[deprecated(note = "use AsSliceError instead")]
pub type NonContiguousError = AsSliceError;

#[cfg(doctest)]
mod doctest {
macro_rules! doc_comment {
Expand Down
33 changes: 33 additions & 0 deletions src/untyped_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,39 @@ pub trait PyUntypedArrayMethods<'py>: Sealed {
/// [PyArray_DTYPE]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_DTYPE
fn dtype(&self) -> Bound<'py, PyArrayDescr>;

/// Returns `true` if the internal data of the array is aligned for the dtype.
///
/// Note that NumPy considers zero-length data to be aligned regardless of the base pointer,
/// which is a weaker condition than Rust's slice guarantees. [PyArrayMethods::as_slice] will
/// safely handle the case of a misaligned zero-length array.
///
/// # Example
///
/// ```
/// use numpy::{PyArray1, PyUntypedArrayMethods};
/// use pyo3::{types::{IntoPyDict, PyAnyMethods}, Python, ffi::c_str};
///
/// # fn main() -> pyo3::PyResult<()> {
/// Python::attach(|py| {
/// let array = PyArray1::<u16>::zeros(py, 8, false);
/// assert!(array.is_aligned());
///
/// let view = py
/// .eval(
/// c_str!("array.view('u1')[1:-1].view('u2')"),
/// None,
/// Some(&[("array", array)].into_py_dict(py)?),
/// )?
/// .cast_into::<PyArray1<u16>>()?;
/// assert!(!view.is_aligned());
/// # Ok(())
/// })
/// # }
/// ```
fn is_aligned(&self) -> bool {
unsafe { check_flags(&*self.as_array_ptr(), npyffi::NPY_ARRAY_ALIGNED) }
}

/// Returns `true` if the internal data of the array is contiguous,
/// indepedently of whether C-style/row-major or Fortran-style/column-major.
///
Expand Down
60 changes: 59 additions & 1 deletion tests/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ fn not_contiguous_array(py: Python<'_>) -> Bound<'_, PyArray1<i32>> {
.unwrap()
}

fn not_aligned_array(py: Python<'_>) -> Bound<'_, PyArray1<u16>> {
py.eval(
c_str!("np.zeros(8, dtype='u2').view('u1')[1:-1].view('u2')"),
None,
Some(&get_np_locals(py)),
)
.unwrap()
.cast_into()
.unwrap()
}

#[test]
fn new_c_order() {
Python::attach(|py| {
Expand All @@ -51,6 +62,7 @@ fn new_c_order() {
assert!(arr.is_contiguous());
assert!(arr.is_c_contiguous());
assert!(!arr.is_fortran_contiguous());
assert!(arr.is_aligned());
});
}

Expand All @@ -73,6 +85,7 @@ fn new_fortran_order() {
assert!(arr.is_contiguous());
assert!(!arr.is_c_contiguous());
assert!(arr.is_fortran_contiguous());
assert!(arr.is_aligned());
});
}

Expand Down Expand Up @@ -179,7 +192,52 @@ fn as_slice() {

let not_contiguous = not_contiguous_array(py);
let err = not_contiguous.readonly().as_slice().unwrap_err();
assert_eq!(err.to_string(), "The given array is not contiguous");
assert_eq!(
err.to_string(),
"The given array is not contiguous or is misaligned."
);
let err = not_contiguous.readwrite().as_slice_mut().unwrap_err();
assert_eq!(
err.to_string(),
"The given array is not contiguous or is misaligned."
);

let not_aligned = not_aligned_array(py);
assert!(!not_aligned.is_aligned());
let err = not_aligned.readonly().as_slice().unwrap_err();
assert_eq!(
err.to_string(),
"The given array is not contiguous or is misaligned."
);
let err = not_aligned.readwrite().as_slice_mut().unwrap_err();
assert_eq!(
err.to_string(),
"The given array is not contiguous or is misaligned."
);

let misaligned_empty: Bound<'_, PyArray1<u16>> = {
let arr = not_aligned_array(py);
py.eval(
c_str!("arr[0:0]"),
None,
Some(&[("arr", arr)].into_py_dict(py).unwrap()),
)
.unwrap()
.cast_into()
.unwrap()
};
assert_eq!(
misaligned_empty.readonly().as_slice().unwrap(),
&[] as &[u16]
);
assert_eq!(
misaligned_empty.readwrite().as_slice_mut().unwrap(),
&mut [] as &mut [u16]
);
assert_eq!(
misaligned_empty.readonly().to_vec().unwrap(),
Vec::<u16>::new()
);
});
}

Expand Down
Loading