Skip to content

Commit 0a20571

Browse files
committed
Handle misaligned arrays in as_slice
Previously, `as_slice` would only check that arrays are contiguous. While misaligned non-empty arrays remain problematic across the library (`get`, for example, would still be invalid), this change begins handling this situation in a function that already returns `Result`. As a convenience, the empty slice is returned for zero-length arrays, regardless of the alignment of the underlying pointer. Misaligned zero-length pointers can fairly easily arise from allocator optimisations. For example, CPython reliably returned an odd-address pointer in `bytearray()` in (at least) CPython 3.14.0 on Linux x86-64, which caused empty Numpy arrays passing through Pickle protocol 5 (with the default handling of its `PickleBuffer`s) to be backed by a misaligned pointer for multi-byte aligned dtypes.
1 parent 4149c5d commit 0a20571

3 files changed

Lines changed: 77 additions & 4 deletions

File tree

src/array.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -744,8 +744,13 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> {
744744
T: Element,
745745
D: Dimension,
746746
{
747-
if self.is_contiguous() {
748-
Ok(slice::from_raw_parts(self.data(), self.len()))
747+
let len = self.len();
748+
if len == 0 {
749+
// We can still produce a slice over zero objects regardless of whether
750+
// the underlying pointer is aligned or not.
751+
Ok(&[])
752+
} else if self.is_aligned() && self.is_contiguous() {
753+
Ok(slice::from_raw_parts(self.data(), len))
749754
} else {
750755
Err(NotContiguousError)
751756
}
@@ -766,8 +771,13 @@ pub trait PyArrayMethods<'py, T, D>: PyUntypedArrayMethods<'py> {
766771
T: Element,
767772
D: Dimension,
768773
{
769-
if self.is_contiguous() {
770-
Ok(slice::from_raw_parts_mut(self.data(), self.len()))
774+
let len = self.len();
775+
if len == 0 {
776+
// We can still produce a slice over zero objects regardless of whether
777+
// the underlying pointer is aligned or not.
778+
Ok(&mut [])
779+
} else if self.is_aligned() && self.is_contiguous() {
780+
Ok(slice::from_raw_parts_mut(self.data(), len))
771781
} else {
772782
Err(NotContiguousError)
773783
}

src/untyped_array.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ pub trait PyUntypedArrayMethods<'py>: Sealed {
102102
/// [PyArray_DTYPE]: https://numpy.org/doc/stable/reference/c-api/array.html#c.PyArray_DTYPE
103103
fn dtype(&self) -> Bound<'py, PyArrayDescr>;
104104

105+
/// Returns `true` if the internal data of the array is aligned for the dtype.
106+
///
107+
/// Note that NumPy considers zero-length data to be aligned regardless of the base pointer,
108+
/// which is a weaker condition than Rust's slice guarantees. [PyArrayMethods::as_slice] will
109+
/// safely handle the case of a misaligned zero-length array.
110+
///
111+
/// # Example
112+
///
113+
/// ```
114+
/// use numpy::{PyArray1, PyUntypedArrayMethods};
115+
/// use pyo3::{types::{IntoPyDict, PyAnyMethods}, Python, ffi::c_str};
116+
///
117+
/// # fn main() -> pyo3::PyResult<()> {
118+
/// Python::attach(|py| {
119+
/// let array = PyArray1::<u16>::zeros(py, 8, false);
120+
/// assert!(array.is_aligned());
121+
///
122+
/// let view = py
123+
/// .eval(
124+
/// c_str!("array.view('u1')[1:-1].view('u2')"),
125+
/// None,
126+
/// Some(&[("array", array)].into_py_dict(py)?),
127+
/// )?
128+
/// .cast_into::<PyArray1<u16>>()?;
129+
/// assert!(!view.is_aligned());
130+
/// # Ok(())
131+
/// })
132+
/// # }
133+
/// ```
134+
fn is_aligned(&self) -> bool {
135+
unsafe { check_flags(&*self.as_array_ptr(), npyffi::NPY_ARRAY_ALIGNED) }
136+
}
137+
105138
/// Returns `true` if the internal data of the array is contiguous,
106139
/// indepedently of whether C-style/row-major or Fortran-style/column-major.
107140
///

tests/array.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ fn not_contiguous_array(py: Python<'_>) -> Bound<'_, PyArray1<i32>> {
3232
.unwrap()
3333
}
3434

35+
fn not_aligned_array(py: Python<'_>) -> Bound<'_, PyArray1<u16>> {
36+
py.eval(
37+
c_str!("np.zeros(8, dtype='u2').view('u1')[1:-1].view('u2')"),
38+
None,
39+
Some(&get_np_locals(py)),
40+
)
41+
.unwrap()
42+
.cast_into()
43+
.unwrap()
44+
}
45+
3546
#[test]
3647
fn new_c_order() {
3748
Python::attach(|py| {
@@ -51,6 +62,7 @@ fn new_c_order() {
5162
assert!(arr.is_contiguous());
5263
assert!(arr.is_c_contiguous());
5364
assert!(!arr.is_fortran_contiguous());
65+
assert!(arr.is_aligned());
5466
});
5567
}
5668

@@ -73,6 +85,7 @@ fn new_fortran_order() {
7385
assert!(arr.is_contiguous());
7486
assert!(!arr.is_c_contiguous());
7587
assert!(arr.is_fortran_contiguous());
88+
assert!(arr.is_aligned());
7689
});
7790
}
7891

@@ -180,6 +193,23 @@ fn as_slice() {
180193
let not_contiguous = not_contiguous_array(py);
181194
let err = not_contiguous.readonly().as_slice().unwrap_err();
182195
assert_eq!(err.to_string(), "The given array is not contiguous");
196+
197+
let not_aligned = not_aligned_array(py);
198+
assert!(!not_aligned.is_aligned());
199+
assert!(not_aligned.readonly().as_slice().is_err());
200+
201+
let misaligned_empty: Bound<'_, PyArray1<u16>> = {
202+
let arr = not_aligned_array(py);
203+
py.eval(
204+
c_str!("arr[0:0]"),
205+
None,
206+
Some(&[("arr", arr)].into_py_dict(py).unwrap()),
207+
)
208+
.unwrap()
209+
.cast_into()
210+
.unwrap()
211+
};
212+
assert_eq!(misaligned_empty.readonly().as_slice().unwrap(), &[] as &[u16]);
183213
});
184214
}
185215

0 commit comments

Comments
 (0)