From 301ecb96b59077aa6956a7107fe143c2e28eaef7 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Thu, 19 Mar 2026 18:03:02 +0800 Subject: [PATCH 01/25] feat: introduce stack-allocated `PyBuffer` --- src/buffer.rs | 448 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) diff --git a/src/buffer.rs b/src/buffer.rs index e22a235cad4..51f4027c225 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -825,6 +825,279 @@ impl_element!(isize, SignedInteger); impl_element!(f32, Float); impl_element!(f64, Float); +/// Stack-allocated typed buffer view. Not constructible directly — use +/// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. +/// +/// This is a lightweight alternative to [`PyBuffer`] that avoids heap allocation +/// by placing the `Py_buffer` on the stack. The scoped closure API ensures the +/// buffer cannot be moved +#[repr(transparent)] +pub struct PyBufferView(PyUntypedBufferView, PhantomData<[T]>); + +/// Unlike [`PyUntypedBuffer`] which always requests `PyBUF_FULL_RO`, this type allows +/// arbitrary flags, so flag-dependent accessors like [`format()`](Self::format), +/// [`shape()`](Self::shape), and [`strides()`](Self::strides) return `Option`. +pub struct PyUntypedBufferView { + raw: ffi::Py_buffer, + flags: c_int, +} + +impl PyUntypedBufferView { + /// Acquire a buffer view on the stack, pass it to `f`, then release the buffer. + /// + /// The `flags` parameter controls which buffer fields are requested from the exporter. + /// Use constants like [`ffi::PyBUF_SIMPLE`], [`ffi::PyBUF_FULL_RO`], etc. + pub fn with( + obj: &Bound<'_, PyAny>, + flags: c_int, + f: impl FnOnce(&PyUntypedBufferView) -> R, + ) -> PyResult { + let mut view = mem::MaybeUninit::::uninit(); + let view_ptr = view.as_mut_ptr(); + + unsafe { + ptr::addr_of_mut!((*view_ptr).flags).write(flags); + } + + err::error_on_minusone(obj.py(), unsafe { + ffi::PyObject_GetBuffer(obj.as_ptr(), ptr::addr_of_mut!((*view_ptr).raw), flags) + })?; + + // TODO: needs a cleanup strategy — MaybeUninit never drops its contents, so PyBuffer_Release is not currently called. + Ok(f(unsafe { view.assume_init_ref() })) + } + + /// Gets the pointer to the start of the buffer memory. + #[inline] + pub fn buf_ptr(&self) -> *mut c_void { + self.raw.buf + } + + /// Gets whether the underlying buffer is read-only. + #[inline] + pub fn readonly(&self) -> bool { + self.raw.readonly != 0 + } + + /// Gets the size of a single element, in bytes. + #[inline] + pub fn item_size(&self) -> usize { + self.raw.itemsize as usize + } + + /// Gets the total number of items. + #[inline] + pub fn item_count(&self) -> usize { + (self.raw.len as usize) / (self.raw.itemsize as usize) + } + + /// `item_size() * item_count()`. + /// For contiguous arrays, this is the length of the underlying memory block. + #[inline] + pub fn len_bytes(&self) -> usize { + self.raw.len as usize + } + + /// Gets the number of dimensions. + /// + /// May be 0 to indicate a single scalar value. + #[inline] + pub fn dimensions(&self) -> usize { + self.raw.ndim as usize + } + + /// A string in struct module style syntax describing the contents of a single item. + /// + /// Returns `None` if `PyBUF_FORMAT` was not included in the flags. + #[inline] + pub fn format(&self) -> Option<&CStr> { + if self.flags & ffi::PyBUF_FORMAT != ffi::PyBUF_FORMAT { + return None; + } + if self.raw.format.is_null() { + Some(ffi::c_str!("B")) + } else { + Some(unsafe { CStr::from_ptr(self.raw.format) }) + } + } + + /// Returns the shape array. `shape[i]` is the length of dimension `i`. + /// + /// Returns `None` if `PyBUF_ND` was not included in the flags. + #[inline] + pub fn shape(&self) -> Option<&[usize]> { + if self.flags & ffi::PyBUF_ND != ffi::PyBUF_ND || self.raw.shape.is_null() { + return None; + } + + Some(unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) }) + } + + /// Returns the strides array. + /// + /// Returns `None` if `PyBUF_STRIDES` was not included in the flags. + #[inline] + pub fn strides(&self) -> Option<&[isize]> { + if self.flags & ffi::PyBUF_STRIDES != ffi::PyBUF_STRIDES || self.raw.strides.is_null() { + return None; + } + + Some(unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) }) + } + + /// Returns the suboffsets array. + /// + /// May return `None` even with `PyBUF_INDIRECT` if the exporter sets `suboffsets` to NULL. + #[inline] + pub fn suboffsets(&self) -> Option<&[isize]> { + if self.raw.suboffsets.is_null() { + None + } else { + Some(unsafe { slice::from_raw_parts(self.raw.suboffsets, self.raw.ndim as usize) }) + } + } + + /// Gets whether the buffer is contiguous in C-style order. + #[inline] + pub fn is_c_contiguous(&self) -> bool { + unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } + } + + /// Gets whether the buffer is contiguous in Fortran-style order. + #[inline] + pub fn is_fortran_contiguous(&self) -> bool { + unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } + } + + /// Attempt to interpret this untyped view as containing elements of type `T`. + /// + /// Requires that `PyBUF_FORMAT` was included in the flags. + pub fn as_typed(&self) -> PyResult<&PyBufferView> { + self.ensure_compatible_with::()?; + // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView + Ok(unsafe { NonNull::from(self).cast::>().as_ref() }) + } + + fn ensure_compatible_with(&self) -> PyResult<()> { + let format = self.format().ok_or_else(|| { + PyBufferError::new_err( + "buffer format not available (PyBUF_FORMAT flag was not requested)", + ) + })?; + + if mem::size_of::() != self.item_size() || !T::is_compatible_format(format) { + Err(PyBufferError::new_err(format!( + "buffer contents are not compatible with {}", + std::any::type_name::() + ))) + } else if self.raw.buf.align_offset(mem::align_of::()) != 0 { + Err(PyBufferError::new_err(format!( + "buffer contents are insufficiently aligned for {}", + std::any::type_name::() + ))) + } else { + Ok(()) + } + } +} + +impl Debug for PyUntypedBufferView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + debug_buffer_view("PyUntypedBufferView", self, f) + } +} + +impl PyBufferView { + /// Acquire a typed buffer view on the stack with `PyBUF_FULL_RO` flags, + /// validating that the buffer format is compatible with `T`. + pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { + Self::with_flags(obj, ffi::PyBUF_FULL_RO, f) + } + + /// Acquire a typed buffer view on the stack with user-specified flags. + /// + /// The flags must include `PyBUF_FORMAT` for type validation to succeed. + pub fn with_flags( + obj: &Bound<'_, PyAny>, + flags: c_int, + f: impl FnOnce(&PyBufferView) -> R, + ) -> PyResult { + PyUntypedBufferView::with(obj, flags, |view| view.as_typed::().map(f))? + } + + /// Gets the buffer memory as a slice. + /// + /// Returns `None` if the buffer is not C-contiguous. + /// + /// The returned slice uses type [`ReadOnlyCell`] because it's theoretically possible + /// for any call into the Python runtime to modify the values in the slice. + pub fn as_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [ReadOnlyCell]> { + if self.is_c_contiguous() { + unsafe { + Some(slice::from_raw_parts( + self.0.raw.buf.cast(), + self.item_count(), + )) + } + } else { + None + } + } + + /// Gets the buffer memory as a mutable slice. + /// + /// Returns `None` if the buffer is read-only or not C-contiguous. + /// + /// The returned slice uses type [`Cell`](cell::Cell) because it's theoretically possible + /// for any call into the Python runtime to modify the values in the slice. + pub fn as_mut_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [cell::Cell]> { + if !self.readonly() && self.is_c_contiguous() { + unsafe { + Some(slice::from_raw_parts( + self.0.raw.buf.cast(), + self.item_count(), + )) + } + } else { + None + } + } +} + +impl std::ops::Deref for PyBufferView { + type Target = PyUntypedBufferView; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Debug for PyBufferView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + debug_buffer_view("PyBufferView", &self.0, f) + } +} + +fn debug_buffer_view( + name: &str, + view: &PyUntypedBufferView, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + f.debug_struct(name) + .field("buf", &view.raw.buf) + .field("obj", &view.raw.obj) + .field("len", &view.raw.len) + .field("itemsize", &view.raw.itemsize) + .field("readonly", &view.raw.readonly) + .field("ndim", &view.raw.ndim) + .field("format", &view.format()) + .field("shape", &view.shape()) + .field("strides", &view.strides()) + .field("suboffsets", &view.suboffsets()) + .field("internal", &view.raw.internal) + .finish() +} + #[cfg(test)] mod tests { use super::*; @@ -1161,4 +1434,179 @@ mod tests { assert!(buffer.as_fortran_slice_ptr().is_none()); }); } + + #[test] + fn test_untyped_buffer_view_simple() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + PyUntypedBufferView::with(&bytes, ffi::PyBUF_SIMPLE, |view| { + assert!(!view.buf_ptr().is_null()); + assert_eq!(view.len_bytes(), 5); + assert_eq!(view.item_size(), 1); + assert_eq!(view.item_count(), 5); + assert!(view.readonly()); + assert_eq!(view.dimensions(), 1); + // PyBUF_SIMPLE doesn't include FORMAT, ND, or STRIDES + assert!(view.format().is_none()); + assert!(view.shape().is_none()); + assert!(view.strides().is_none()); + assert!(view.suboffsets().is_none()); + }) + .unwrap(); + }); + } + + #[test] + fn test_untyped_buffer_view_full() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + PyUntypedBufferView::with(&bytes, ffi::PyBUF_FULL_RO, |view| { + assert!(!view.buf_ptr().is_null()); + assert_eq!(view.len_bytes(), 5); + assert_eq!(view.item_size(), 1); + assert_eq!(view.item_count(), 5); + assert!(view.readonly()); + assert_eq!(view.dimensions(), 1); + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert_eq!(view.shape().unwrap(), [5]); + assert_eq!(view.strides().unwrap(), [1]); + assert!(view.suboffsets().is_none()); + assert!(view.is_c_contiguous()); + assert!(view.is_fortran_contiguous()); + }) + .unwrap(); + }); + } + + #[test] + fn test_typed_buffer_view() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + PyBufferView::::with(&bytes, |view| { + assert_eq!(view.dimensions(), 1); + assert_eq!(view.item_count(), 5); + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert_eq!(view.shape().unwrap(), [5]); + + let slice = view.as_slice(py).unwrap(); + assert_eq!(slice.len(), 5); + assert_eq!(slice[0].get(), b'a'); + assert_eq!(slice[4].get(), b'e'); + + // bytes are read-only + assert!(view.as_mut_slice(py).is_none()); + }) + .unwrap(); + }); + } + + #[test] + fn test_buffer_view_array() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) + .unwrap(); + PyBufferView::::with(&array, |view| { + assert_eq!(view.dimensions(), 1); + assert_eq!(view.item_count(), 4); + assert_eq!(view.format().unwrap().to_str().unwrap(), "f"); + assert_eq!(view.shape().unwrap(), [4]); + + let slice = view.as_slice(py).unwrap(); + assert_eq!(slice.len(), 4); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[3].get(), 2.5); + + // array.array is writable + let mut_slice = view.as_mut_slice(py).unwrap(); + assert_eq!(mut_slice[0].get(), 1.0); + mut_slice[3].set(2.75); + assert_eq!(slice[3].get(), 2.75); + }) + .unwrap(); + }); + } + + #[test] + fn test_buffer_view_option_accessors() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + + PyUntypedBufferView::with(&bytes, ffi::PyBUF_SIMPLE, |view| { + assert!(view.format().is_none()); + assert!(view.shape().is_none()); + assert!(view.strides().is_none()); + }) + .unwrap(); + + PyUntypedBufferView::with(&bytes, ffi::PyBUF_ND, |view| { + assert!(view.format().is_none()); + assert_eq!(view.shape().unwrap(), [5]); + assert!(view.strides().is_none()); + }) + .unwrap(); + + PyUntypedBufferView::with(&bytes, ffi::PyBUF_STRIDES, |view| { + assert!(view.format().is_none()); + assert_eq!(view.shape().unwrap(), [5]); + assert_eq!(view.strides().unwrap(), [1]); + }) + .unwrap(); + + PyUntypedBufferView::with(&bytes, ffi::PyBUF_FORMAT, |view| { + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert!(view.shape().is_none()); + assert!(view.strides().is_none()); + }) + .unwrap(); + }); + } + + #[test] + fn test_buffer_view_error() { + Python::attach(|py| { + let list = crate::types::PyList::empty(py); + let result = PyUntypedBufferView::with(&list, ffi::PyBUF_SIMPLE, |_view| {}); + assert!(result.is_err()); + }); + } + + #[test] + fn test_buffer_view_debug() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + + PyUntypedBufferView::with(&bytes, ffi::PyBUF_FULL_RO, |view| { + let expected = format!( + concat!( + "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", + "len: 5, itemsize: 1, readonly: 1, ", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "strides: Some([1]), suboffsets: None, internal: {:?} }}", + ), + view.raw.buf, view.raw.obj, view.raw.internal, + ); + let debug_repr = format!("{:?}", view); + assert_eq!(debug_repr, expected); + }) + .unwrap(); + + PyBufferView::::with(&bytes, |view| { + let expected = format!( + concat!( + "PyBufferView {{ buf: {:?}, obj: {:?}, ", + "len: 5, itemsize: 1, readonly: 1, ", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "strides: Some([1]), suboffsets: None, internal: {:?} }}", + ), + view.0.raw.buf, view.0.raw.obj, view.0.raw.internal, + ); + let debug_repr = format!("{:?}", view); + assert_eq!(debug_repr, expected); + }) + .unwrap(); + }); + } } From 9146bbd6b9ef1f5e321dcf97637dbb94243041b5 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Thu, 19 Mar 2026 18:24:47 +0800 Subject: [PATCH 02/25] docs: update CHANGELOG --- guide/src/conversions/tables.md | 2 +- newsfragments/5894.added.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5894.added.md diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index b7a67ef3a82..8680faa55c9 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -29,7 +29,7 @@ The table below contains the Python type and the corresponding function argument | `slice` | - | `PySlice` | | `type` | - | `PyType` | | `module` | - | `PyModule` | -| `collections.abc.Buffer` | - | `PyBuffer` | +| `collections.abc.Buffer` | - | `PyBuffer`, `PyBufferView` | | `datetime.datetime` | `SystemTime`, `chrono::DateTime`[^7], `chrono::NaiveDateTime`[^7] | `PyDateTime` | | `datetime.date` | `chrono::NaiveDate`[^7] | `PyDate` | | `datetime.time` | `chrono::NaiveTime`[^7] | `PyTime` | diff --git a/newsfragments/5894.added.md b/newsfragments/5894.added.md new file mode 100644 index 00000000000..85c1695022f --- /dev/null +++ b/newsfragments/5894.added.md @@ -0,0 +1 @@ +Add `PyBufferView` and `PyUntypedBufferView`, stack-allocated alternatives to `PyBuffer` and `PyUntypedBuffer` with a scoped closure API that avoids heap allocation. From 45600dd22656fd4f1c27349ad88cedd96fb5e0a2 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Sat, 21 Mar 2026 00:43:55 +0800 Subject: [PATCH 03/25] refactor: apply suggestions --- src/buffer.rs | 144 +++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 67 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 51f4027c225..a515019848f 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -838,8 +838,7 @@ pub struct PyBufferView(PyUntypedBufferView, PhantomData<[T]>); /// arbitrary flags, so flag-dependent accessors like [`format()`](Self::format), /// [`shape()`](Self::shape), and [`strides()`](Self::strides) return `Option`. pub struct PyUntypedBufferView { - raw: ffi::Py_buffer, - flags: c_int, + raw: mem::MaybeUninit, } impl PyUntypedBufferView { @@ -852,50 +851,60 @@ impl PyUntypedBufferView { flags: c_int, f: impl FnOnce(&PyUntypedBufferView) -> R, ) -> PyResult { - let mut view = mem::MaybeUninit::::uninit(); - let view_ptr = view.as_mut_ptr(); - - unsafe { - ptr::addr_of_mut!((*view_ptr).flags).write(flags); - } + let mut raw = mem::MaybeUninit::::uninit(); err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), ptr::addr_of_mut!((*view_ptr).raw), flags) + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) })?; - // TODO: needs a cleanup strategy — MaybeUninit never drops its contents, so PyBuffer_Release is not currently called. - Ok(f(unsafe { view.assume_init_ref() })) + // Construct view only after successful GetBuffer, so Drop always + // runs on an initialized Py_buffer. + let mut view = PyUntypedBufferView { raw }; + + // When shape is NULL the consumer must assume itemsize == 1. + if view.raw().shape.is_null() { + unsafe { &mut *view.raw.as_mut_ptr() }.itemsize = 1; + } + + Ok(f(&view)) + } + + #[inline] + fn raw(&self) -> &ffi::Py_buffer { + // SAFETY: PyUntypedBufferView is only constructed after a successful + // PyObject_GetBuffer call, so raw is always initialized. + unsafe { self.raw.assume_init_ref() } } /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { - self.raw.buf + self.raw().buf } /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { - self.raw.readonly != 0 + self.raw().readonly != 0 } /// Gets the size of a single element, in bytes. #[inline] pub fn item_size(&self) -> usize { - self.raw.itemsize as usize + self.raw().itemsize as usize } /// Gets the total number of items. #[inline] pub fn item_count(&self) -> usize { - (self.raw.len as usize) / (self.raw.itemsize as usize) + (self.raw().len as usize) / (self.raw().itemsize as usize) } /// `item_size() * item_count()`. /// For contiguous arrays, this is the length of the underlying memory block. #[inline] pub fn len_bytes(&self) -> usize { - self.raw.len as usize + self.raw().len as usize } /// Gets the number of dimensions. @@ -903,46 +912,42 @@ impl PyUntypedBufferView { /// May be 0 to indicate a single scalar value. #[inline] pub fn dimensions(&self) -> usize { - self.raw.ndim as usize + self.raw().ndim as usize } - /// A string in struct module style syntax describing the contents of a single item. - /// - /// Returns `None` if `PyBUF_FORMAT` was not included in the flags. + /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) + /// string describing the contents of a single item. Defaults to `"B"` if NULL. #[inline] - pub fn format(&self) -> Option<&CStr> { - if self.flags & ffi::PyBUF_FORMAT != ffi::PyBUF_FORMAT { - return None; - } - if self.raw.format.is_null() { - Some(ffi::c_str!("B")) + pub fn format(&self) -> &CStr { + if self.raw().format.is_null() { + ffi::c_str!("B") } else { - Some(unsafe { CStr::from_ptr(self.raw.format) }) + unsafe { CStr::from_ptr(self.raw().format) } } } /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// - /// Returns `None` if `PyBUF_ND` was not included in the flags. + /// Returns `None` if the exporter set `shape` to NULL (e.g. `PyBUF_SIMPLE` was requested). #[inline] pub fn shape(&self) -> Option<&[usize]> { - if self.flags & ffi::PyBUF_ND != ffi::PyBUF_ND || self.raw.shape.is_null() { + if self.raw().shape.is_null() { return None; } - Some(unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) }) + Some(unsafe { slice::from_raw_parts(self.raw().shape.cast(), self.raw().ndim as usize) }) } /// Returns the strides array. /// - /// Returns `None` if `PyBUF_STRIDES` was not included in the flags. + /// Returns `None` if the exporter set `strides` to NULL (e.g. `PyBUF_SIMPLE` was requested). #[inline] pub fn strides(&self) -> Option<&[isize]> { - if self.flags & ffi::PyBUF_STRIDES != ffi::PyBUF_STRIDES || self.raw.strides.is_null() { + if self.raw().strides.is_null() { return None; } - Some(unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) }) + Some(unsafe { slice::from_raw_parts(self.raw().strides, self.raw().ndim as usize) }) } /// Returns the suboffsets array. @@ -950,23 +955,23 @@ impl PyUntypedBufferView { /// May return `None` even with `PyBUF_INDIRECT` if the exporter sets `suboffsets` to NULL. #[inline] pub fn suboffsets(&self) -> Option<&[isize]> { - if self.raw.suboffsets.is_null() { + if self.raw().suboffsets.is_null() { None } else { - Some(unsafe { slice::from_raw_parts(self.raw.suboffsets, self.raw.ndim as usize) }) + Some(unsafe { slice::from_raw_parts(self.raw().suboffsets, self.raw().ndim as usize) }) } } /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'C' as std::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'F' as std::ffi::c_char) != 0 } } /// Attempt to interpret this untyped view as containing elements of type `T`. @@ -979,18 +984,12 @@ impl PyUntypedBufferView { } fn ensure_compatible_with(&self) -> PyResult<()> { - let format = self.format().ok_or_else(|| { - PyBufferError::new_err( - "buffer format not available (PyBUF_FORMAT flag was not requested)", - ) - })?; - - if mem::size_of::() != self.item_size() || !T::is_compatible_format(format) { + if mem::size_of::() != self.item_size() || !T::is_compatible_format(self.format()) { Err(PyBufferError::new_err(format!( "buffer contents are not compatible with {}", std::any::type_name::() ))) - } else if self.raw.buf.align_offset(mem::align_of::()) != 0 { + } else if self.raw().buf.align_offset(mem::align_of::()) != 0 { Err(PyBufferError::new_err(format!( "buffer contents are insufficiently aligned for {}", std::any::type_name::() @@ -1001,6 +1000,12 @@ impl PyUntypedBufferView { } } +impl Drop for PyUntypedBufferView { + fn drop(&mut self) { + unsafe { ffi::PyBuffer_Release(self.raw.as_mut_ptr()) } + } +} + impl Debug for PyUntypedBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { debug_buffer_view("PyUntypedBufferView", self, f) @@ -1035,7 +1040,7 @@ impl PyBufferView { if self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.raw.buf.cast(), + self.0.raw().buf.cast(), self.item_count(), )) } @@ -1054,7 +1059,7 @@ impl PyBufferView { if !self.readonly() && self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.raw.buf.cast(), + self.0.raw().buf.cast(), self.item_count(), )) } @@ -1083,18 +1088,19 @@ fn debug_buffer_view( view: &PyUntypedBufferView, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { + let raw = view.raw(); f.debug_struct(name) - .field("buf", &view.raw.buf) - .field("obj", &view.raw.obj) - .field("len", &view.raw.len) - .field("itemsize", &view.raw.itemsize) - .field("readonly", &view.raw.readonly) - .field("ndim", &view.raw.ndim) + .field("buf", &raw.buf) + .field("obj", &raw.obj) + .field("len", &raw.len) + .field("itemsize", &raw.itemsize) + .field("readonly", &raw.readonly) + .field("ndim", &raw.ndim) .field("format", &view.format()) .field("shape", &view.shape()) .field("strides", &view.strides()) .field("suboffsets", &view.suboffsets()) - .field("internal", &view.raw.internal) + .field("internal", &raw.internal) .finish() } @@ -1446,8 +1452,8 @@ mod tests { assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // PyBUF_SIMPLE doesn't include FORMAT, ND, or STRIDES - assert!(view.format().is_none()); + // PyBUF_SIMPLE doesn't include ND or STRIDES; format defaults to "B" + assert_eq!(view.format().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); assert!(view.suboffsets().is_none()); @@ -1467,7 +1473,7 @@ mod tests { assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); assert_eq!(view.strides().unwrap(), [1]); assert!(view.suboffsets().is_none()); @@ -1485,7 +1491,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); let slice = view.as_slice(py).unwrap(); @@ -1511,7 +1517,7 @@ mod tests { PyBufferView::::with(&array, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 4); - assert_eq!(view.format().unwrap().to_str().unwrap(), "f"); + assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape().unwrap(), [4]); let slice = view.as_slice(py).unwrap(); @@ -1535,28 +1541,28 @@ mod tests { let bytes = PyBytes::new(py, b"abcde"); PyUntypedBufferView::with(&bytes, ffi::PyBUF_SIMPLE, |view| { - assert!(view.format().is_none()); + assert_eq!(view.format().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); }) .unwrap(); PyUntypedBufferView::with(&bytes, ffi::PyBUF_ND, |view| { - assert!(view.format().is_none()); + assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); assert!(view.strides().is_none()); }) .unwrap(); PyUntypedBufferView::with(&bytes, ffi::PyBUF_STRIDES, |view| { - assert!(view.format().is_none()); + assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); assert_eq!(view.strides().unwrap(), [1]); }) .unwrap(); PyUntypedBufferView::with(&bytes, ffi::PyBUF_FORMAT, |view| { - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); + assert_eq!(view.format().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); }) @@ -1583,10 +1589,12 @@ mod tests { concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "ndim: 1, format: \"B\", shape: Some([5]), ", "strides: Some([1]), suboffsets: None, internal: {:?} }}", ), - view.raw.buf, view.raw.obj, view.raw.internal, + view.raw().buf, + view.raw().obj, + view.raw().internal, ); let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); @@ -1598,10 +1606,12 @@ mod tests { concat!( "PyBufferView {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "ndim: 1, format: \"B\", shape: Some([5]), ", "strides: Some([1]), suboffsets: None, internal: {:?} }}", ), - view.0.raw.buf, view.0.raw.obj, view.0.raw.internal, + view.0.raw().buf, + view.0.raw().obj, + view.0.raw().internal, ); let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); From 10c82f188a9ffc50ed70b62b2a54024fea02974d Mon Sep 17 00:00:00 2001 From: "Winston H." Date: Sun, 22 Mar 2026 23:49:23 +0800 Subject: [PATCH 04/25] refactor: use `assume_init` Co-authored-by: David Hewitt --- src/buffer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index a515019848f..384a8d13b7a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -857,9 +857,9 @@ impl PyUntypedBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) })?; - // Construct view only after successful GetBuffer, so Drop always + // SAEFTY: Construct view only after successful GetBuffer, so Drop always // runs on an initialized Py_buffer. - let mut view = PyUntypedBufferView { raw }; + let mut view = PyUntypedBufferView { raw: unsafe { raw.assume_init() } }; // When shape is NULL the consumer must assume itemsize == 1. if view.raw().shape.is_null() { From 186a01ebc75ad38915edd07e836aebe2927e57d4 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 23 Mar 2026 00:18:41 +0800 Subject: [PATCH 05/25] refactor: apply some suggestions --- src/buffer.rs | 181 ++++++++++++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 78 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 384a8d13b7a..b31423a2bfa 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -838,15 +838,43 @@ pub struct PyBufferView(PyUntypedBufferView, PhantomData<[T]>); /// arbitrary flags, so flag-dependent accessors like [`format()`](Self::format), /// [`shape()`](Self::shape), and [`strides()`](Self::strides) return `Option`. pub struct PyUntypedBufferView { - raw: mem::MaybeUninit, + raw: ffi::Py_buffer, } impl PyUntypedBufferView { + /// Acquire a buffer view on the stack with [`ffi::PyBUF_SIMPLE`] flags, + /// pass it to `f`, then release the buffer. + /// + /// Format is patched to `"B"` and itemsize to `1`, as required by the + /// buffer protocol for `PyBUF_SIMPLE` requests. + pub fn with( + obj: &Bound<'_, PyAny>, + f: impl FnOnce(&PyUntypedBufferView) -> R, + ) -> PyResult { + let mut raw = mem::MaybeUninit::::uninit(); + + err::error_on_minusone(obj.py(), unsafe { + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), ffi::PyBUF_SIMPLE) + })?; + + // SAFETY: Construct view only after successful GetBuffer, so Drop always + // runs on an initialized Py_buffer. + let mut view = PyUntypedBufferView { + raw: unsafe { raw.assume_init() }, + }; + + // For PyBUF_SIMPLE, the consumer must assume itemsize == 1 and format "B". + view.raw.itemsize = 1; + view.raw.format = ffi::c_str!("B").as_ptr() as *mut _; + + Ok(f(&view)) + } + /// Acquire a buffer view on the stack, pass it to `f`, then release the buffer. /// /// The `flags` parameter controls which buffer fields are requested from the exporter. /// Use constants like [`ffi::PyBUF_SIMPLE`], [`ffi::PyBUF_FULL_RO`], etc. - pub fn with( + pub fn with_flags( obj: &Bound<'_, PyAny>, flags: c_int, f: impl FnOnce(&PyUntypedBufferView) -> R, @@ -857,54 +885,44 @@ impl PyUntypedBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) })?; - // SAEFTY: Construct view only after successful GetBuffer, so Drop always + // SAFETY: Construct view only after successful GetBuffer, so Drop always // runs on an initialized Py_buffer. - let mut view = PyUntypedBufferView { raw: unsafe { raw.assume_init() } }; - - // When shape is NULL the consumer must assume itemsize == 1. - if view.raw().shape.is_null() { - unsafe { &mut *view.raw.as_mut_ptr() }.itemsize = 1; - } + let view = PyUntypedBufferView { + raw: unsafe { raw.assume_init() }, + }; Ok(f(&view)) } - #[inline] - fn raw(&self) -> &ffi::Py_buffer { - // SAFETY: PyUntypedBufferView is only constructed after a successful - // PyObject_GetBuffer call, so raw is always initialized. - unsafe { self.raw.assume_init_ref() } - } - /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { - self.raw().buf + self.raw.buf } /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { - self.raw().readonly != 0 + self.raw.readonly != 0 } /// Gets the size of a single element, in bytes. #[inline] pub fn item_size(&self) -> usize { - self.raw().itemsize as usize + self.raw.itemsize as usize } /// Gets the total number of items. #[inline] pub fn item_count(&self) -> usize { - (self.raw().len as usize) / (self.raw().itemsize as usize) + (self.raw.len as usize) / (self.raw.itemsize as usize) } /// `item_size() * item_count()`. /// For contiguous arrays, this is the length of the underlying memory block. #[inline] pub fn len_bytes(&self) -> usize { - self.raw().len as usize + self.raw.len as usize } /// Gets the number of dimensions. @@ -912,18 +930,21 @@ impl PyUntypedBufferView { /// May be 0 to indicate a single scalar value. #[inline] pub fn dimensions(&self) -> usize { - self.raw().ndim as usize + self.raw.ndim as usize } /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) - /// string describing the contents of a single item. Defaults to `"B"` if NULL. + /// string describing the contents of a single item. + /// + /// Returns `None` if `PyBUF_FORMAT` was not requested and the request was not + /// `PyBUF_SIMPLE` or `PyBUF_WRITABLE`. #[inline] - pub fn format(&self) -> &CStr { - if self.raw().format.is_null() { - ffi::c_str!("B") - } else { - unsafe { CStr::from_ptr(self.raw().format) } + pub fn format(&self) -> Option<&CStr> { + if self.raw.format.is_null() { + return None; } + + Some(unsafe { CStr::from_ptr(self.raw.format) }) } /// Returns the shape array. `shape[i]` is the length of dimension `i`. @@ -931,11 +952,11 @@ impl PyUntypedBufferView { /// Returns `None` if the exporter set `shape` to NULL (e.g. `PyBUF_SIMPLE` was requested). #[inline] pub fn shape(&self) -> Option<&[usize]> { - if self.raw().shape.is_null() { + if self.raw.shape.is_null() { return None; } - Some(unsafe { slice::from_raw_parts(self.raw().shape.cast(), self.raw().ndim as usize) }) + Some(unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) }) } /// Returns the strides array. @@ -943,11 +964,11 @@ impl PyUntypedBufferView { /// Returns `None` if the exporter set `strides` to NULL (e.g. `PyBUF_SIMPLE` was requested). #[inline] pub fn strides(&self) -> Option<&[isize]> { - if self.raw().strides.is_null() { + if self.raw.strides.is_null() { return None; } - Some(unsafe { slice::from_raw_parts(self.raw().strides, self.raw().ndim as usize) }) + Some(unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) }) } /// Returns the suboffsets array. @@ -955,23 +976,23 @@ impl PyUntypedBufferView { /// May return `None` even with `PyBUF_INDIRECT` if the exporter sets `suboffsets` to NULL. #[inline] pub fn suboffsets(&self) -> Option<&[isize]> { - if self.raw().suboffsets.is_null() { - None - } else { - Some(unsafe { slice::from_raw_parts(self.raw().suboffsets, self.raw().ndim as usize) }) + if self.raw.suboffsets.is_null() { + return None; } + + Some(unsafe { slice::from_raw_parts(self.raw.suboffsets, self.raw.ndim as usize) }) } /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'C' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'F' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } } /// Attempt to interpret this untyped view as containing elements of type `T`. @@ -984,12 +1005,18 @@ impl PyUntypedBufferView { } fn ensure_compatible_with(&self) -> PyResult<()> { - if mem::size_of::() != self.item_size() || !T::is_compatible_format(self.format()) { + let format = self.format().ok_or_else(|| { + PyBufferError::new_err( + "buffer format is not available (PyBUF_FORMAT was not requested)", + ) + })?; + + if mem::size_of::() != self.item_size() || !T::is_compatible_format(format) { Err(PyBufferError::new_err(format!( "buffer contents are not compatible with {}", std::any::type_name::() ))) - } else if self.raw().buf.align_offset(mem::align_of::()) != 0 { + } else if self.raw.buf.align_offset(mem::align_of::()) != 0 { Err(PyBufferError::new_err(format!( "buffer contents are insufficiently aligned for {}", std::any::type_name::() @@ -1002,7 +1029,7 @@ impl PyUntypedBufferView { impl Drop for PyUntypedBufferView { fn drop(&mut self) { - unsafe { ffi::PyBuffer_Release(self.raw.as_mut_ptr()) } + unsafe { ffi::PyBuffer_Release(&mut self.raw) } } } @@ -1021,13 +1048,15 @@ impl PyBufferView { /// Acquire a typed buffer view on the stack with user-specified flags. /// - /// The flags must include `PyBUF_FORMAT` for type validation to succeed. + /// `PyBUF_FORMAT` is implicitly added to the flags for type validation. pub fn with_flags( obj: &Bound<'_, PyAny>, flags: c_int, f: impl FnOnce(&PyBufferView) -> R, ) -> PyResult { - PyUntypedBufferView::with(obj, flags, |view| view.as_typed::().map(f))? + PyUntypedBufferView::with_flags(obj, flags | ffi::PyBUF_FORMAT, |view| { + view.as_typed::().map(f) + })? } /// Gets the buffer memory as a slice. @@ -1040,7 +1069,7 @@ impl PyBufferView { if self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.raw().buf.cast(), + self.0.raw.buf.cast(), self.item_count(), )) } @@ -1059,7 +1088,7 @@ impl PyBufferView { if !self.readonly() && self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.raw().buf.cast(), + self.0.raw.buf.cast(), self.item_count(), )) } @@ -1088,19 +1117,18 @@ fn debug_buffer_view( view: &PyUntypedBufferView, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { - let raw = view.raw(); f.debug_struct(name) - .field("buf", &raw.buf) - .field("obj", &raw.obj) - .field("len", &raw.len) - .field("itemsize", &raw.itemsize) - .field("readonly", &raw.readonly) - .field("ndim", &raw.ndim) + .field("buf", &view.raw.buf) + .field("obj", &view.raw.obj) + .field("len", &view.raw.len) + .field("itemsize", &view.raw.itemsize) + .field("readonly", &view.raw.readonly) + .field("ndim", &view.raw.ndim) .field("format", &view.format()) .field("shape", &view.shape()) .field("strides", &view.strides()) .field("suboffsets", &view.suboffsets()) - .field("internal", &raw.internal) + .field("internal", &view.raw.internal) .finish() } @@ -1445,15 +1473,15 @@ mod tests { fn test_untyped_buffer_view_simple() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_SIMPLE, |view| { + PyUntypedBufferView::with(&bytes, |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // PyBUF_SIMPLE doesn't include ND or STRIDES; format defaults to "B" - assert_eq!(view.format().to_str().unwrap(), "B"); + // with() uses PyBUF_SIMPLE and patches format to "B" + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); assert!(view.suboffsets().is_none()); @@ -1466,14 +1494,14 @@ mod tests { fn test_untyped_buffer_view_full() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_FULL_RO, |view| { + PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FULL_RO, |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); assert_eq!(view.strides().unwrap(), [1]); assert!(view.suboffsets().is_none()); @@ -1491,7 +1519,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); assert_eq!(view.shape().unwrap(), [5]); let slice = view.as_slice(py).unwrap(); @@ -1517,7 +1545,7 @@ mod tests { PyBufferView::::with(&array, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 4); - assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.format().unwrap().to_str().unwrap(), "f"); assert_eq!(view.shape().unwrap(), [4]); let slice = view.as_slice(py).unwrap(); @@ -1540,29 +1568,30 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_SIMPLE, |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); + PyUntypedBufferView::with(&bytes, |view| { + assert_eq!(view.item_size(), 1); + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); }) .unwrap(); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_ND, |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); + PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_ND, |view| { + assert!(view.format().is_none()); assert_eq!(view.shape().unwrap(), [5]); assert!(view.strides().is_none()); }) .unwrap(); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_STRIDES, |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); + PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_STRIDES, |view| { + assert!(view.format().is_none()); assert_eq!(view.shape().unwrap(), [5]); assert_eq!(view.strides().unwrap(), [1]); }) .unwrap(); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_FORMAT, |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); + PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FORMAT, |view| { + assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); assert!(view.shape().is_none()); assert!(view.strides().is_none()); }) @@ -1574,7 +1603,7 @@ mod tests { fn test_buffer_view_error() { Python::attach(|py| { let list = crate::types::PyList::empty(py); - let result = PyUntypedBufferView::with(&list, ffi::PyBUF_SIMPLE, |_view| {}); + let result = PyUntypedBufferView::with(&list, |_view| {}); assert!(result.is_err()); }); } @@ -1584,17 +1613,15 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, ffi::PyBUF_FULL_RO, |view| { + PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FULL_RO, |view| { let expected = format!( concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: \"B\", shape: Some([5]), ", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", "strides: Some([1]), suboffsets: None, internal: {:?} }}", ), - view.raw().buf, - view.raw().obj, - view.raw().internal, + view.raw.buf, view.raw.obj, view.raw.internal, ); let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); @@ -1606,12 +1633,10 @@ mod tests { concat!( "PyBufferView {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: \"B\", shape: Some([5]), ", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", "strides: Some([1]), suboffsets: None, internal: {:?} }}", ), - view.0.raw().buf, - view.0.raw().obj, - view.0.raw().internal, + view.0.raw.buf, view.0.raw.obj, view.0.raw.internal, ); let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); From 4fe19635928d032eadb9f7818fd30af626d42371 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 23 Mar 2026 00:46:42 +0800 Subject: [PATCH 06/25] fix: handle `PyBUF_WRITABLE` --- src/buffer.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index b31423a2bfa..dd5704c05d9 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -844,9 +844,6 @@ pub struct PyUntypedBufferView { impl PyUntypedBufferView { /// Acquire a buffer view on the stack with [`ffi::PyBUF_SIMPLE`] flags, /// pass it to `f`, then release the buffer. - /// - /// Format is patched to `"B"` and itemsize to `1`, as required by the - /// buffer protocol for `PyBUF_SIMPLE` requests. pub fn with( obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyUntypedBufferView) -> R, @@ -887,10 +884,16 @@ impl PyUntypedBufferView { // SAFETY: Construct view only after successful GetBuffer, so Drop always // runs on an initialized Py_buffer. - let view = PyUntypedBufferView { + let mut view = PyUntypedBufferView { raw: unsafe { raw.assume_init() }, }; + // For PyBUF_WRITABLE, the consumer must assume itemsize == 1 and format "B". + if flags == ffi::PyBUF_WRITABLE { + view.raw.itemsize = 1; + view.raw.format = ffi::c_str!("B").as_ptr() as *mut _; + } + Ok(f(&view)) } From def7dc70bbff3fc942541faa5f844a0719b3cee5 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 23 Mar 2026 01:57:59 +0800 Subject: [PATCH 07/25] refactor: encode compile-time buffer field availability --- src/buffer.rs | 479 ++++++++++++++++++++++++++------------------------ 1 file changed, 248 insertions(+), 231 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index dd5704c05d9..874091d4435 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -825,78 +825,58 @@ impl_element!(isize, SignedInteger); impl_element!(f32, Float); impl_element!(f64, Float); -/// Stack-allocated typed buffer view. Not constructible directly — use -/// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. -/// -/// This is a lightweight alternative to [`PyBuffer`] that avoids heap allocation -/// by placing the `Py_buffer` on the stack. The scoped closure API ensures the -/// buffer cannot be moved -#[repr(transparent)] -pub struct PyBufferView(PyUntypedBufferView, PhantomData<[T]>); - -/// Unlike [`PyUntypedBuffer`] which always requests `PyBUF_FULL_RO`, this type allows -/// arbitrary flags, so flag-dependent accessors like [`format()`](Self::format), -/// [`shape()`](Self::shape), and [`strides()`](Self::strides) return `Option`. -pub struct PyUntypedBufferView { - raw: ffi::Py_buffer, -} - -impl PyUntypedBufferView { - /// Acquire a buffer view on the stack with [`ffi::PyBUF_SIMPLE`] flags, - /// pass it to `f`, then release the buffer. - pub fn with( - obj: &Bound<'_, PyAny>, - f: impl FnOnce(&PyUntypedBufferView) -> R, - ) -> PyResult { - let mut raw = mem::MaybeUninit::::uninit(); - - err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), ffi::PyBUF_SIMPLE) - })?; +/// Sealed marker for buffer field availability. Either [`Known`] or [`Unknown`]. +mod buffer_info { + /// Whether a buffer field is guaranteed non-null. + pub trait FieldInfo: sealed::Sealed {} - // SAFETY: Construct view only after successful GetBuffer, so Drop always - // runs on an initialized Py_buffer. - let mut view = PyUntypedBufferView { - raw: unsafe { raw.assume_init() }, - }; - - // For PyBUF_SIMPLE, the consumer must assume itemsize == 1 and format "B". - view.raw.itemsize = 1; - view.raw.format = ffi::c_str!("B").as_ptr() as *mut _; - - Ok(f(&view)) + mod sealed { + pub trait Sealed {} + impl Sealed for super::Known {} + impl Sealed for super::Unknown {} } - /// Acquire a buffer view on the stack, pass it to `f`, then release the buffer. - /// - /// The `flags` parameter controls which buffer fields are requested from the exporter. - /// Use constants like [`ffi::PyBUF_SIMPLE`], [`ffi::PyBUF_FULL_RO`], etc. - pub fn with_flags( - obj: &Bound<'_, PyAny>, - flags: c_int, - f: impl FnOnce(&PyUntypedBufferView) -> R, - ) -> PyResult { - let mut raw = mem::MaybeUninit::::uninit(); + /// The field is guaranteed to be non-null. The accessor returns the value directly. + pub struct Known; + /// The field may be null. The accessor is not available. + pub struct Unknown; - err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) - })?; - - // SAFETY: Construct view only after successful GetBuffer, so Drop always - // runs on an initialized Py_buffer. - let mut view = PyUntypedBufferView { - raw: unsafe { raw.assume_init() }, - }; - - // For PyBUF_WRITABLE, the consumer must assume itemsize == 1 and format "B". - if flags == ffi::PyBUF_WRITABLE { - view.raw.itemsize = 1; - view.raw.format = ffi::c_str!("B").as_ptr() as *mut _; - } + impl FieldInfo for Known {} + impl FieldInfo for Unknown {} +} +pub use buffer_info::{FieldInfo, Known, Unknown}; - Ok(f(&view)) - } +/// A typed form of [`PyUntypedBufferView`]. Not constructible directly — use +/// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. +#[repr(transparent)] +pub struct PyBufferView< + T, + Format: FieldInfo = Known, + Shape: FieldInfo = Known, + Stride: FieldInfo = Known, +>(PyUntypedBufferView, PhantomData<[T]>); + +/// Stack-allocated untyped buffer view. +/// +/// Unlike [`PyUntypedBuffer`] which heap-allocates, this places the `Py_buffer` on the +/// stack. The scoped closure API ensures the buffer cannot be moved. +/// +/// [`with()`](Self::with) requests `PyBUF_FULL_RO` and provides [`format()`](Self::format), +/// [`shape()`](Self::shape), and [`strides()`](Self::strides) accessors. +/// [`with_flags()`](Self::with_flags) accepts arbitrary flags but does not provide those +/// accessors. +pub struct PyUntypedBufferView< + Format: FieldInfo = Unknown, + Shape: FieldInfo = Unknown, + Stride: FieldInfo = Unknown, +> { + raw: ffi::Py_buffer, + _marker: PhantomData<(Format, Shape, Stride)>, +} +impl + PyUntypedBufferView +{ /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { @@ -936,44 +916,6 @@ impl PyUntypedBufferView { self.raw.ndim as usize } - /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) - /// string describing the contents of a single item. - /// - /// Returns `None` if `PyBUF_FORMAT` was not requested and the request was not - /// `PyBUF_SIMPLE` or `PyBUF_WRITABLE`. - #[inline] - pub fn format(&self) -> Option<&CStr> { - if self.raw.format.is_null() { - return None; - } - - Some(unsafe { CStr::from_ptr(self.raw.format) }) - } - - /// Returns the shape array. `shape[i]` is the length of dimension `i`. - /// - /// Returns `None` if the exporter set `shape` to NULL (e.g. `PyBUF_SIMPLE` was requested). - #[inline] - pub fn shape(&self) -> Option<&[usize]> { - if self.raw.shape.is_null() { - return None; - } - - Some(unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) }) - } - - /// Returns the strides array. - /// - /// Returns `None` if the exporter set `strides` to NULL (e.g. `PyBUF_SIMPLE` was requested). - #[inline] - pub fn strides(&self) -> Option<&[isize]> { - if self.raw.strides.is_null() { - return None; - } - - Some(unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) }) - } - /// Returns the suboffsets array. /// /// May return `None` even with `PyBUF_INDIRECT` if the exporter sets `suboffsets` to NULL. @@ -997,71 +939,202 @@ impl PyUntypedBufferView { pub fn is_fortran_contiguous(&self) -> bool { unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } } +} + +impl PyUntypedBufferView { + /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) + /// string describing the contents of a single item. + #[inline] + pub fn format(&self) -> &CStr { + debug_assert!(!self.raw.format.is_null()); + unsafe { CStr::from_ptr(self.raw.format) } + } /// Attempt to interpret this untyped view as containing elements of type `T`. - /// - /// Requires that `PyBUF_FORMAT` was included in the flags. - pub fn as_typed(&self) -> PyResult<&PyBufferView> { + pub fn as_typed(&self) -> PyResult<&PyBufferView> { self.ensure_compatible_with::()?; - // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView - Ok(unsafe { NonNull::from(self).cast::>().as_ref() }) + // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView + Ok(unsafe { + NonNull::from(self) + .cast::>() + .as_ref() + }) } fn ensure_compatible_with(&self) -> PyResult<()> { - let format = self.format().ok_or_else(|| { - PyBufferError::new_err( - "buffer format is not available (PyBUF_FORMAT was not requested)", - ) - })?; + let name = std::any::type_name::(); - if mem::size_of::() != self.item_size() || !T::is_compatible_format(format) { - Err(PyBufferError::new_err(format!( - "buffer contents are not compatible with {}", - std::any::type_name::() - ))) - } else if self.raw.buf.align_offset(mem::align_of::()) != 0 { - Err(PyBufferError::new_err(format!( - "buffer contents are insufficiently aligned for {}", - std::any::type_name::() - ))) - } else { - Ok(()) + if mem::size_of::() != self.item_size() || !T::is_compatible_format(self.format()) { + return Err(PyBufferError::new_err(format!( + "buffer contents are not compatible with {name}" + ))); + } + if self.raw.buf.align_offset(mem::align_of::()) != 0 { + return Err(PyBufferError::new_err(format!( + "buffer contents are insufficiently aligned for {name}" + ))); } + + Ok(()) + } +} + +impl PyUntypedBufferView { + /// Returns the shape array. `shape[i]` is the length of dimension `i`. + /// + /// Despite Python using an array of signed integers, the values are guaranteed to be + /// non-negative. However, dimensions of length 0 are possible and might need special + /// attention. + #[inline] + pub fn shape(&self) -> &[usize] { + debug_assert!(!self.raw.shape.is_null()); + unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) } + } +} + +impl PyUntypedBufferView { + /// Returns the strides array. + /// + /// Stride values can be any integer. For regular arrays, strides are usually positive, + /// but a consumer MUST be able to handle the case `strides[n] <= 0`. + #[inline] + pub fn strides(&self) -> &[isize] { + debug_assert!(!self.raw.strides.is_null()); + unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) } + } +} + +impl PyUntypedBufferView { + /// Acquire a buffer view with [`ffi::PyBUF_FULL_RO`] flags, + /// pass it to `f`, then release the buffer. + pub fn with( + obj: &Bound<'_, PyAny>, + f: impl FnOnce(&PyUntypedBufferView) -> R, + ) -> PyResult { + let mut raw = mem::MaybeUninit::::uninit(); + + err::error_on_minusone(obj.py(), unsafe { + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), ffi::PyBUF_FULL_RO) + })?; + + let view = PyUntypedBufferView { + raw: unsafe { raw.assume_init() }, + _marker: PhantomData, + }; + + Ok(f(&view)) + } +} + +impl PyUntypedBufferView { + /// Acquire a buffer view with arbitrary flags, + /// pass it to `f`, then release the buffer. + /// + /// The `flags` parameter controls which buffer fields are requested from the exporter. + /// Use constants like [`ffi::PyBUF_SIMPLE`], [`ffi::PyBUF_ND`], [`ffi::PyBUF_STRIDES`], etc. + pub fn with_flags( + obj: &Bound<'_, PyAny>, + flags: c_int, + f: impl FnOnce(&PyUntypedBufferView) -> R, + ) -> PyResult { + let mut raw = mem::MaybeUninit::::uninit(); + + err::error_on_minusone(obj.py(), unsafe { + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) + })?; + + let view = PyUntypedBufferView { + raw: unsafe { raw.assume_init() }, + _marker: PhantomData, + }; + + Ok(f(&view)) } } -impl Drop for PyUntypedBufferView { +fn debug_buffer_view( + name: &str, + raw: &ffi::Py_buffer, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + let ndim = raw.ndim as usize; + let format = NonNull::new(raw.format).map(|p| unsafe { CStr::from_ptr(p.as_ptr()) }); + let shape = NonNull::new(raw.shape) + .map(|p| unsafe { slice::from_raw_parts(p.as_ptr().cast::(), ndim) }); + let strides = + NonNull::new(raw.strides).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); + let suboffsets = + NonNull::new(raw.suboffsets).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); + + f.debug_struct(name) + .field("buf", &raw.buf) + .field("obj", &raw.obj) + .field("len", &raw.len) + .field("itemsize", &raw.itemsize) + .field("readonly", &raw.readonly) + .field("ndim", &raw.ndim) + .field("format", &format) + .field("shape", &shape) + .field("strides", &strides) + .field("suboffsets", &suboffsets) + .field("internal", &raw.internal) + .finish() +} + +impl Drop + for PyUntypedBufferView +{ fn drop(&mut self) { unsafe { ffi::PyBuffer_Release(&mut self.raw) } } } -impl Debug for PyUntypedBufferView { +impl Debug + for PyUntypedBufferView +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - debug_buffer_view("PyUntypedBufferView", self, f) + debug_buffer_view("PyUntypedBufferView", &self.raw, f) } } -impl PyBufferView { - /// Acquire a typed buffer view on the stack with `PyBUF_FULL_RO` flags, +impl PyBufferView { + /// Acquire a typed buffer view with `PyBUF_FULL_RO` flags, /// validating that the buffer format is compatible with `T`. - pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { - Self::with_flags(obj, ffi::PyBUF_FULL_RO, f) + pub fn with( + obj: &Bound<'_, PyAny>, + f: impl FnOnce(&PyBufferView) -> R, + ) -> PyResult { + PyUntypedBufferView::with(obj, |view| view.as_typed::().map(f))? } +} - /// Acquire a typed buffer view on the stack with user-specified flags. +impl PyBufferView { + /// Acquire a typed buffer view with user-specified flags. /// /// `PyBUF_FORMAT` is implicitly added to the flags for type validation. pub fn with_flags( obj: &Bound<'_, PyAny>, flags: c_int, - f: impl FnOnce(&PyBufferView) -> R, + f: impl FnOnce(&PyBufferView) -> R, ) -> PyResult { - PyUntypedBufferView::with_flags(obj, flags | ffi::PyBUF_FORMAT, |view| { - view.as_typed::().map(f) - })? + let mut raw = mem::MaybeUninit::::uninit(); + + err::error_on_minusone(obj.py(), unsafe { + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags | ffi::PyBUF_FORMAT) + })?; + + let view = PyUntypedBufferView:: { + raw: unsafe { raw.assume_init() }, + _marker: PhantomData, + }; + + view.as_typed::().map(f) } +} +impl + PyBufferView +{ /// Gets the buffer memory as a slice. /// /// Returns `None` if the buffer is not C-contiguous. @@ -1069,16 +1142,11 @@ impl PyBufferView { /// The returned slice uses type [`ReadOnlyCell`] because it's theoretically possible /// for any call into the Python runtime to modify the values in the slice. pub fn as_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [ReadOnlyCell]> { - if self.is_c_contiguous() { - unsafe { - Some(slice::from_raw_parts( - self.0.raw.buf.cast(), - self.item_count(), - )) - } - } else { - None + if !self.is_c_contiguous() { + return None; } + + Some(unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) }) } /// Gets the buffer memory as a mutable slice. @@ -1088,53 +1156,32 @@ impl PyBufferView { /// The returned slice uses type [`Cell`](cell::Cell) because it's theoretically possible /// for any call into the Python runtime to modify the values in the slice. pub fn as_mut_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [cell::Cell]> { - if !self.readonly() && self.is_c_contiguous() { - unsafe { - Some(slice::from_raw_parts( - self.0.raw.buf.cast(), - self.item_count(), - )) - } - } else { - None + if self.readonly() || !self.is_c_contiguous() { + return None; } + + Some(unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) }) } } -impl std::ops::Deref for PyBufferView { - type Target = PyUntypedBufferView; +impl std::ops::Deref + for PyBufferView +{ + type Target = PyUntypedBufferView; fn deref(&self) -> &Self::Target { &self.0 } } -impl Debug for PyBufferView { +impl Debug + for PyBufferView +{ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - debug_buffer_view("PyBufferView", &self.0, f) + debug_buffer_view("PyBufferView", &self.0.raw, f) } } -fn debug_buffer_view( - name: &str, - view: &PyUntypedBufferView, - f: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result { - f.debug_struct(name) - .field("buf", &view.raw.buf) - .field("obj", &view.raw.obj) - .field("len", &view.raw.len) - .field("itemsize", &view.raw.itemsize) - .field("readonly", &view.raw.readonly) - .field("ndim", &view.raw.ndim) - .field("format", &view.format()) - .field("shape", &view.shape()) - .field("strides", &view.strides()) - .field("suboffsets", &view.suboffsets()) - .field("internal", &view.raw.internal) - .finish() -} - #[cfg(test)] mod tests { use super::*; @@ -1473,7 +1520,7 @@ mod tests { } #[test] - fn test_untyped_buffer_view_simple() { + fn test_untyped_buffer_view() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); PyUntypedBufferView::with(&bytes, |view| { @@ -1483,30 +1530,10 @@ mod tests { assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // with() uses PyBUF_SIMPLE and patches format to "B" - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); - assert!(view.shape().is_none()); - assert!(view.strides().is_none()); - assert!(view.suboffsets().is_none()); - }) - .unwrap(); - }); - } - - #[test] - fn test_untyped_buffer_view_full() { - Python::attach(|py| { - let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FULL_RO, |view| { - assert!(!view.buf_ptr().is_null()); - assert_eq!(view.len_bytes(), 5); - assert_eq!(view.item_size(), 1); - assert_eq!(view.item_count(), 5); - assert!(view.readonly()); - assert_eq!(view.dimensions(), 1); - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); - assert_eq!(view.shape().unwrap(), [5]); - assert_eq!(view.strides().unwrap(), [1]); + // with() uses PyBUF_FULL_RO — all Known, direct return types + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); assert!(view.suboffsets().is_none()); assert!(view.is_c_contiguous()); assert!(view.is_fortran_contiguous()); @@ -1522,8 +1549,9 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); - assert_eq!(view.shape().unwrap(), [5]); + // PyBufferView::with uses PyBUF_FULL_RO — all Known + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); let slice = view.as_slice(py).unwrap(); assert_eq!(slice.len(), 5); @@ -1548,8 +1576,8 @@ mod tests { PyBufferView::::with(&array, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 4); - assert_eq!(view.format().unwrap().to_str().unwrap(), "f"); - assert_eq!(view.shape().unwrap(), [4]); + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); let slice = view.as_slice(py).unwrap(); assert_eq!(slice.len(), 4); @@ -1567,36 +1595,22 @@ mod tests { } #[test] - fn test_buffer_view_option_accessors() { + fn test_buffer_view_with_flags() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, |view| { - assert_eq!(view.item_size(), 1); - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); - assert!(view.shape().is_none()); - assert!(view.strides().is_none()); - }) - .unwrap(); - + // with_flags gives all-Unknown — only always-available methods PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_ND, |view| { - assert!(view.format().is_none()); - assert_eq!(view.shape().unwrap(), [5]); - assert!(view.strides().is_none()); - }) - .unwrap(); - - PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_STRIDES, |view| { - assert!(view.format().is_none()); - assert_eq!(view.shape().unwrap(), [5]); - assert_eq!(view.strides().unwrap(), [1]); + assert_eq!(view.item_count(), 5); + assert_eq!(view.len_bytes(), 5); + assert!(view.readonly()); + assert!(view.suboffsets().is_none()); }) .unwrap(); PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FORMAT, |view| { - assert_eq!(view.format().unwrap().to_str().unwrap(), "B"); - assert!(view.shape().is_none()); - assert!(view.strides().is_none()); + assert_eq!(view.item_count(), 5); + assert!(view.suboffsets().is_none()); }) .unwrap(); }); @@ -1616,6 +1630,7 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); + // Debug always uses raw_format/raw_shape/raw_strides (Option in output) PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FULL_RO, |view| { let expected = format!( concat!( @@ -1626,6 +1641,7 @@ mod tests { ), view.raw.buf, view.raw.obj, view.raw.internal, ); + let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); }) @@ -1641,6 +1657,7 @@ mod tests { ), view.0.raw.buf, view.0.raw.obj, view.0.raw.internal, ); + let debug_repr = format!("{:?}", view); assert_eq!(debug_repr, expected); }) From bb626150c264b7ba5f001b926ae4229e36bf9aeb Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 23 Mar 2026 02:16:08 +0800 Subject: [PATCH 08/25] style: clean up --- src/buffer.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 874091d4435..a91ffc5bfd3 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -953,12 +953,14 @@ impl PyUntypedBufferView(&self) -> PyResult<&PyBufferView> { self.ensure_compatible_with::()?; - // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView - Ok(unsafe { + // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> + let typed = unsafe { NonNull::from(self) .cast::>() .as_ref() - }) + }; + + Ok(typed) } fn ensure_compatible_with(&self) -> PyResult<()> { @@ -969,6 +971,7 @@ impl PyUntypedBufferView()) != 0 { return Err(PyBufferError::new_err(format!( "buffer contents are insufficiently aligned for {name}" From 9acd5339e8973cfdf2bc7b34ddcd32fccbbc62d9 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Wed, 8 Apr 2026 02:07:29 +0800 Subject: [PATCH 09/25] tests: extend coverage --- src/buffer.rs | 143 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 18 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index a91ffc5bfd3..a6febe4b382 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1347,6 +1347,27 @@ mod tests { (c"=d", Float { bytes: 8 }), (c"=z", Unknown), (c"=0", Unknown), + // bare char (no prefix) goes to native_element_type_from_type_char + ( + c"b", + SignedInteger { + bytes: size_of::(), + }, + ), + ( + c"B", + UnsignedInteger { + bytes: size_of::(), + }, + ), + (c"?", Bool), + (c"f", Float { bytes: 4 }), + (c"d", Float { bytes: 8 }), + (c"z", Unknown), + // <, >, ! prefixes go to standard_element_type_from_type_char + (c"H", UnsignedInteger { bytes: 2 }), + (c"!q", SignedInteger { bytes: 8 }), // unknown prefix -> Unknown (c":b", Unknown), ] { @@ -1384,6 +1405,7 @@ mod tests { assert_eq!(slice.len(), 5); assert_eq!(slice[0].get(), b'a'); assert_eq!(slice[2].get(), b'c'); + assert_eq!(unsafe { *slice[0].as_ptr() }, b'a'); assert_eq!(unsafe { *(buffer.get_ptr(&[1]).cast::()) }, b'b'); @@ -1457,24 +1479,6 @@ mod tests { }); } - #[test] - fn test_untyped_buffer() { - Python::attach(|py| { - let bytes = PyBytes::new(py, b"abcde"); - let untyped = PyUntypedBuffer::get(&bytes).unwrap(); - assert_eq!(untyped.dimensions(), 1); - assert_eq!(untyped.item_count(), 5); - assert_eq!(untyped.format().to_str().unwrap(), "B"); - assert_eq!(untyped.shape(), [5]); - - let typed: &PyBuffer = untyped.as_typed().unwrap(); - assert_eq!(typed.dimensions(), 1); - assert_eq!(typed.item_count(), 5); - assert_eq!(typed.format().to_str().unwrap(), "B"); - assert_eq!(typed.shape(), [5]); - }); - } - #[test] fn test_obj_getter() { Python::attach(|py| { @@ -1522,6 +1526,67 @@ mod tests { }); } + #[test] + fn test_copy_to_fortran_slice() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) + .unwrap(); + let buffer = PyBuffer::get(&array).unwrap(); + + // wrong length + assert!(buffer.copy_to_fortran_slice(py, &mut [0.0f32]).is_err()); + // correct length + let mut arr = [0.0f32; 4]; + buffer.copy_to_fortran_slice(py, &mut arr).unwrap(); + assert_eq!(arr, [1.0, 1.5, 2.0, 2.5]); + }); + } + + #[test] + fn test_copy_from_slice_wrong_length() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) + .unwrap(); + let buffer = PyBuffer::get(&array).unwrap(); + // writable buffer, but wrong length + assert!(!buffer.readonly()); + assert!(buffer.copy_from_slice(py, &[0.0f32; 2]).is_err()); + assert!(buffer.copy_from_fortran_slice(py, &[0.0f32; 2]).is_err()); + }); + } + + #[test] + fn test_untyped_buffer() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + let buffer = PyUntypedBuffer::get(&bytes).unwrap(); + assert_eq!(buffer.dimensions(), 1); + assert_eq!(buffer.item_count(), 5); + assert_eq!(buffer.format().to_str().unwrap(), "B"); + assert_eq!(buffer.shape(), [5]); + assert!(!buffer.buf_ptr().is_null()); + assert_eq!(buffer.strides(), &[1]); + assert_eq!(buffer.len_bytes(), 5); + assert_eq!(buffer.item_size(), 1); + assert!(buffer.readonly()); + assert!(buffer.suboffsets().is_none()); + + assert!(format!("{:?}", buffer).starts_with("PyUntypedBuffer { buf: ")); + + let typed: &PyBuffer = buffer.as_typed().unwrap(); + assert_eq!(typed.dimensions(), 1); + assert_eq!(typed.item_count(), 5); + assert_eq!(typed.format().to_str().unwrap(), "B"); + assert_eq!(typed.shape(), [5]); + }); + } + #[test] fn test_untyped_buffer_view() { Python::attach(|py| { @@ -1619,6 +1684,48 @@ mod tests { }); } + #[test] + fn test_typed_buffer_view_with_flags() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) + .unwrap(); + + PyBufferView::::with_flags( + &array, + ffi::PyBUF_ND, + |view| { + assert_eq!(view.item_count(), 4); + assert_eq!(view.format().to_str().unwrap(), "f"); + + let slice = view.as_slice(py).unwrap(); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[3].get(), 2.5); + + let mut_slice = view.as_mut_slice(py).unwrap(); + mut_slice[0].set(9.0); + assert_eq!(slice[0].get(), 9.0); + }, + ) + .unwrap(); + }); + } + + #[test] + fn test_typed_buffer_view_with_flags_incompatible() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + let result = PyBufferView::::with_flags( + &bytes, + ffi::PyBUF_ND, + |_view| {}, + ); + assert!(result.is_err()); + }); + } + #[test] fn test_buffer_view_error() { Python::attach(|py| { From 0697c25cdcf8de16f5deba160bf44b630a663135 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Wed, 8 Apr 2026 02:11:35 +0800 Subject: [PATCH 10/25] chore: add `obj` API --- src/buffer.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/buffer.rs b/src/buffer.rs index a6febe4b382..5185ca4ca86 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -883,6 +883,12 @@ impl self.raw.buf } + /// Returns the Python object that owns the buffer data. + #[inline] + pub fn obj<'py>(&self, py: Python<'py>) -> Option<&Bound<'py, PyAny>> { + unsafe { Bound::ref_from_ptr_or_opt(py, &self.raw.obj).as_ref() } + } + /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { @@ -1605,6 +1611,7 @@ mod tests { assert!(view.suboffsets().is_none()); assert!(view.is_c_contiguous()); assert!(view.is_fortran_contiguous()); + assert!(view.obj(py).unwrap().is(&bytes)); }) .unwrap(); }); From eabd5c002df0ef7ca9c7ecce3ff7c4349b701bf2 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Wed, 8 Apr 2026 04:27:23 +0800 Subject: [PATCH 11/25] refactor: use `PyBufferFlags` --- src/buffer.rs | 459 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 313 insertions(+), 146 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 5185ca4ca86..ef852dbbf50 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -825,58 +825,130 @@ impl_element!(isize, SignedInteger); impl_element!(f32, Float); impl_element!(f64, Float); -/// Sealed marker for buffer field availability. Either [`Known`] or [`Unknown`]. -mod buffer_info { - /// Whether a buffer field is guaranteed non-null. - pub trait FieldInfo: sealed::Sealed {} +/// Type-safe buffer request flags. The const parameters encode which fields +/// the exporter is required to fill. +pub struct PyBufferFlags< + const FORMAT: bool = false, + const SHAPE: bool = false, + const STRIDE: bool = false, + const WRITABLE: bool = false, + const C_CONTIGUOUS: bool = false, + const F_CONTIGUOUS: bool = false, +>(c_int); + +mod py_buffer_flags_sealed { + pub trait Sealed {} + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > Sealed for super::PyBufferFlags + { + } +} - mod sealed { - pub trait Sealed {} - impl Sealed for super::Known {} - impl Sealed for super::Unknown {} +/// Trait implemented by all [`PyBufferFlags`] instantiations. +pub trait PyBufferFlagsType: py_buffer_flags_sealed::Sealed {} + +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlagsType for PyBufferFlags +{ +} + +// PyBufferFlags::FORMAT | +impl + std::ops::BitOr> + for PyBufferFlags +{ + type Output = PyBufferFlags; + fn bitor(self, rhs: PyBufferFlags) -> Self::Output { + PyBufferFlags(self.0 | rhs.0) } +} - /// The field is guaranteed to be non-null. The accessor returns the value directly. - pub struct Known; - /// The field may be null. The accessor is not available. - pub struct Unknown; +// | PyBufferFlags::FORMAT +impl + std::ops::BitOr> + for PyBufferFlags +{ + type Output = PyBufferFlags; + fn bitor(self, rhs: PyBufferFlags) -> Self::Output { + PyBufferFlags(self.0 | rhs.0) + } +} - impl FieldInfo for Known {} - impl FieldInfo for Unknown {} +#[allow(non_upper_case_globals)] +impl PyBufferFlags { + /// Request a simple buffer with no shape, strides, or format information. + pub const SIMPLE: PyBufferFlags = PyBufferFlags(ffi::PyBUF_SIMPLE); + /// Request format information only. + pub const FORMAT: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FORMAT); + /// Request shape information. + pub const ND: PyBufferFlags = PyBufferFlags(ffi::PyBUF_ND); + /// Request shape and strides. + pub const STRIDES: PyBufferFlags = PyBufferFlags(ffi::PyBUF_STRIDES); + /// Request C-contiguous buffer with shape and strides. + pub const C_CONTIGUOUS: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_C_CONTIGUOUS); + /// Request Fortran-contiguous buffer with shape and strides. + pub const F_CONTIGUOUS: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_F_CONTIGUOUS); + /// Request contiguous buffer (C or Fortran) with shape and strides. + pub const ANY_CONTIGUOUS: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_ANY_CONTIGUOUS); + /// Request shape, strides, and suboffsets. + pub const INDIRECT: PyBufferFlags = PyBufferFlags(ffi::PyBUF_INDIRECT); + /// Request writable buffer with shape. + pub const CONTIG: PyBufferFlags = PyBufferFlags(ffi::PyBUF_CONTIG); + /// Request shape (read-only, equivalent to [`Self::ND`]). + pub const CONTIG_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_CONTIG_RO); + /// Request writable buffer with shape and strides. + pub const STRIDED: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_STRIDED); + /// Request shape and strides (read-only, equivalent to [`Self::STRIDES`]). + pub const STRIDED_RO: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_STRIDED_RO); + /// Request writable buffer with shape, strides, and format. + pub const RECORDS: PyBufferFlags = + PyBufferFlags(ffi::PyBUF_RECORDS); + /// Request shape, strides, and format. + pub const RECORDS_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_RECORDS_RO); + /// Request writable buffer with all information including suboffsets. + pub const FULL: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FULL); + /// Request all buffer information including suboffsets. + pub const FULL_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FULL_RO); } -pub use buffer_info::{FieldInfo, Known, Unknown}; /// A typed form of [`PyUntypedBufferView`]. Not constructible directly — use /// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. #[repr(transparent)] -pub struct PyBufferView< - T, - Format: FieldInfo = Known, - Shape: FieldInfo = Known, - Stride: FieldInfo = Known, ->(PyUntypedBufferView, PhantomData<[T]>); +pub struct PyBufferView>( + PyUntypedBufferView, + PhantomData<[T]>, +); /// Stack-allocated untyped buffer view. /// /// Unlike [`PyUntypedBuffer`] which heap-allocates, this places the `Py_buffer` on the /// stack. The scoped closure API ensures the buffer cannot be moved. /// -/// [`with()`](Self::with) requests `PyBUF_FULL_RO` and provides [`format()`](Self::format), -/// [`shape()`](Self::shape), and [`strides()`](Self::strides) accessors. -/// [`with_flags()`](Self::with_flags) accepts arbitrary flags but does not provide those -/// accessors. -pub struct PyUntypedBufferView< - Format: FieldInfo = Unknown, - Shape: FieldInfo = Unknown, - Stride: FieldInfo = Unknown, -> { +/// Use [`with_flags()`](Self::with_flags) with a [`PyBufferFlags`] constant to acquire a view. +/// The available accessors depend on the flags used. +pub struct PyUntypedBufferView { raw: ffi::Py_buffer, - _marker: PhantomData<(Format, Shape, Stride)>, + _flags: PhantomData, } -impl - PyUntypedBufferView -{ +impl PyUntypedBufferView { /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { @@ -947,7 +1019,9 @@ impl } } -impl PyUntypedBufferView { +impl + PyUntypedBufferView> +{ /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. #[inline] @@ -957,38 +1031,26 @@ impl PyUntypedBufferView(&self) -> PyResult<&PyBufferView> { + pub fn as_typed( + &self, + ) -> PyResult<&PyBufferView>> { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> - let typed = unsafe { + Ok(unsafe { NonNull::from(self) - .cast::>() + .cast::>>() .as_ref() - }; - - Ok(typed) + }) } fn ensure_compatible_with(&self) -> PyResult<()> { - let name = std::any::type_name::(); - - if mem::size_of::() != self.item_size() || !T::is_compatible_format(self.format()) { - return Err(PyBufferError::new_err(format!( - "buffer contents are not compatible with {name}" - ))); - } - - if self.raw.buf.align_offset(mem::align_of::()) != 0 { - return Err(PyBufferError::new_err(format!( - "buffer contents are insufficiently aligned for {name}" - ))); - } - - Ok(()) + check_buffer_compatibility::(self.raw.buf, self.item_size(), self.format()) } } -impl PyUntypedBufferView { +impl + PyUntypedBufferView> +{ /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// /// Despite Python using an array of signed integers, the values are guaranteed to be @@ -1001,7 +1063,9 @@ impl PyUntypedBufferView PyUntypedBufferView { +impl + PyUntypedBufferView> +{ /// Returns the strides array. /// /// Stride values can be any integer. For regular arrays, strides are usually positive, @@ -1013,48 +1077,59 @@ impl PyUntypedBufferView { - /// Acquire a buffer view with [`ffi::PyBUF_FULL_RO`] flags, - /// pass it to `f`, then release the buffer. - pub fn with( - obj: &Bound<'_, PyAny>, - f: impl FnOnce(&PyUntypedBufferView) -> R, - ) -> PyResult { - let mut raw = mem::MaybeUninit::::uninit(); - - err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), ffi::PyBUF_FULL_RO) - })?; +/// Check that a buffer is compatible with element type `T`. +fn check_buffer_compatibility( + buf: *mut c_void, + itemsize: usize, + format: &CStr, +) -> PyResult<()> { + let name = std::any::type_name::(); - let view = PyUntypedBufferView { - raw: unsafe { raw.assume_init() }, - _marker: PhantomData, - }; + if mem::size_of::() != itemsize || !T::is_compatible_format(format) { + return Err(PyBufferError::new_err(format!( + "buffer contents are not compatible with {name}" + ))); + } - Ok(f(&view)) + if buf.align_offset(mem::align_of::()) != 0 { + return Err(PyBufferError::new_err(format!( + "buffer contents are insufficiently aligned for {name}" + ))); } + + Ok(()) } -impl PyUntypedBufferView { - /// Acquire a buffer view with arbitrary flags, +impl PyUntypedBufferView { + /// Acquire a buffer view with the given flags, /// pass it to `f`, then release the buffer. /// - /// The `flags` parameter controls which buffer fields are requested from the exporter. - /// Use constants like [`ffi::PyBUF_SIMPLE`], [`ffi::PyBUF_ND`], [`ffi::PyBUF_STRIDES`], etc. - pub fn with_flags( + /// Use predefined flag constants like [`PyBufferFlags::SIMPLE`], [`PyBufferFlags::ND`], + /// [`PyBufferFlags::STRIDES`], [`PyBufferFlags::FULL_RO`], etc. + pub fn with_flags< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CCONTIGUOUS: bool, + const FCONTIGUOUS: bool, + R, + >( obj: &Bound<'_, PyAny>, - flags: c_int, - f: impl FnOnce(&PyUntypedBufferView) -> R, + flags: PyBufferFlags, + f: impl FnOnce( + &PyUntypedBufferView>, + ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags) + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0) })?; let view = PyUntypedBufferView { raw: unsafe { raw.assume_init() }, - _marker: PhantomData, + _flags: PhantomData, }; Ok(f(&view)) @@ -1090,60 +1165,65 @@ fn debug_buffer_view( .finish() } -impl Drop - for PyUntypedBufferView -{ +impl Drop for PyUntypedBufferView { fn drop(&mut self) { unsafe { ffi::PyBuffer_Release(&mut self.raw) } } } -impl Debug - for PyUntypedBufferView -{ +impl Debug for PyUntypedBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { debug_buffer_view("PyUntypedBufferView", &self.raw, f) } } -impl PyBufferView { - /// Acquire a typed buffer view with `PyBUF_FULL_RO` flags, +impl PyBufferView { + /// Acquire a typed buffer view with `PyBufferFlags::FULL_RO` flags, /// validating that the buffer format is compatible with `T`. - pub fn with( - obj: &Bound<'_, PyAny>, - f: impl FnOnce(&PyBufferView) -> R, - ) -> PyResult { - PyUntypedBufferView::with(obj, |view| view.as_typed::().map(f))? + pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { + PyUntypedBufferView::with_flags(obj, PyBufferFlags::FULL_RO, |view| { + view.as_typed::().map(f) + })? } -} -impl PyBufferView { - /// Acquire a typed buffer view with user-specified flags. + /// Acquire a typed buffer view with the given flags. /// - /// `PyBUF_FORMAT` is implicitly added to the flags for type validation. - pub fn with_flags( + /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. + pub fn with_flags< + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CCONTIGUOUS: bool, + const FCONTIGUOUS: bool, + R, + >( obj: &Bound<'_, PyAny>, - flags: c_int, - f: impl FnOnce(&PyBufferView) -> R, + flags: PyBufferFlags, + f: impl FnOnce( + &PyBufferView>, + ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags | ffi::PyBUF_FORMAT) + ffi::PyObject_GetBuffer( + obj.as_ptr(), + raw.as_mut_ptr(), + flags.0 | ffi::PyBUF_FORMAT, + ) })?; - let view = PyUntypedBufferView:: { - raw: unsafe { raw.assume_init() }, - _marker: PhantomData, - }; + let view = + PyUntypedBufferView::> { + raw: unsafe { raw.assume_init() }, + _flags: PhantomData, + }; view.as_typed::().map(f) } } -impl - PyBufferView -{ +impl PyBufferView { /// Gets the buffer memory as a slice. /// /// Returns `None` if the buffer is not C-contiguous. @@ -1173,19 +1253,72 @@ impl } } -impl std::ops::Deref - for PyBufferView +// C-contiguous guaranteed — no contiguity check needed. +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const FCONTIGUOUS: bool, + > PyBufferView> { - type Target = PyUntypedBufferView; + /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. + pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { + unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + } +} + +// C-contiguous + writable guaranteed — no checks needed. +impl + PyBufferView> +{ + /// Gets the buffer memory as a mutable slice. + /// The buffer is guaranteed C-contiguous and writable. + pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] { + unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + } +} + +// Fortran-contiguous guaranteed — no contiguity check needed. +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CCONTIGUOUS: bool, + > PyBufferView> +{ + /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. + pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { + unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + } +} + +// Fortran-contiguous + writable guaranteed — no checks needed. +impl + PyBufferView> +{ + /// Gets the buffer memory as a mutable slice. + /// The buffer is guaranteed Fortran-contiguous and writable. + pub fn as_fortran_contiguous_mut_slice<'a>( + &'a self, + _py: Python<'a>, + ) -> &'a [cell::Cell] { + unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + } +} + +impl std::ops::Deref for PyBufferView { + type Target = PyUntypedBufferView; fn deref(&self) -> &Self::Target { &self.0 } } -impl Debug - for PyBufferView -{ +impl Debug for PyBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { debug_buffer_view("PyBufferView", &self.0.raw, f) } @@ -1597,14 +1730,14 @@ mod tests { fn test_untyped_buffer_view() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with(&bytes, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FULL_RO, |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // with() uses PyBUF_FULL_RO — all Known, direct return types + // with() uses PyBufferFlags::FULL_RO — all Known, direct return types assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -1624,7 +1757,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - // PyBufferView::with uses PyBUF_FULL_RO — all Known + // PyBufferView::with uses PyBufferFlags::FULL_RO — all Known assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -1674,8 +1807,7 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - // with_flags gives all-Unknown — only always-available methods - PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_ND, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::SIMPLE, |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.len_bytes(), 5); assert!(view.readonly()); @@ -1683,9 +1815,21 @@ mod tests { }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FORMAT, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::ND, |view| { assert_eq!(view.item_count(), 5); - assert!(view.suboffsets().is_none()); + assert_eq!(view.shape(), [5]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::STRIDES, |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FORMAT, |view| { + assert_eq!(view.item_count(), 5); + assert_eq!(view.format().to_str().unwrap(), "B"); }) .unwrap(); }); @@ -1700,22 +1844,19 @@ mod tests { .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); - PyBufferView::::with_flags( - &array, - ffi::PyBUF_ND, - |view| { - assert_eq!(view.item_count(), 4); - assert_eq!(view.format().to_str().unwrap(), "f"); + PyBufferView::::with_flags(&array, PyBufferFlags::ND, |view| { + assert_eq!(view.item_count(), 4); + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); - let slice = view.as_slice(py).unwrap(); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[3].get(), 2.5); + let slice = view.as_slice(py).unwrap(); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[3].get(), 2.5); - let mut_slice = view.as_mut_slice(py).unwrap(); - mut_slice[0].set(9.0); - assert_eq!(slice[0].get(), 9.0); - }, - ) + let mut_slice = view.as_mut_slice(py).unwrap(); + mut_slice[0].set(9.0); + assert_eq!(slice[0].get(), 9.0); + }) .unwrap(); }); } @@ -1724,20 +1865,46 @@ mod tests { fn test_typed_buffer_view_with_flags_incompatible() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - let result = PyBufferView::::with_flags( - &bytes, - ffi::PyBUF_ND, - |_view| {}, - ); + let result = PyBufferView::::with_flags(&bytes, PyBufferFlags::ND, |_view| {}); assert!(result.is_err()); }); } + #[test] + fn test_c_contiguous_slice() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0)), None) + .unwrap(); + + // C_CONTIGUOUS: guaranteed contiguous readonly access (no Option) + PyBufferView::::with_flags(&array, PyBufferFlags::C_CONTIGUOUS, |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice.len(), 3); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[2].get(), 2.0); + }) + .unwrap(); + + // C_CONTIGUOUS | WRITABLE (via CONTIG combined with STRIDES-level): + // no predefined constant, but we can use PyBufferView::with on a writable array + // and the Option-based as_mut_slice still works + PyBufferView::::with(&array, |view| { + let mut_slice = view.as_mut_slice(py).unwrap(); + mut_slice[2].set(9.0); + assert_eq!(view.as_slice(py).unwrap()[2].get(), 9.0); + }) + .unwrap(); + }); + } + #[test] fn test_buffer_view_error() { Python::attach(|py| { let list = crate::types::PyList::empty(py); - let result = PyUntypedBufferView::with(&list, |_view| {}); + let result = PyUntypedBufferView::with_flags(&list, PyBufferFlags::FULL_RO, |_view| {}); assert!(result.is_err()); }); } @@ -1748,7 +1915,7 @@ mod tests { let bytes = PyBytes::new(py, b"abcde"); // Debug always uses raw_format/raw_shape/raw_strides (Option in output) - PyUntypedBufferView::with_flags(&bytes, ffi::PyBUF_FULL_RO, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FULL_RO, |view| { let expected = format!( concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", From f04f0d4e73ec6975bf38296522f87f3e59a2d961 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Wed, 8 Apr 2026 04:59:12 +0800 Subject: [PATCH 12/25] feat: add flag builder --- src/buffer.rs | 580 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 466 insertions(+), 114 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index ef852dbbf50..9ff80dfed35 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -845,7 +845,8 @@ mod py_buffer_flags_sealed { const WRITABLE: bool, const C_CONTIGUOUS: bool, const F_CONTIGUOUS: bool, - > Sealed for super::PyBufferFlags + > Sealed + for super::PyBufferFlags { } } @@ -860,72 +861,144 @@ impl< const WRITABLE: bool, const C_CONTIGUOUS: bool, const F_CONTIGUOUS: bool, - > PyBufferFlagsType for PyBufferFlags + > PyBufferFlagsType + for PyBufferFlags { } -// PyBufferFlags::FORMAT | -impl - std::ops::BitOr> - for PyBufferFlags +// Builder methods for composing flags. + +impl< + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlags { - type Output = PyBufferFlags; - fn bitor(self, rhs: PyBufferFlags) -> Self::Output { - PyBufferFlags(self.0 | rhs.0) + /// Request format information. + pub fn format( + self, + ) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_FORMAT) } } -// | PyBufferFlags::FORMAT -impl - std::ops::BitOr> - for PyBufferFlags +impl< + const FORMAT: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlags { - type Output = PyBufferFlags; - fn bitor(self, rhs: PyBufferFlags) -> Self::Output { - PyBufferFlags(self.0 | rhs.0) + /// Request shape information. + pub fn nd(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_ND) } } -#[allow(non_upper_case_globals)] -impl PyBufferFlags { - /// Request a simple buffer with no shape, strides, or format information. - pub const SIMPLE: PyBufferFlags = PyBufferFlags(ffi::PyBUF_SIMPLE); - /// Request format information only. - pub const FORMAT: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FORMAT); - /// Request shape information. - pub const ND: PyBufferFlags = PyBufferFlags(ffi::PyBUF_ND); - /// Request shape and strides. - pub const STRIDES: PyBufferFlags = PyBufferFlags(ffi::PyBUF_STRIDES); - /// Request C-contiguous buffer with shape and strides. - pub const C_CONTIGUOUS: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_C_CONTIGUOUS); - /// Request Fortran-contiguous buffer with shape and strides. - pub const F_CONTIGUOUS: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_F_CONTIGUOUS); - /// Request contiguous buffer (C or Fortran) with shape and strides. - pub const ANY_CONTIGUOUS: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_ANY_CONTIGUOUS); - /// Request shape, strides, and suboffsets. - pub const INDIRECT: PyBufferFlags = PyBufferFlags(ffi::PyBUF_INDIRECT); - /// Request writable buffer with shape. - pub const CONTIG: PyBufferFlags = PyBufferFlags(ffi::PyBUF_CONTIG); - /// Request shape (read-only, equivalent to [`Self::ND`]). - pub const CONTIG_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_CONTIG_RO); - /// Request writable buffer with shape and strides. - pub const STRIDED: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_STRIDED); - /// Request shape and strides (read-only, equivalent to [`Self::STRIDES`]). - pub const STRIDED_RO: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_STRIDED_RO); - /// Request writable buffer with shape, strides, and format. - pub const RECORDS: PyBufferFlags = - PyBufferFlags(ffi::PyBUF_RECORDS); - /// Request shape, strides, and format. - pub const RECORDS_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_RECORDS_RO); - /// Request writable buffer with all information including suboffsets. - pub const FULL: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FULL); +impl< + const FORMAT: bool, + const SHAPE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlags +{ + /// Request strides information. Implies shape. + pub fn strides( + self, + ) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_STRIDES) + } + + /// Request suboffsets (indirect). Implies shape and strides. + pub fn indirect( + self, + ) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT) + } +} + +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlags +{ + /// Request a writable buffer. + pub fn writable( + self, + ) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE) + } +} + +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const F_CONTIGUOUS: bool, + > PyBufferFlags +{ + /// Require C-contiguous layout. Implies shape and strides. + pub fn c_contiguous(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS) + } +} + +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + > PyBufferFlags +{ + /// Require Fortran-contiguous layout. Implies shape and strides. + pub fn f_contiguous(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS) + } +} + +impl + PyBufferFlags +{ + /// Require contiguous layout (C or Fortran). Implies shape and strides. + /// + /// The specific contiguity order is not known at compile time, + /// so this does not unlock non-Option slice accessors. + pub fn any_contiguous(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS) + } +} + +// Requires FORMAT=false, SHAPE=false, STRIDE=false +impl + PyBufferFlags +{ /// Request all buffer information including suboffsets. - pub const FULL_RO: PyBufferFlags = PyBufferFlags(ffi::PyBUF_FULL_RO); + /// Implies format, shape, and strides. Chain `.writable()` for write access. + pub fn full(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_FULL_RO) + } + + /// Request format, shape, and strides. + /// Chain `.writable()` for write access. + pub fn records(self) -> PyBufferFlags { + PyBufferFlags(self.0 | ffi::PyBUF_RECORDS_RO) + } +} + +impl PyBufferFlags { + /// Create a base buffer request. Chain builder methods to add flags. + pub fn simple() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_SIMPLE) + } } /// A typed form of [`PyUntypedBufferView`]. Not constructible directly — use @@ -1019,8 +1092,14 @@ impl PyUntypedBufferView { } } -impl - PyUntypedBufferView> +impl< + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > + PyUntypedBufferView> { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. @@ -1033,12 +1112,17 @@ impl( &self, - ) -> PyResult<&PyBufferView>> { + ) -> PyResult< + &PyBufferView>, + > { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> Ok(unsafe { NonNull::from(self) - .cast::>>() + .cast::, + >>() .as_ref() }) } @@ -1048,8 +1132,14 @@ impl - PyUntypedBufferView> +impl< + const FORMAT: bool, + const STRIDE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > + PyUntypedBufferView> { /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// @@ -1063,8 +1153,14 @@ impl - PyUntypedBufferView> +impl< + const FORMAT: bool, + const SHAPE: bool, + const WRITABLE: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, + > + PyUntypedBufferView> { /// Returns the strides array. /// @@ -1077,6 +1173,15 @@ impl { + /// Returns the format string for a simple buffer, which is always `"B"`. + #[inline] + pub fn format(&self) -> &CStr { + ffi::c_str!("B") + } +} + /// Check that a buffer is compatible with element type `T`. fn check_buffer_compatibility( buf: *mut c_void, @@ -1104,21 +1209,23 @@ impl PyUntypedBufferView { /// Acquire a buffer view with the given flags, /// pass it to `f`, then release the buffer. /// - /// Use predefined flag constants like [`PyBufferFlags::SIMPLE`], [`PyBufferFlags::ND`], - /// [`PyBufferFlags::STRIDES`], [`PyBufferFlags::FULL_RO`], etc. + /// Use [`PyBufferFlags::simple()`] to compose flags, e.g. + /// `PyBufferFlags::simple().strides()`, `PyBufferFlags::simple().full()`, etc. pub fn with_flags< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const CCONTIGUOUS: bool, - const FCONTIGUOUS: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags, f: impl FnOnce( - &PyUntypedBufferView>, + &PyUntypedBufferView< + PyBufferFlags, + >, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1178,10 +1285,10 @@ impl Debug for PyUntypedBufferView { } impl PyBufferView { - /// Acquire a typed buffer view with `PyBufferFlags::FULL_RO` flags, + /// Acquire a typed buffer view with `PyBufferFlags::simple().full()` flags, /// validating that the buffer format is compatible with `T`. pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { - PyUntypedBufferView::with_flags(obj, PyBufferFlags::FULL_RO, |view| { + PyUntypedBufferView::with_flags(obj, PyBufferFlags::simple().full(), |view| { view.as_typed::().map(f) })? } @@ -1193,31 +1300,31 @@ impl PyBufferView { const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const CCONTIGUOUS: bool, - const FCONTIGUOUS: bool, + const C_CONTIGUOUS: bool, + const F_CONTIGUOUS: bool, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags, f: impl FnOnce( - &PyBufferView>, + &PyBufferView< + T, + PyBufferFlags, + >, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer( - obj.as_ptr(), - raw.as_mut_ptr(), - flags.0 | ffi::PyBUF_FORMAT, - ) + ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = - PyUntypedBufferView::> { - raw: unsafe { raw.assume_init() }, - _flags: PhantomData, - }; + let view = PyUntypedBufferView::< + PyBufferFlags, + > { + raw: unsafe { raw.assume_init() }, + _flags: PhantomData, + }; view.as_typed::().map(f) } @@ -1260,8 +1367,8 @@ impl< const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const FCONTIGUOUS: bool, - > PyBufferView> + const F_CONTIGUOUS: bool, + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1270,8 +1377,13 @@ impl< } // C-contiguous + writable guaranteed — no checks needed. -impl - PyBufferView> +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const F_CONTIGUOUS: bool, + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1287,8 +1399,8 @@ impl< const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const CCONTIGUOUS: bool, - > PyBufferView> + const C_CONTIGUOUS: bool, + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1297,15 +1409,17 @@ impl< } // Fortran-contiguous + writable guaranteed — no checks needed. -impl - PyBufferView> +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const C_CONTIGUOUS: bool, + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. - pub fn as_fortran_contiguous_mut_slice<'a>( - &'a self, - _py: Python<'a>, - ) -> &'a [cell::Cell] { + pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } } @@ -1730,14 +1844,14 @@ mod tests { fn test_untyped_buffer_view() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FULL_RO, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // with() uses PyBufferFlags::FULL_RO — all Known, direct return types + // with() uses PyBufferFlags::simple().full() — all Known, direct return types assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -1757,7 +1871,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - // PyBufferView::with uses PyBufferFlags::FULL_RO — all Known + // PyBufferView::with uses PyBufferFlags::simple().full() — all Known assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -1807,7 +1921,7 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::SIMPLE, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.len_bytes(), 5); assert!(view.readonly()); @@ -1815,19 +1929,19 @@ mod tests { }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::ND, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().nd(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.shape(), [5]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::STRIDES, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().strides(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FORMAT, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.format().to_str().unwrap(), "B"); }) @@ -1844,7 +1958,7 @@ mod tests { .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); - PyBufferView::::with_flags(&array, PyBufferFlags::ND, |view| { + PyBufferView::::with_flags(&array, PyBufferFlags::simple().nd(), |view| { assert_eq!(view.item_count(), 4); assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); @@ -1865,7 +1979,8 @@ mod tests { fn test_typed_buffer_view_with_flags_incompatible() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - let result = PyBufferView::::with_flags(&bytes, PyBufferFlags::ND, |_view| {}); + let result = + PyBufferView::::with_flags(&bytes, PyBufferFlags::simple().nd(), |_view| {}); assert!(result.is_err()); }); } @@ -1880,12 +1995,16 @@ mod tests { .unwrap(); // C_CONTIGUOUS: guaranteed contiguous readonly access (no Option) - PyBufferView::::with_flags(&array, PyBufferFlags::C_CONTIGUOUS, |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice.len(), 3); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[2].get(), 2.0); - }) + PyBufferView::::with_flags( + &array, + PyBufferFlags::simple().c_contiguous(), + |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice.len(), 3); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[2].get(), 2.0); + }, + ) .unwrap(); // C_CONTIGUOUS | WRITABLE (via CONTIG combined with STRIDES-level): @@ -1904,18 +2023,251 @@ mod tests { fn test_buffer_view_error() { Python::attach(|py| { let list = crate::types::PyList::empty(py); - let result = PyUntypedBufferView::with_flags(&list, PyBufferFlags::FULL_RO, |_view| {}); + let result = + PyUntypedBufferView::with_flags(&list, PyBufferFlags::simple().full(), |_view| {}); assert!(result.is_err()); }); } + #[test] + fn test_flag_builders() { + Python::attach(|py| { + let bytes = PyBytes::new(py, b"abcde"); + let array = py + .import("array") + .unwrap() + .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) + .unwrap(); + + // Primitive builders + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().nd(), |view| { + assert_eq!(view.shape(), [5]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().strides(), |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().indirect(), |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().writable().nd(), + |view| { + assert_eq!(view.shape(), [4]); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + // Chained primitive builders + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().nd().format(), + |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.format().to_str().unwrap(), "B"); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().strides().format(), + |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + assert_eq!(view.format().to_str().unwrap(), "B"); + }, + ) + .unwrap(); + + // Contiguity builders + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().c_contiguous(), + |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().f_contiguous(), + |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().any_contiguous(), + |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) + .unwrap(); + + // Compound builders (read-only) + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().records(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + + // Compound builders + .writable() + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().full().writable(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); + assert_eq!(view.strides(), [4]); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().records().writable(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().strides().writable(), + |view| { + assert_eq!(view.shape(), [4]); + assert_eq!(view.strides(), [4]); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().nd().writable(), + |view| { + assert_eq!(view.shape(), [4]); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + // Compound + contiguity + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().full().c_contiguous(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &array, + PyBufferFlags::simple().full().writable().c_contiguous(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert!(!view.readonly()); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().strides().format(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) + .unwrap(); + + PyUntypedBufferView::with_flags( + &bytes, + PyBufferFlags::simple().c_contiguous().format(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + }, + ) + .unwrap(); + + // Contiguity builder on typed view + PyBufferView::::with_flags( + &array, + PyBufferFlags::simple().c_contiguous(), + |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice[0].get(), 1.0); + }, + ) + .unwrap(); + + // Writable + contiguity on typed view + PyBufferView::::with_flags( + &array, + PyBufferFlags::simple().c_contiguous().writable(), + |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice[0].get(), 1.0); + let mut_slice = view.as_contiguous_mut_slice(py); + mut_slice[0].set(9.0); + assert_eq!(slice[0].get(), 9.0); + }, + ) + .unwrap(); + + // SIMPLE format() returns "B" + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + }) + .unwrap(); + }); + } + #[test] fn test_buffer_view_debug() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); // Debug always uses raw_format/raw_shape/raw_strides (Option in output) - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::FULL_RO, |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { let expected = format!( concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", From e8f473c4ca8c4f95dbf376ba4ed5bc5158ebbf78 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Sun, 12 Apr 2026 22:39:11 +0800 Subject: [PATCH 13/25] refactor: apply suggestions --- src/buffer.rs | 452 ++++++++++++++++++++++---------------------------- 1 file changed, 199 insertions(+), 253 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 9ff80dfed35..2878247f59a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -825,6 +825,19 @@ impl_element!(isize, SignedInteger); impl_element!(f32, Float); impl_element!(f64, Float); +#[repr(u8)] +enum PyBufferContiguity { + Undefined = 0, + C = 1, + F = 2, + Any = 3, +} + +const CONTIGUITY_UNDEFINED: u8 = PyBufferContiguity::Undefined as u8; +const CONTIGUITY_C: u8 = PyBufferContiguity::C as u8; +const CONTIGUITY_F: u8 = PyBufferContiguity::F as u8; +const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; + /// Type-safe buffer request flags. The const parameters encode which fields /// the exporter is required to fill. pub struct PyBufferFlags< @@ -832,8 +845,7 @@ pub struct PyBufferFlags< const SHAPE: bool = false, const STRIDE: bool = false, const WRITABLE: bool = false, - const C_CONTIGUOUS: bool = false, - const F_CONTIGUOUS: bool = false, + const CONTIGUITY: u8 = CONTIGUITY_UNDEFINED, >(c_int); mod py_buffer_flags_sealed { @@ -843,161 +855,138 @@ mod py_buffer_flags_sealed { const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > Sealed - for super::PyBufferFlags + const CONTIGUITY: u8, + > Sealed for super::PyBufferFlags { } } /// Trait implemented by all [`PyBufferFlags`] instantiations. -pub trait PyBufferFlagsType: py_buffer_flags_sealed::Sealed {} +pub trait PyBufferFlagsType: py_buffer_flags_sealed::Sealed { + /// The contiguity requirement encoded by these flags. + const CONTIGUITY: u8; +} impl< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlagsType - for PyBufferFlags + const CONTIGUITY_REQ: u8, + > PyBufferFlagsType for PyBufferFlags { + const CONTIGUITY: u8 = CONTIGUITY_REQ; } -// Builder methods for composing flags. - -impl< - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlags +impl + PyBufferFlags { /// Request format information. - pub fn format( - self, - ) -> PyBufferFlags { + pub const fn format(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_FORMAT) } } -impl< - const FORMAT: bool, - const STRIDE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlags +impl + PyBufferFlags { /// Request shape information. - pub fn nd(self) -> PyBufferFlags { + pub const fn nd(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_ND) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlags +impl + PyBufferFlags { /// Request strides information. Implies shape. - pub fn strides( - self, - ) -> PyBufferFlags { + pub const fn strides(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_STRIDES) } /// Request suboffsets (indirect). Implies shape and strides. - pub fn indirect( - self, - ) -> PyBufferFlags { + pub const fn indirect(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlags +impl + PyBufferFlags { /// Request a writable buffer. - pub fn writable( - self, - ) -> PyBufferFlags { + pub const fn writable(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const F_CONTIGUOUS: bool, - > PyBufferFlags +impl + PyBufferFlags { /// Require C-contiguous layout. Implies shape and strides. - pub fn c_contiguous(self) -> PyBufferFlags { + pub const fn c_contiguous(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS) } -} -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - > PyBufferFlags -{ /// Require Fortran-contiguous layout. Implies shape and strides. - pub fn f_contiguous(self) -> PyBufferFlags { + pub const fn f_contiguous(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS) } -} -impl - PyBufferFlags -{ /// Require contiguous layout (C or Fortran). Implies shape and strides. /// /// The specific contiguity order is not known at compile time, /// so this does not unlock non-Option slice accessors. - pub fn any_contiguous(self) -> PyBufferFlags { + pub const fn any_contiguous( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS) } } -// Requires FORMAT=false, SHAPE=false, STRIDE=false -impl - PyBufferFlags -{ - /// Request all buffer information including suboffsets. - /// Implies format, shape, and strides. Chain `.writable()` for write access. - pub fn full(self) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_FULL_RO) +impl PyBufferFlags { + /// Create a base buffer request. Chain builder methods to add flags. + pub const fn simple() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_SIMPLE) } - /// Request format, shape, and strides. - /// Chain `.writable()` for write access. - pub fn records(self) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_RECORDS_RO) + /// Create a writable request for all buffer information including suboffsets. + pub const fn full() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_FULL) } -} -impl PyBufferFlags { - /// Create a base buffer request. Chain builder methods to add flags. - pub fn simple() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_SIMPLE) + /// Create a read-only request for all buffer information including suboffsets. + pub const fn full_ro() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_FULL_RO) + } + + /// Create a writable request for format, shape, and strides. + pub const fn records() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_RECORDS) + } + + /// Create a read-only request for format, shape, and strides. + pub const fn records_ro() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_RECORDS_RO) + } + + /// Create a writable request for shape and strides. + pub const fn strided() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_STRIDED) + } + + /// Create a read-only request for shape and strides. + pub const fn strided_ro() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_STRIDED_RO) + } + + /// Create a writable C-contiguous request. + pub const fn contig() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_CONTIG) + } + + /// Create a read-only C-contiguous request. + pub const fn contig_ro() -> PyBufferFlags { + PyBufferFlags(ffi::PyBUF_CONTIG_RO) } } @@ -1014,7 +1003,7 @@ pub struct PyBufferView { raw: ffi::Py_buffer, @@ -1082,24 +1071,20 @@ impl PyUntypedBufferView { /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } + Flags::CONTIGUITY == CONTIGUITY_C + || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { - unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } + Flags::CONTIGUITY == CONTIGUITY_F + || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } } } -impl< - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > - PyUntypedBufferView> +impl + PyUntypedBufferView> { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. @@ -1112,17 +1097,12 @@ impl< /// Attempt to interpret this untyped view as containing elements of type `T`. pub fn as_typed( &self, - ) -> PyResult< - &PyBufferView>, - > { + ) -> PyResult<&PyBufferView>> { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> Ok(unsafe { NonNull::from(self) - .cast::, - >>() + .cast::>>() .as_ref() }) } @@ -1132,14 +1112,8 @@ impl< } } -impl< - const FORMAT: bool, - const STRIDE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > - PyUntypedBufferView> +impl + PyUntypedBufferView> { /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// @@ -1153,14 +1127,8 @@ impl< } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, - > - PyUntypedBufferView> +impl + PyUntypedBufferView> { /// Returns the strides array. /// @@ -1173,9 +1141,11 @@ impl< } } -// SIMPLE: format is guaranteed to be "B" (unsigned bytes) by the buffer protocol. -impl PyUntypedBufferView { - /// Returns the format string for a simple buffer, which is always `"B"`. +// SIMPLE and WRITABLE requests guarantee the implicit "B" format. +impl + PyUntypedBufferView> +{ + /// Returns the format string for a simple byte buffer, which is always `"B"`. #[inline] pub fn format(&self) -> &CStr { ffi::c_str!("B") @@ -1209,23 +1179,20 @@ impl PyUntypedBufferView { /// Acquire a buffer view with the given flags, /// pass it to `f`, then release the buffer. /// - /// Use [`PyBufferFlags::simple()`] to compose flags, e.g. - /// `PyBufferFlags::simple().strides()`, `PyBufferFlags::simple().full()`, etc. + /// Use [`PyBufferFlags::simple()`] or one of the compound-request constructors such as + /// [`PyBufferFlags::full_ro()`] to acquire a view. pub fn with_flags< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, + const CONTIGUITY: u8, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags, f: impl FnOnce( - &PyUntypedBufferView< - PyBufferFlags, - >, + &PyUntypedBufferView>, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1285,10 +1252,10 @@ impl Debug for PyUntypedBufferView { } impl PyBufferView { - /// Acquire a typed buffer view with `PyBufferFlags::simple().full()` flags, + /// Acquire a typed buffer view with `PyBufferFlags::full_ro()` flags, /// validating that the buffer format is compatible with `T`. pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { - PyUntypedBufferView::with_flags(obj, PyBufferFlags::simple().full(), |view| { + PyUntypedBufferView::with_flags(obj, PyBufferFlags::full_ro(), |view| { view.as_typed::().map(f) })? } @@ -1297,21 +1264,16 @@ impl PyBufferView { /// /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. pub fn with_flags< + const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const C_CONTIGUOUS: bool, - const F_CONTIGUOUS: bool, + const CONTIGUITY: u8, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, - f: impl FnOnce( - &PyBufferView< - T, - PyBufferFlags, - >, - ) -> R, + flags: PyBufferFlags, + f: impl FnOnce(&PyBufferView>) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1319,9 +1281,7 @@ impl PyBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = PyUntypedBufferView::< - PyBufferFlags, - > { + let view = PyUntypedBufferView::> { raw: unsafe { raw.assume_init() }, _flags: PhantomData, }; @@ -1367,8 +1327,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const F_CONTIGUOUS: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1377,13 +1336,8 @@ impl< } // C-contiguous + writable guaranteed — no checks needed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const F_CONTIGUOUS: bool, - > PyBufferView> +impl + PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1399,8 +1353,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const WRITABLE: bool, - const C_CONTIGUOUS: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1409,13 +1362,8 @@ impl< } // Fortran-contiguous + writable guaranteed — no checks needed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const C_CONTIGUOUS: bool, - > PyBufferView> +impl + PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. @@ -1844,14 +1792,14 @@ mod tests { fn test_untyped_buffer_view() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // with() uses PyBufferFlags::simple().full() — all Known, direct return types + // with_flags() uses PyBufferFlags::full_ro() — all Known, direct return types assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -1871,7 +1819,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - // PyBufferView::with uses PyBufferFlags::simple().full() — all Known + // PyBufferView::with uses PyBufferFlags::full_ro() — all Known assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -2024,7 +1972,7 @@ mod tests { Python::attach(|py| { let list = crate::types::PyList::empty(py); let result = - PyUntypedBufferView::with_flags(&list, PyBufferFlags::simple().full(), |_view| {}); + PyUntypedBufferView::with_flags(&list, PyBufferFlags::full_ro(), |_view| {}); assert!(result.is_err()); }); } @@ -2040,6 +1988,11 @@ mod tests { .unwrap(); // Primitive builders + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + }) + .unwrap(); + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); }) @@ -2062,6 +2015,12 @@ mod tests { }) .unwrap(); + PyUntypedBufferView::with_flags(&array, PyBufferFlags::simple().writable(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert!(!view.readonly()); + }) + .unwrap(); + PyUntypedBufferView::with_flags( &array, PyBufferFlags::simple().writable().nd(), @@ -2125,70 +2084,67 @@ mod tests { ) .unwrap(); - // Compound builders (read-only) - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { + // Compound requests (read-only) + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().records(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::records_ro(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - // Compound builders + .writable() - PyUntypedBufferView::with_flags( - &array, - PyBufferFlags::simple().full().writable(), - |view| { - assert_eq!(view.format().to_str().unwrap(), "f"); - assert_eq!(view.shape(), [4]); - assert_eq!(view.strides(), [4]); - assert!(!view.readonly()); - }, - ) + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::strided_ro(), |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) .unwrap(); - PyUntypedBufferView::with_flags( - &array, - PyBufferFlags::simple().records().writable(), - |view| { - assert_eq!(view.format().to_str().unwrap(), "f"); - assert_eq!(view.shape(), [4]); - assert!(!view.readonly()); - }, - ) + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::contig_ro(), |view| { + assert_eq!(view.shape(), [5]); + assert!(view.is_c_contiguous()); + }) .unwrap(); - PyUntypedBufferView::with_flags( - &array, - PyBufferFlags::simple().strides().writable(), - |view| { - assert_eq!(view.shape(), [4]); - assert_eq!(view.strides(), [4]); - assert!(!view.readonly()); - }, - ) + // Writable compound requests + PyUntypedBufferView::with_flags(&array, PyBufferFlags::full(), |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); + assert_eq!(view.strides(), [4]); + assert!(!view.readonly()); + }) .unwrap(); - PyUntypedBufferView::with_flags( - &array, - PyBufferFlags::simple().nd().writable(), - |view| { - assert_eq!(view.shape(), [4]); - assert!(!view.readonly()); - }, - ) + PyUntypedBufferView::with_flags(&array, PyBufferFlags::records(), |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); + assert!(!view.readonly()); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&array, PyBufferFlags::strided(), |view| { + assert_eq!(view.shape(), [4]); + assert_eq!(view.strides(), [4]); + assert!(!view.readonly()); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&array, PyBufferFlags::contig(), |view| { + assert_eq!(view.shape(), [4]); + assert!(!view.readonly()); + assert!(view.is_c_contiguous()); + }) .unwrap(); // Compound + contiguity PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().full().c_contiguous(), + PyBufferFlags::full_ro().c_contiguous(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -2197,25 +2153,17 @@ mod tests { ) .unwrap(); - PyUntypedBufferView::with_flags( - &array, - PyBufferFlags::simple().full().writable().c_contiguous(), - |view| { - assert_eq!(view.format().to_str().unwrap(), "f"); - assert!(!view.readonly()); - }, - ) + PyUntypedBufferView::with_flags(&array, PyBufferFlags::full().c_contiguous(), |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert!(!view.readonly()); + }) .unwrap(); - PyUntypedBufferView::with_flags( - &bytes, - PyBufferFlags::simple().strides().format(), - |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); - assert_eq!(view.shape(), [5]); - assert_eq!(view.strides(), [1]); - }, - ) + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::strided_ro().format(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }) .unwrap(); PyUntypedBufferView::with_flags( @@ -2229,28 +2177,26 @@ mod tests { .unwrap(); // Contiguity builder on typed view - PyBufferView::::with_flags( - &array, - PyBufferFlags::simple().c_contiguous(), - |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice[0].get(), 1.0); - }, - ) + PyBufferView::::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.item_count(), 5); + }) + .unwrap(); + + PyBufferView::::with_flags(&array, PyBufferFlags::contig(), |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice[0].get(), 1.0); + }) .unwrap(); // Writable + contiguity on typed view - PyBufferView::::with_flags( - &array, - PyBufferFlags::simple().c_contiguous().writable(), - |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice[0].get(), 1.0); - let mut_slice = view.as_contiguous_mut_slice(py); - mut_slice[0].set(9.0); - assert_eq!(slice[0].get(), 9.0); - }, - ) + PyBufferView::::with_flags(&array, PyBufferFlags::contig(), |view| { + let slice = view.as_contiguous_slice(py); + assert_eq!(slice[0].get(), 1.0); + let mut_slice = view.as_contiguous_mut_slice(py); + mut_slice[0].set(9.0); + assert_eq!(slice[0].get(), 9.0); + }) .unwrap(); // SIMPLE format() returns "B" @@ -2267,7 +2213,7 @@ mod tests { let bytes = PyBytes::new(py, b"abcde"); // Debug always uses raw_format/raw_shape/raw_strides (Option in output) - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().full(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { let expected = format!( concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", From 37f2a18d94c6776d9622ab07bc05f0bc69cc5764 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Tue, 14 Apr 2026 01:00:18 +0800 Subject: [PATCH 14/25] refactor: hide suboffsets if guaranteed null --- src/buffer.rs | 367 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 251 insertions(+), 116 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 2878247f59a..846c4169c09 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -58,24 +58,20 @@ struct RawBuffer(ffi::Py_buffer, PhantomPinned); unsafe impl Send for PyUntypedBuffer {} unsafe impl Sync for PyUntypedBuffer {} -impl Debug for PyBuffer { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - debug_buffer("PyBuffer", &self.0, f) - } -} - -impl Debug for PyUntypedBuffer { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - debug_buffer("PyUntypedBuffer", self, f) - } -} - fn debug_buffer( name: &str, - b: &PyUntypedBuffer, + raw: &ffi::Py_buffer, f: &mut core::fmt::Formatter<'_>, ) -> core::fmt::Result { - let raw = b.raw(); + let ndim = raw.ndim as usize; + let format = NonNull::new(raw.format).map(|p| unsafe { CStr::from_ptr(p.as_ptr()) }); + let shape = NonNull::new(raw.shape) + .map(|p| unsafe { slice::from_raw_parts(p.as_ptr().cast::(), ndim) }); + let strides = + NonNull::new(raw.strides).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); + let suboffsets = + NonNull::new(raw.suboffsets).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); + f.debug_struct(name) .field("buf", &raw.buf) .field("obj", &raw.obj) @@ -83,14 +79,26 @@ fn debug_buffer( .field("itemsize", &raw.itemsize) .field("readonly", &raw.readonly) .field("ndim", &raw.ndim) - .field("format", &b.format()) - .field("shape", &b.shape()) - .field("strides", &b.strides()) - .field("suboffsets", &b.suboffsets()) + .field("format", &format) + .field("shape", &shape) + .field("strides", &strides) + .field("suboffsets", &suboffsets) .field("internal", &raw.internal) .finish() } +impl Debug for PyBuffer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + debug_buffer("PyBuffer", self.raw(), f) + } +} + +impl Debug for PyUntypedBuffer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + debug_buffer("PyUntypedBuffer", self.raw(), f) + } +} + /// Represents the type of a Python buffer element. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ElementType { @@ -844,6 +852,7 @@ pub struct PyBufferFlags< const FORMAT: bool = false, const SHAPE: bool = false, const STRIDE: bool = false, + const INDIRECT: bool = false, const WRITABLE: bool = false, const CONTIGUITY: u8 = CONTIGUITY_UNDEFINED, >(c_int); @@ -854,9 +863,10 @@ mod py_buffer_flags_sealed { const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > Sealed for super::PyBufferFlags + > Sealed for super::PyBufferFlags { } } @@ -865,70 +875,115 @@ mod py_buffer_flags_sealed { pub trait PyBufferFlagsType: py_buffer_flags_sealed::Sealed { /// The contiguity requirement encoded by these flags. const CONTIGUITY: u8; + + /// Whether these flags require a writable buffer. + const WRITABLE: bool; } impl< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY_REQ: u8, - > PyBufferFlagsType for PyBufferFlags + > PyBufferFlagsType + for PyBufferFlags { const CONTIGUITY: u8 = CONTIGUITY_REQ; + const WRITABLE: bool = WRITABLE; } -impl - PyBufferFlags +impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyBufferFlags { /// Request format information. - pub const fn format(self) -> PyBufferFlags { + pub const fn format( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_FORMAT) } } -impl - PyBufferFlags +impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyBufferFlags { /// Request shape information. - pub const fn nd(self) -> PyBufferFlags { + pub const fn nd(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_ND) } } -impl - PyBufferFlags +impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyBufferFlags { /// Request strides information. Implies shape. - pub const fn strides(self) -> PyBufferFlags { + pub const fn strides( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_STRIDES) } +} +impl + PyBufferFlags +{ /// Request suboffsets (indirect). Implies shape and strides. - pub const fn indirect(self) -> PyBufferFlags { + pub const fn indirect(self) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT) } } -impl - PyBufferFlags +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const CONTIGUITY: u8, + > PyBufferFlags { /// Request a writable buffer. - pub const fn writable(self) -> PyBufferFlags { + pub const fn writable( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE) } } -impl - PyBufferFlags +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + > PyBufferFlags { /// Require C-contiguous layout. Implies shape and strides. - pub const fn c_contiguous(self) -> PyBufferFlags { + pub const fn c_contiguous( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS) } /// Require Fortran-contiguous layout. Implies shape and strides. - pub const fn f_contiguous(self) -> PyBufferFlags { + pub const fn f_contiguous( + self, + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS) } @@ -938,7 +993,7 @@ impl PyBufferFlags { + ) -> PyBufferFlags { PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS) } } @@ -950,42 +1005,42 @@ impl PyBufferFlags { } /// Create a writable request for all buffer information including suboffsets. - pub const fn full() -> PyBufferFlags { + pub const fn full() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_FULL) } /// Create a read-only request for all buffer information including suboffsets. - pub const fn full_ro() -> PyBufferFlags { + pub const fn full_ro() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_FULL_RO) } /// Create a writable request for format, shape, and strides. - pub const fn records() -> PyBufferFlags { + pub const fn records() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_RECORDS) } /// Create a read-only request for format, shape, and strides. - pub const fn records_ro() -> PyBufferFlags { + pub const fn records_ro() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_RECORDS_RO) } /// Create a writable request for shape and strides. - pub const fn strided() -> PyBufferFlags { + pub const fn strided() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_STRIDED) } /// Create a read-only request for shape and strides. - pub const fn strided_ro() -> PyBufferFlags { + pub const fn strided_ro() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_STRIDED_RO) } /// Create a writable C-contiguous request. - pub const fn contig() -> PyBufferFlags { + pub const fn contig() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_CONTIG) } /// Create a read-only C-contiguous request. - pub const fn contig_ro() -> PyBufferFlags { + pub const fn contig_ro() -> PyBufferFlags { PyBufferFlags(ffi::PyBUF_CONTIG_RO) } } @@ -993,7 +1048,7 @@ impl PyBufferFlags { /// A typed form of [`PyUntypedBufferView`]. Not constructible directly — use /// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. #[repr(transparent)] -pub struct PyBufferView>( +pub struct PyBufferView>( PyUntypedBufferView, PhantomData<[T]>, ); @@ -1026,7 +1081,7 @@ impl PyUntypedBufferView { /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { - self.raw.readonly != 0 + !Flags::WRITABLE && self.raw.readonly != 0 } /// Gets the size of a single element, in bytes. @@ -1056,18 +1111,6 @@ impl PyUntypedBufferView { self.raw.ndim as usize } - /// Returns the suboffsets array. - /// - /// May return `None` even with `PyBUF_INDIRECT` if the exporter sets `suboffsets` to NULL. - #[inline] - pub fn suboffsets(&self) -> Option<&[isize]> { - if self.raw.suboffsets.is_null() { - return None; - } - - Some(unsafe { slice::from_raw_parts(self.raw.suboffsets, self.raw.ndim as usize) }) - } - /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { @@ -1083,8 +1126,13 @@ impl PyUntypedBufferView { } } -impl - PyUntypedBufferView> +impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyUntypedBufferView> { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. @@ -1097,12 +1145,19 @@ impl( &self, - ) -> PyResult<&PyBufferView>> { + ) -> PyResult< + &PyBufferView>, + > { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> Ok(unsafe { NonNull::from(self) - .cast::>>() + .cast::< + PyBufferView< + T, + PyBufferFlags, + >, + >() .as_ref() }) } @@ -1112,8 +1167,13 @@ impl - PyUntypedBufferView> +impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyUntypedBufferView> { /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// @@ -1127,8 +1187,13 @@ impl - PyUntypedBufferView> +impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > PyUntypedBufferView> { /// Returns the strides array. /// @@ -1141,9 +1206,31 @@ impl PyUntypedBufferView> +{ + /// Returns the suboffsets array. + /// + /// May return `None` even when suboffsets were requested if the exporter sets + /// `suboffsets` to `NULL`. + #[inline] + pub fn suboffsets(&self) -> Option<&[isize]> { + if self.raw.suboffsets.is_null() { + return None; + } + + Some(unsafe { slice::from_raw_parts(self.raw.suboffsets, self.raw.ndim as usize) }) + } +} + // SIMPLE and WRITABLE requests guarantee the implicit "B" format. impl - PyUntypedBufferView> + PyUntypedBufferView> { /// Returns the format string for a simple byte buffer, which is always `"B"`. #[inline] @@ -1185,14 +1272,17 @@ impl PyUntypedBufferView { const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags, f: impl FnOnce( - &PyUntypedBufferView>, + &PyUntypedBufferView< + PyBufferFlags, + >, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1210,35 +1300,6 @@ impl PyUntypedBufferView { } } -fn debug_buffer_view( - name: &str, - raw: &ffi::Py_buffer, - f: &mut std::fmt::Formatter<'_>, -) -> std::fmt::Result { - let ndim = raw.ndim as usize; - let format = NonNull::new(raw.format).map(|p| unsafe { CStr::from_ptr(p.as_ptr()) }); - let shape = NonNull::new(raw.shape) - .map(|p| unsafe { slice::from_raw_parts(p.as_ptr().cast::(), ndim) }); - let strides = - NonNull::new(raw.strides).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); - let suboffsets = - NonNull::new(raw.suboffsets).map(|p| unsafe { slice::from_raw_parts(p.as_ptr(), ndim) }); - - f.debug_struct(name) - .field("buf", &raw.buf) - .field("obj", &raw.obj) - .field("len", &raw.len) - .field("itemsize", &raw.itemsize) - .field("readonly", &raw.readonly) - .field("ndim", &raw.ndim) - .field("format", &format) - .field("shape", &shape) - .field("strides", &strides) - .field("suboffsets", &suboffsets) - .field("internal", &raw.internal) - .finish() -} - impl Drop for PyUntypedBufferView { fn drop(&mut self) { unsafe { ffi::PyBuffer_Release(&mut self.raw) } @@ -1247,7 +1308,7 @@ impl Drop for PyUntypedBufferView { impl Debug for PyUntypedBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - debug_buffer_view("PyUntypedBufferView", &self.raw, f) + debug_buffer("PyUntypedBufferView", &self.raw, f) } } @@ -1267,13 +1328,16 @@ impl PyBufferView { const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, - f: impl FnOnce(&PyBufferView>) -> R, + flags: PyBufferFlags, + f: impl FnOnce( + &PyBufferView>, + ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1281,7 +1345,9 @@ impl PyBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = PyUntypedBufferView::> { + let view = PyUntypedBufferView::< + PyBufferFlags, + > { raw: unsafe { raw.assume_init() }, _flags: PhantomData, }; @@ -1326,8 +1392,9 @@ impl< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1336,8 +1403,13 @@ impl< } // C-contiguous + writable guaranteed — no checks needed. -impl - PyBufferView> +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1346,14 +1418,15 @@ impl } } -// Fortran-contiguous guaranteed — no contiguity check needed. +// Fortran-contiguous guaranteed. impl< T: Element, const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, + const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1361,9 +1434,14 @@ impl< } } -// Fortran-contiguous + writable guaranteed — no checks needed. -impl - PyBufferView> +// Fortran-contiguous + writable guaranteed. +impl< + T: Element, + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. @@ -1382,7 +1460,7 @@ impl std::ops::Deref for PyBufferView { impl Debug for PyBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - debug_buffer_view("PyBufferView", &self.0.raw, f) + debug_buffer("PyBufferView", &self.0.raw, f) } } @@ -1422,8 +1500,8 @@ mod tests { concat!( "PyBuffer {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: \"B\", shape: [5], ", - "strides: [1], suboffsets: None, internal: {:?} }}", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "strides: Some([1]), suboffsets: None, internal: {:?} }}", ), buffer.raw().buf, buffer.raw().obj, @@ -1431,6 +1509,21 @@ mod tests { ); let debug_repr = format!("{:?}", buffer); assert_eq!(debug_repr, expected); + + let untyped = PyUntypedBuffer::get(&bytes).unwrap(); + let expected = format!( + concat!( + "PyUntypedBuffer {{ buf: {:?}, obj: {:?}, ", + "len: 5, itemsize: 1, readonly: 1, ", + "ndim: 1, format: Some(\"B\"), shape: Some([5]), ", + "strides: Some([1]), suboffsets: None, internal: {:?} }}", + ), + untyped.raw().buf, + untyped.raw().obj, + untyped.raw().internal + ); + let debug_repr = format!("{:?}", untyped); + assert_eq!(debug_repr, expected); }); } @@ -1822,6 +1915,7 @@ mod tests { // PyBufferView::with uses PyBufferFlags::full_ro() — all Known assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); + assert!(view.suboffsets().is_none()); let slice = view.as_slice(py).unwrap(); assert_eq!(slice.len(), 5); @@ -1873,7 +1967,6 @@ mod tests { assert_eq!(view.item_count(), 5); assert_eq!(view.len_bytes(), 5); assert!(view.readonly()); - assert!(view.suboffsets().is_none()); }) .unwrap(); @@ -1889,6 +1982,13 @@ mod tests { }) .unwrap(); + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().indirect(), |view| { + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + assert!(view.suboffsets().is_none()); + }) + .unwrap(); + PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.format().to_str().unwrap(), "B"); @@ -1979,6 +2079,38 @@ mod tests { #[test] fn test_flag_builders() { + fn assert_direct< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + >( + _: PyBufferFlags, + ) { + } + + fn assert_indirect< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + >( + _: PyBufferFlags, + ) { + } + + assert_direct(PyBufferFlags::simple()); + assert_direct(PyBufferFlags::records_ro()); + assert_direct(PyBufferFlags::strided_ro()); + assert_direct(PyBufferFlags::contig_ro()); + assert_indirect(PyBufferFlags::simple().indirect()); + assert_indirect(PyBufferFlags::full_ro()); + assert_indirect(PyBufferFlags::full()); + assert_direct(PyBufferFlags::full_ro().c_contiguous()); + assert_direct(PyBufferFlags::full().c_contiguous()); + Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); let array = py @@ -2012,6 +2144,7 @@ mod tests { PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().indirect(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); + assert!(view.suboffsets().is_none()); }) .unwrap(); @@ -2089,6 +2222,7 @@ mod tests { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); + assert!(view.suboffsets().is_none()); }) .unwrap(); @@ -2116,6 +2250,7 @@ mod tests { assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); assert_eq!(view.strides(), [4]); + assert!(view.suboffsets().is_none()); assert!(!view.readonly()); }) .unwrap(); From 0dbb97e8d8f9d4dc266ea9bdbea4da1c528ccbf8 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Tue, 14 Apr 2026 01:21:36 +0800 Subject: [PATCH 15/25] refactor: hide `PyBufferFlags` --- src/buffer.rs | 209 +++++++++++++++++++++++++++----------------------- 1 file changed, 115 insertions(+), 94 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 846c4169c09..24ea8f6ec34 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -846,18 +846,22 @@ const CONTIGUITY_C: u8 = PyBufferContiguity::C as u8; const CONTIGUITY_F: u8 = PyBufferContiguity::F as u8; const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; -/// Type-safe buffer request flags. The const parameters encode which fields -/// the exporter is required to fill. +/// Type-safe buffer request flags. The state parameter is intentionally hidden +/// behind this wrapper so the internal encoding can evolve. pub struct PyBufferFlags< - const FORMAT: bool = false, - const SHAPE: bool = false, - const STRIDE: bool = false, - const INDIRECT: bool = false, - const WRITABLE: bool = false, - const CONTIGUITY: u8 = CONTIGUITY_UNDEFINED, ->(c_int); - -mod py_buffer_flags_sealed { + Flags: PyBufferFlagsType = FlagsImpl, +>(c_int, PhantomData); + +mod py_buffer_flags_impl { + pub struct PyBufferFlagsImpl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + >; + pub trait Sealed {} impl< const FORMAT: bool, @@ -866,13 +870,15 @@ mod py_buffer_flags_sealed { const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > Sealed for super::PyBufferFlags + > Sealed for PyBufferFlagsImpl { } } -/// Trait implemented by all [`PyBufferFlags`] instantiations. -pub trait PyBufferFlagsType: py_buffer_flags_sealed::Sealed { +use self::py_buffer_flags_impl::PyBufferFlagsImpl as FlagsImpl; + +/// Trait implemented by all hidden [`PyBufferFlags`] states. +pub trait PyBufferFlagsType: py_buffer_flags_impl::Sealed { /// The contiguity requirement encoded by these flags. const CONTIGUITY: u8; @@ -887,8 +893,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY_REQ: u8, - > PyBufferFlagsType - for PyBufferFlags + > PyBufferFlagsType for FlagsImpl { const CONTIGUITY: u8 = CONTIGUITY_REQ; const WRITABLE: bool = WRITABLE; @@ -900,13 +905,13 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags + > PyBufferFlags> { /// Request format information. pub const fn format( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_FORMAT) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_FORMAT, PhantomData) } } @@ -916,11 +921,13 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags + > PyBufferFlags> { /// Request shape information. - pub const fn nd(self) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_ND) + pub const fn nd( + self, + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_ND, PhantomData) } } @@ -930,22 +937,24 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags + > PyBufferFlags> { /// Request strides information. Implies shape. pub const fn strides( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_STRIDES) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_STRIDES, PhantomData) } } impl - PyBufferFlags + PyBufferFlags> { /// Request suboffsets (indirect). Implies shape and strides. - pub const fn indirect(self) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT) + pub const fn indirect( + self, + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT, PhantomData) } } @@ -955,13 +964,13 @@ impl< const STRIDE: bool, const INDIRECT: bool, const CONTIGUITY: u8, - > PyBufferFlags + > PyBufferFlags> { /// Request a writable buffer. pub const fn writable( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE, PhantomData) } } @@ -971,20 +980,20 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferFlags + > PyBufferFlags> { /// Require C-contiguous layout. Implies shape and strides. pub const fn c_contiguous( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) } /// Require Fortran-contiguous layout. Implies shape and strides. pub const fn f_contiguous( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) } /// Require contiguous layout (C or Fortran). Implies shape and strides. @@ -993,65 +1002,81 @@ impl< /// so this does not unlock non-Option slice accessors. pub const fn any_contiguous( self, - ) -> PyBufferFlags { - PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS) + ) -> PyBufferFlags> { + PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) } } -impl PyBufferFlags { +impl PyBufferFlags> { /// Create a base buffer request. Chain builder methods to add flags. - pub const fn simple() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_SIMPLE) + pub const fn simple() -> Self { + PyBufferFlags(ffi::PyBUF_SIMPLE, PhantomData) } +} +impl PyBufferFlags> { /// Create a writable request for all buffer information including suboffsets. - pub const fn full() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_FULL) + pub const fn full() -> Self { + PyBufferFlags(ffi::PyBUF_FULL, PhantomData) } +} +impl PyBufferFlags> { /// Create a read-only request for all buffer information including suboffsets. - pub const fn full_ro() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_FULL_RO) + pub const fn full_ro() -> Self { + PyBufferFlags(ffi::PyBUF_FULL_RO, PhantomData) } +} +impl PyBufferFlags> { /// Create a writable request for format, shape, and strides. - pub const fn records() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_RECORDS) + pub const fn records() -> Self { + PyBufferFlags(ffi::PyBUF_RECORDS, PhantomData) } +} +impl PyBufferFlags> { /// Create a read-only request for format, shape, and strides. - pub const fn records_ro() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_RECORDS_RO) + pub const fn records_ro() -> Self { + PyBufferFlags(ffi::PyBUF_RECORDS_RO, PhantomData) } +} +impl PyBufferFlags> { /// Create a writable request for shape and strides. - pub const fn strided() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_STRIDED) + pub const fn strided() -> Self { + PyBufferFlags(ffi::PyBUF_STRIDED, PhantomData) } +} +impl PyBufferFlags> { /// Create a read-only request for shape and strides. - pub const fn strided_ro() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_STRIDED_RO) + pub const fn strided_ro() -> Self { + PyBufferFlags(ffi::PyBUF_STRIDED_RO, PhantomData) } +} +impl PyBufferFlags> { /// Create a writable C-contiguous request. - pub const fn contig() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_CONTIG) + pub const fn contig() -> Self { + PyBufferFlags(ffi::PyBUF_CONTIG, PhantomData) } +} +impl PyBufferFlags> { /// Create a read-only C-contiguous request. - pub const fn contig_ro() -> PyBufferFlags { - PyBufferFlags(ffi::PyBUF_CONTIG_RO) + pub const fn contig_ro() -> Self { + PyBufferFlags(ffi::PyBUF_CONTIG_RO, PhantomData) } } /// A typed form of [`PyUntypedBufferView`]. Not constructible directly — use /// [`PyBufferView::with()`] or [`PyBufferView::with_flags()`]. #[repr(transparent)] -pub struct PyBufferView>( - PyUntypedBufferView, - PhantomData<[T]>, -); +pub struct PyBufferView< + T, + Flags: PyBufferFlagsType = FlagsImpl, +>(PyUntypedBufferView, PhantomData<[T]>); /// Stack-allocated untyped buffer view. /// @@ -1060,7 +1085,9 @@ pub struct PyBufferView { +pub struct PyUntypedBufferView< + Flags: PyBufferFlagsType = FlagsImpl, +> { raw: ffi::Py_buffer, _flags: PhantomData, } @@ -1132,7 +1159,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. @@ -1145,19 +1172,16 @@ impl< /// Attempt to interpret this untyped view as containing elements of type `T`. pub fn as_typed( &self, - ) -> PyResult< - &PyBufferView>, - > { + ) -> PyResult<&PyBufferView>> + { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> Ok(unsafe { NonNull::from(self) - .cast::< - PyBufferView< - T, - PyBufferFlags, - >, - >() + .cast::, + >>() .as_ref() }) } @@ -1173,7 +1197,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// @@ -1193,7 +1217,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the strides array. /// @@ -1212,7 +1236,7 @@ impl< const STRIDE: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the suboffsets array. /// @@ -1230,7 +1254,7 @@ impl< // SIMPLE and WRITABLE requests guarantee the implicit "B" format. impl - PyUntypedBufferView> + PyUntypedBufferView> { /// Returns the format string for a simple byte buffer, which is always `"B"`. #[inline] @@ -1278,11 +1302,9 @@ impl PyUntypedBufferView { R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags>, f: impl FnOnce( - &PyUntypedBufferView< - PyBufferFlags, - >, + &PyUntypedBufferView>, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1334,9 +1356,9 @@ impl PyBufferView { R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags, + flags: PyBufferFlags>, f: impl FnOnce( - &PyBufferView>, + &PyBufferView>, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1345,12 +1367,11 @@ impl PyBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = PyUntypedBufferView::< - PyBufferFlags, - > { - raw: unsafe { raw.assume_init() }, - _flags: PhantomData, - }; + let view = + PyUntypedBufferView::> { + raw: unsafe { raw.assume_init() }, + _flags: PhantomData, + }; view.as_typed::().map(f) } @@ -1394,7 +1415,7 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1409,7 +1430,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const INDIRECT: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1426,7 +1447,7 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1441,7 +1462,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const INDIRECT: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. @@ -2086,7 +2107,7 @@ mod tests { const WRITABLE: bool, const CONTIGUITY: u8, >( - _: PyBufferFlags, + _: PyBufferFlags>, ) { } @@ -2097,7 +2118,7 @@ mod tests { const WRITABLE: bool, const CONTIGUITY: u8, >( - _: PyBufferFlags, + _: PyBufferFlags>, ) { } From 459b492a0b7b6cbb34419a2bbd6f8578fd342264 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Tue, 14 Apr 2026 23:43:22 +0800 Subject: [PATCH 16/25] refactor: apply trivial suggestions --- src/buffer.rs | 351 +++++++++++++++++++++++++++----------------------- 1 file changed, 191 insertions(+), 160 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 24ea8f6ec34..6a724d277ab 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -846,14 +846,25 @@ const CONTIGUITY_C: u8 = PyBufferContiguity::C as u8; const CONTIGUITY_F: u8 = PyBufferContiguity::F as u8; const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; -/// Type-safe buffer request flags. The state parameter is intentionally hidden +/// Type-safe buffer request. The state parameter is intentionally hidden /// behind this wrapper so the internal encoding can evolve. -pub struct PyBufferFlags< - Flags: PyBufferFlagsType = FlagsImpl, +/// +/// The requested flags constrain what exporters are allowed to return. For example, +/// without shape information, only 1-dimensional buffers are permitted, and accessors +/// for unrequested metadata are unavailable on the typed view. +pub struct PyBufferRequest< + Flags: PyBufferRequestType = RequestFlags< + false, + false, + false, + false, + false, + CONTIGUITY_UNDEFINED, + >, >(c_int, PhantomData); -mod py_buffer_flags_impl { - pub struct PyBufferFlagsImpl< +mod py_buffer_flags { + pub struct PyBufferFlags< const FORMAT: bool, const SHAPE: bool, const STRIDE: bool, @@ -870,15 +881,15 @@ mod py_buffer_flags_impl { const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > Sealed for PyBufferFlagsImpl + > Sealed for PyBufferFlags { } } -use self::py_buffer_flags_impl::PyBufferFlagsImpl as FlagsImpl; +use self::py_buffer_flags::PyBufferFlags as RequestFlags; -/// Trait implemented by all hidden [`PyBufferFlags`] states. -pub trait PyBufferFlagsType: py_buffer_flags_impl::Sealed { +/// Trait implemented by all hidden [`PyBufferRequest`] states. +pub trait PyBufferRequestType: py_buffer_flags::Sealed { /// The contiguity requirement encoded by these flags. const CONTIGUITY: u8; @@ -893,7 +904,8 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY_REQ: u8, - > PyBufferFlagsType for FlagsImpl + > PyBufferRequestType + for RequestFlags { const CONTIGUITY: u8 = CONTIGUITY_REQ; const WRITABLE: bool = WRITABLE; @@ -905,13 +917,13 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags> + > PyBufferRequest> { /// Request format information. pub const fn format( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_FORMAT, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_FORMAT, PhantomData) } } @@ -921,13 +933,13 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags> + > PyBufferRequest> { /// Request shape information. pub const fn nd( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_ND, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_ND, PhantomData) } } @@ -937,24 +949,25 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyBufferFlags> + > PyBufferRequest> { /// Request strides information. Implies shape. pub const fn strides( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_STRIDES, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_STRIDES, PhantomData) } } impl - PyBufferFlags> + PyBufferRequest> { /// Request suboffsets (indirect). Implies shape and strides. pub const fn indirect( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_INDIRECT, PhantomData) + ) -> PyBufferRequest> + { + PyBufferRequest(self.0 | ffi::PyBUF_INDIRECT, PhantomData) } } @@ -964,13 +977,13 @@ impl< const STRIDE: bool, const INDIRECT: bool, const CONTIGUITY: u8, - > PyBufferFlags> + > PyBufferRequest> { /// Request a writable buffer. pub const fn writable( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_WRITABLE, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_WRITABLE, PhantomData) } } @@ -980,20 +993,21 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferFlags> + > + PyBufferRequest> { /// Require C-contiguous layout. Implies shape and strides. pub const fn c_contiguous( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) } /// Require Fortran-contiguous layout. Implies shape and strides. pub const fn f_contiguous( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) } /// Require contiguous layout (C or Fortran). Implies shape and strides. @@ -1002,71 +1016,65 @@ impl< /// so this does not unlock non-Option slice accessors. pub const fn any_contiguous( self, - ) -> PyBufferFlags> { - PyBufferFlags(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) + ) -> PyBufferRequest> { + PyBufferRequest(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) } } -impl PyBufferFlags> { +impl PyBufferRequest { /// Create a base buffer request. Chain builder methods to add flags. - pub const fn simple() -> Self { - PyBufferFlags(ffi::PyBUF_SIMPLE, PhantomData) + pub const fn simple( + ) -> PyBufferRequest> + { + PyBufferRequest(ffi::PyBUF_SIMPLE, PhantomData) } -} -impl PyBufferFlags> { /// Create a writable request for all buffer information including suboffsets. - pub const fn full() -> Self { - PyBufferFlags(ffi::PyBUF_FULL, PhantomData) + pub const fn full( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_FULL, PhantomData) } -} -impl PyBufferFlags> { /// Create a read-only request for all buffer information including suboffsets. - pub const fn full_ro() -> Self { - PyBufferFlags(ffi::PyBUF_FULL_RO, PhantomData) + pub const fn full_ro( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_FULL_RO, PhantomData) } -} -impl PyBufferFlags> { /// Create a writable request for format, shape, and strides. - pub const fn records() -> Self { - PyBufferFlags(ffi::PyBUF_RECORDS, PhantomData) + pub const fn records( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_RECORDS, PhantomData) } -} -impl PyBufferFlags> { /// Create a read-only request for format, shape, and strides. - pub const fn records_ro() -> Self { - PyBufferFlags(ffi::PyBUF_RECORDS_RO, PhantomData) + pub const fn records_ro( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_RECORDS_RO, PhantomData) } -} -impl PyBufferFlags> { /// Create a writable request for shape and strides. - pub const fn strided() -> Self { - PyBufferFlags(ffi::PyBUF_STRIDED, PhantomData) + pub const fn strided( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_STRIDED, PhantomData) } -} -impl PyBufferFlags> { /// Create a read-only request for shape and strides. - pub const fn strided_ro() -> Self { - PyBufferFlags(ffi::PyBUF_STRIDED_RO, PhantomData) + pub const fn strided_ro( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_STRIDED_RO, PhantomData) } -} -impl PyBufferFlags> { /// Create a writable C-contiguous request. - pub const fn contig() -> Self { - PyBufferFlags(ffi::PyBUF_CONTIG, PhantomData) + pub const fn contig( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_CONTIG, PhantomData) } -} -impl PyBufferFlags> { /// Create a read-only C-contiguous request. - pub const fn contig_ro() -> Self { - PyBufferFlags(ffi::PyBUF_CONTIG_RO, PhantomData) + pub const fn contig_ro( + ) -> PyBufferRequest> { + PyBufferRequest(ffi::PyBUF_CONTIG_RO, PhantomData) } } @@ -1075,7 +1083,7 @@ impl PyBufferFlags> { #[repr(transparent)] pub struct PyBufferView< T, - Flags: PyBufferFlagsType = FlagsImpl, + Flags: PyBufferRequestType = RequestFlags, >(PyUntypedBufferView, PhantomData<[T]>); /// Stack-allocated untyped buffer view. @@ -1083,16 +1091,23 @@ pub struct PyBufferView< /// Unlike [`PyUntypedBuffer`] which heap-allocates, this places the `Py_buffer` on the /// stack. The scoped closure API ensures the buffer cannot be moved. /// -/// Use [`with_flags()`](Self::with_flags) with a [`PyBufferFlags`] value to acquire a view. +/// Use [`with_flags()`](Self::with_flags) with a [`PyBufferRequest`] value to acquire a view. /// The available accessors depend on the flags used. pub struct PyUntypedBufferView< - Flags: PyBufferFlagsType = FlagsImpl, + Flags: PyBufferRequestType = RequestFlags< + false, + false, + false, + false, + false, + CONTIGUITY_UNDEFINED, + >, > { raw: ffi::Py_buffer, _flags: PhantomData, } -impl PyUntypedBufferView { +impl PyUntypedBufferView { /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { @@ -1159,7 +1174,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. @@ -1172,7 +1187,7 @@ impl< /// Attempt to interpret this untyped view as containing elements of type `T`. pub fn as_typed( &self, - ) -> PyResult<&PyBufferView>> + ) -> PyResult<&PyBufferView>> { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> @@ -1180,7 +1195,7 @@ impl< NonNull::from(self) .cast::, + RequestFlags, >>() .as_ref() }) @@ -1197,7 +1212,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// @@ -1217,7 +1232,7 @@ impl< const INDIRECT: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the strides array. /// @@ -1236,7 +1251,7 @@ impl< const STRIDE: bool, const WRITABLE: bool, const CONTIGUITY: u8, - > PyUntypedBufferView> + > PyUntypedBufferView> { /// Returns the suboffsets array. /// @@ -1254,7 +1269,7 @@ impl< // SIMPLE and WRITABLE requests guarantee the implicit "B" format. impl - PyUntypedBufferView> + PyUntypedBufferView> { /// Returns the format string for a simple byte buffer, which is always `"B"`. #[inline] @@ -1290,8 +1305,11 @@ impl PyUntypedBufferView { /// Acquire a buffer view with the given flags, /// pass it to `f`, then release the buffer. /// - /// Use [`PyBufferFlags::simple()`] or one of the compound-request constructors such as - /// [`PyBufferFlags::full_ro()`] to acquire a view. + /// Use [`PyBufferRequest::simple()`] or one of the compound-request constructors such as + /// [`PyBufferRequest::full_ro()`] to acquire a view. + /// + /// The requested flags constrain what exporters may return. For example, without shape + /// information only 1-dimensional buffers are permitted. pub fn with_flags< const FORMAT: bool, const SHAPE: bool, @@ -1302,9 +1320,11 @@ impl PyUntypedBufferView { R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags>, + flags: PyBufferRequest>, f: impl FnOnce( - &PyUntypedBufferView>, + &PyUntypedBufferView< + RequestFlags, + >, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1322,30 +1342,32 @@ impl PyUntypedBufferView { } } -impl Drop for PyUntypedBufferView { +impl Drop for PyUntypedBufferView { fn drop(&mut self) { unsafe { ffi::PyBuffer_Release(&mut self.raw) } } } -impl Debug for PyUntypedBufferView { +impl Debug for PyUntypedBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { debug_buffer("PyUntypedBufferView", &self.raw, f) } } impl PyBufferView { - /// Acquire a typed buffer view with `PyBufferFlags::full_ro()` flags, + /// Acquire a typed buffer view with `PyBufferRequest::full_ro()` flags, /// validating that the buffer format is compatible with `T`. pub fn with(obj: &Bound<'_, PyAny>, f: impl FnOnce(&PyBufferView) -> R) -> PyResult { - PyUntypedBufferView::with_flags(obj, PyBufferFlags::full_ro(), |view| { + PyUntypedBufferView::with_flags(obj, PyBufferRequest::full_ro(), |view| { view.as_typed::().map(f) })? } /// Acquire a typed buffer view with the given flags. /// - /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. + /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. As with + /// [`PyUntypedBufferView::with_flags`], the requested flags also constrain what exporters + /// may return. pub fn with_flags< const FORMAT: bool, const SHAPE: bool, @@ -1356,9 +1378,9 @@ impl PyBufferView { R, >( obj: &Bound<'_, PyAny>, - flags: PyBufferFlags>, + flags: PyBufferRequest>, f: impl FnOnce( - &PyBufferView>, + &PyBufferView>, ) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1367,17 +1389,18 @@ impl PyBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = - PyUntypedBufferView::> { - raw: unsafe { raw.assume_init() }, - _flags: PhantomData, - }; + let view = PyUntypedBufferView::< + RequestFlags, + > { + raw: unsafe { raw.assume_init() }, + _flags: PhantomData, + }; view.as_typed::().map(f) } } -impl PyBufferView { +impl PyBufferView { /// Gets the buffer memory as a slice. /// /// Returns `None` if the buffer is not C-contiguous. @@ -1415,7 +1438,7 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1430,7 +1453,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const INDIRECT: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1447,7 +1470,7 @@ impl< const STRIDE: bool, const INDIRECT: bool, const WRITABLE: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1462,7 +1485,7 @@ impl< const SHAPE: bool, const STRIDE: bool, const INDIRECT: bool, - > PyBufferView> + > PyBufferView> { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. @@ -1471,7 +1494,7 @@ impl< } } -impl std::ops::Deref for PyBufferView { +impl std::ops::Deref for PyBufferView { type Target = PyUntypedBufferView; fn deref(&self) -> &Self::Target { @@ -1479,7 +1502,7 @@ impl std::ops::Deref for PyBufferView { } } -impl Debug for PyBufferView { +impl Debug for PyBufferView { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { debug_buffer("PyBufferView", &self.0.raw, f) } @@ -1906,14 +1929,14 @@ mod tests { fn test_untyped_buffer_view() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::full_ro(), |view| { assert!(!view.buf_ptr().is_null()); assert_eq!(view.len_bytes(), 5); assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), 5); assert!(view.readonly()); assert_eq!(view.dimensions(), 1); - // with_flags() uses PyBufferFlags::full_ro() — all Known, direct return types + // with_flags() uses PyBufferRequest::full_ro() — all Known, direct return types assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -1933,7 +1956,7 @@ mod tests { PyBufferView::::with(&bytes, |view| { assert_eq!(view.dimensions(), 1); assert_eq!(view.item_count(), 5); - // PyBufferView::with uses PyBufferFlags::full_ro() — all Known + // PyBufferView::with uses PyBufferRequest::full_ro() — all Known assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert!(view.suboffsets().is_none()); @@ -1984,33 +2007,33 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.len_bytes(), 5); assert!(view.readonly()); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().nd(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().nd(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.shape(), [5]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().strides(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().strides(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().indirect(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().indirect(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); assert!(view.suboffsets().is_none()); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().format(), |view| { assert_eq!(view.item_count(), 5); assert_eq!(view.format().to_str().unwrap(), "B"); }) @@ -2027,7 +2050,7 @@ mod tests { .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); - PyBufferView::::with_flags(&array, PyBufferFlags::simple().nd(), |view| { + PyBufferView::::with_flags(&array, PyBufferRequest::simple().nd(), |view| { assert_eq!(view.item_count(), 4); assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); @@ -2049,7 +2072,7 @@ mod tests { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); let result = - PyBufferView::::with_flags(&bytes, PyBufferFlags::simple().nd(), |_view| {}); + PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().nd(), |_view| {}); assert!(result.is_err()); }); } @@ -2066,7 +2089,7 @@ mod tests { // C_CONTIGUOUS: guaranteed contiguous readonly access (no Option) PyBufferView::::with_flags( &array, - PyBufferFlags::simple().c_contiguous(), + PyBufferRequest::simple().c_contiguous(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice.len(), 3); @@ -2093,7 +2116,7 @@ mod tests { Python::attach(|py| { let list = crate::types::PyList::empty(py); let result = - PyUntypedBufferView::with_flags(&list, PyBufferFlags::full_ro(), |_view| {}); + PyUntypedBufferView::with_flags(&list, PyBufferRequest::full_ro(), |_view| {}); assert!(result.is_err()); }); } @@ -2107,7 +2130,7 @@ mod tests { const WRITABLE: bool, const CONTIGUITY: u8, >( - _: PyBufferFlags>, + _: PyBufferRequest>, ) { } @@ -2118,19 +2141,19 @@ mod tests { const WRITABLE: bool, const CONTIGUITY: u8, >( - _: PyBufferFlags>, + _: PyBufferRequest>, ) { } - assert_direct(PyBufferFlags::simple()); - assert_direct(PyBufferFlags::records_ro()); - assert_direct(PyBufferFlags::strided_ro()); - assert_direct(PyBufferFlags::contig_ro()); - assert_indirect(PyBufferFlags::simple().indirect()); - assert_indirect(PyBufferFlags::full_ro()); - assert_indirect(PyBufferFlags::full()); - assert_direct(PyBufferFlags::full_ro().c_contiguous()); - assert_direct(PyBufferFlags::full().c_contiguous()); + assert_direct(PyBufferRequest::simple()); + assert_direct(PyBufferRequest::records_ro()); + assert_direct(PyBufferRequest::strided_ro()); + assert_direct(PyBufferRequest::contig_ro()); + assert_indirect(PyBufferRequest::simple().indirect()); + assert_indirect(PyBufferRequest::full_ro()); + assert_indirect(PyBufferRequest::full()); + assert_direct(PyBufferRequest::full_ro().c_contiguous()); + assert_direct(PyBufferRequest::full().c_contiguous()); Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); @@ -2141,35 +2164,35 @@ mod tests { .unwrap(); // Primitive builders - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().format(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().nd(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().nd(), |view| { assert_eq!(view.shape(), [5]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().strides(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().strides(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple().indirect(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple().indirect(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); assert!(view.suboffsets().is_none()); }) .unwrap(); - PyUntypedBufferView::with_flags(&array, PyBufferFlags::simple().writable(), |view| { + PyUntypedBufferView::with_flags(&array, PyBufferRequest::simple().writable(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert!(!view.readonly()); }) @@ -2177,7 +2200,7 @@ mod tests { PyUntypedBufferView::with_flags( &array, - PyBufferFlags::simple().writable().nd(), + PyBufferRequest::simple().writable().nd(), |view| { assert_eq!(view.shape(), [4]); assert!(!view.readonly()); @@ -2188,7 +2211,7 @@ mod tests { // Chained primitive builders PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().nd().format(), + PyBufferRequest::simple().nd().format(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.format().to_str().unwrap(), "B"); @@ -2198,7 +2221,7 @@ mod tests { PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().strides().format(), + PyBufferRequest::simple().strides().format(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -2210,7 +2233,7 @@ mod tests { // Contiguity builders PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().c_contiguous(), + PyBufferRequest::simple().c_contiguous(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -2220,7 +2243,7 @@ mod tests { PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().f_contiguous(), + PyBufferRequest::simple().f_contiguous(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -2230,7 +2253,7 @@ mod tests { PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().any_contiguous(), + PyBufferRequest::simple().any_contiguous(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -2239,7 +2262,7 @@ mod tests { .unwrap(); // Compound requests (read-only) - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::full_ro(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); @@ -2247,27 +2270,27 @@ mod tests { }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::records_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::records_ro(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::strided_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::strided_ro(), |view| { assert_eq!(view.shape(), [5]); assert_eq!(view.strides(), [1]); }) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::contig_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::contig_ro(), |view| { assert_eq!(view.shape(), [5]); assert!(view.is_c_contiguous()); }) .unwrap(); // Writable compound requests - PyUntypedBufferView::with_flags(&array, PyBufferFlags::full(), |view| { + PyUntypedBufferView::with_flags(&array, PyBufferRequest::full(), |view| { assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); assert_eq!(view.strides(), [4]); @@ -2276,21 +2299,21 @@ mod tests { }) .unwrap(); - PyUntypedBufferView::with_flags(&array, PyBufferFlags::records(), |view| { + PyUntypedBufferView::with_flags(&array, PyBufferRequest::records(), |view| { assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); assert!(!view.readonly()); }) .unwrap(); - PyUntypedBufferView::with_flags(&array, PyBufferFlags::strided(), |view| { + PyUntypedBufferView::with_flags(&array, PyBufferRequest::strided(), |view| { assert_eq!(view.shape(), [4]); assert_eq!(view.strides(), [4]); assert!(!view.readonly()); }) .unwrap(); - PyUntypedBufferView::with_flags(&array, PyBufferFlags::contig(), |view| { + PyUntypedBufferView::with_flags(&array, PyBufferRequest::contig(), |view| { assert_eq!(view.shape(), [4]); assert!(!view.readonly()); assert!(view.is_c_contiguous()); @@ -2300,7 +2323,7 @@ mod tests { // Compound + contiguity PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::full_ro().c_contiguous(), + PyBufferRequest::full_ro().c_contiguous(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -2309,22 +2332,30 @@ mod tests { ) .unwrap(); - PyUntypedBufferView::with_flags(&array, PyBufferFlags::full().c_contiguous(), |view| { - assert_eq!(view.format().to_str().unwrap(), "f"); - assert!(!view.readonly()); - }) + PyUntypedBufferView::with_flags( + &array, + PyBufferRequest::full().c_contiguous(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "f"); + assert!(!view.readonly()); + }, + ) .unwrap(); - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::strided_ro().format(), |view| { - assert_eq!(view.format().to_str().unwrap(), "B"); - assert_eq!(view.shape(), [5]); - assert_eq!(view.strides(), [1]); - }) + PyUntypedBufferView::with_flags( + &bytes, + PyBufferRequest::strided_ro().format(), + |view| { + assert_eq!(view.format().to_str().unwrap(), "B"); + assert_eq!(view.shape(), [5]); + assert_eq!(view.strides(), [1]); + }, + ) .unwrap(); PyUntypedBufferView::with_flags( &bytes, - PyBufferFlags::simple().c_contiguous().format(), + PyBufferRequest::simple().c_contiguous().format(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.shape(), [5]); @@ -2333,20 +2364,20 @@ mod tests { .unwrap(); // Contiguity builder on typed view - PyBufferView::::with_flags(&bytes, PyBufferFlags::simple().format(), |view| { + PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().format(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); assert_eq!(view.item_count(), 5); }) .unwrap(); - PyBufferView::::with_flags(&array, PyBufferFlags::contig(), |view| { + PyBufferView::::with_flags(&array, PyBufferRequest::contig(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice[0].get(), 1.0); }) .unwrap(); // Writable + contiguity on typed view - PyBufferView::::with_flags(&array, PyBufferFlags::contig(), |view| { + PyBufferView::::with_flags(&array, PyBufferRequest::contig(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice[0].get(), 1.0); let mut_slice = view.as_contiguous_mut_slice(py); @@ -2356,7 +2387,7 @@ mod tests { .unwrap(); // SIMPLE format() returns "B" - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::simple(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::simple(), |view| { assert_eq!(view.format().to_str().unwrap(), "B"); }) .unwrap(); @@ -2369,7 +2400,7 @@ mod tests { let bytes = PyBytes::new(py, b"abcde"); // Debug always uses raw_format/raw_shape/raw_strides (Option in output) - PyUntypedBufferView::with_flags(&bytes, PyBufferFlags::full_ro(), |view| { + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::full_ro(), |view| { let expected = format!( concat!( "PyUntypedBufferView {{ buf: {:?}, obj: {:?}, ", From 8ff899e967582841b0972437fb2f24de79908a5f Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Fri, 17 Apr 2026 04:33:47 +0800 Subject: [PATCH 17/25] refactor: add diagnostic traits --- src/buffer.rs | 515 +++++++++++------- tests/ui/invalid_buffer_flags.rs | 13 + tests/ui/invalid_buffer_flags.stderr | 17 + tests/ui/invalid_buffer_flags_contiguity.rs | 5 + .../ui/invalid_buffer_flags_contiguity.stderr | 19 + .../invalid_buffer_flags_duplicate_format.rs | 5 + ...valid_buffer_flags_duplicate_format.stderr | 19 + tests/ui/invalid_buffer_flags_indirect.rs | 5 + tests/ui/invalid_buffer_flags_indirect.stderr | 19 + 9 files changed, 435 insertions(+), 182 deletions(-) create mode 100644 tests/ui/invalid_buffer_flags.rs create mode 100644 tests/ui/invalid_buffer_flags.stderr create mode 100644 tests/ui/invalid_buffer_flags_contiguity.rs create mode 100644 tests/ui/invalid_buffer_flags_contiguity.stderr create mode 100644 tests/ui/invalid_buffer_flags_duplicate_format.rs create mode 100644 tests/ui/invalid_buffer_flags_duplicate_format.stderr create mode 100644 tests/ui/invalid_buffer_flags_indirect.rs create mode 100644 tests/ui/invalid_buffer_flags_indirect.stderr diff --git a/src/buffer.rs b/src/buffer.rs index 6a724d277ab..2d9c1ea881a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -873,6 +873,223 @@ mod py_buffer_flags { const CONTIGUITY: u8, >; + #[diagnostic::on_unimplemented( + message = "format information has already been requested for this buffer request", + note = "remove the extra `.format()` call" + )] + pub trait CanRequestFormat {} + #[diagnostic::do_not_recommend] + impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > CanRequestFormat for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "shape information has already been requested for this buffer request", + note = "remove the extra `.nd()` call" + )] + pub trait CanRequestShape {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > CanRequestShape for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "stride information has already been requested for this buffer request", + note = "remove the extra `.strides()` call" + )] + pub trait CanRequestStrides {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > CanRequestStrides + for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "suboffsets can only be requested on a direct unconstrained buffer request", + note = "call `.indirect()` before any contiguity builder, and only once" + )] + pub trait CanRequestIndirect {} + #[diagnostic::do_not_recommend] + impl + CanRequestIndirect + for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "writability has already been requested for this buffer request", + note = "remove the extra `.writable()` call" + )] + pub trait CanRequestWritable {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const CONTIGUITY: u8, + > CanRequestWritable for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "contiguity has already been constrained for this buffer request", + note = "only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used" + )] + pub trait CanRequestContiguity {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + > CanRequestContiguity + for PyBufferFlags< + FORMAT, + SHAPE, + STRIDE, + INDIRECT, + WRITABLE, + { super::CONTIGUITY_UNDEFINED }, + > + { + } + + pub trait GuaranteesWritable {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const CONTIGUITY: u8, + > GuaranteesWritable for PyBufferFlags + { + } + + pub trait GuaranteesCContiguous {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + > GuaranteesCContiguous + for PyBufferFlags + { + } + + pub trait GuaranteesFContiguous {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + > GuaranteesFContiguous + for PyBufferFlags + { + } + + /// Marker trait for buffer flags which have requested format information. + #[diagnostic::on_unimplemented( + message = "format information is not available with the requested buffer flags", + note = "use `.format()` when building a buffer request to request format information", + note = "`PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format" + )] + pub trait IncludesFormat { + const ASSUME_U8: bool; + } + + #[diagnostic::do_not_recommend] + impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > IncludesFormat for PyBufferFlags + { + const ASSUME_U8: bool = false; + } + + // Simple (maybe writable) buffers also have an implied u8 format. + #[diagnostic::do_not_recommend] + impl IncludesFormat + for PyBufferFlags + { + const ASSUME_U8: bool = true; + } + + #[diagnostic::on_unimplemented( + message = "shape information is not available with the requested buffer flags", + note = "use `.nd()` when building a buffer request to request shape information" + )] + pub trait IncludesShape {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > IncludesShape for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "strides information is not available with the requested buffer flags", + note = "use `.strides()` when building a buffer request to request stride information" + )] + pub trait IncludesStrides {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > IncludesStrides for PyBufferFlags + { + } + + #[diagnostic::on_unimplemented( + message = "suboffsets information is not available with the requested buffer flags", + note = "use `.indirect()` when building a buffer request to request suboffset information" + )] + pub trait IncludesSuboffsets {} + #[diagnostic::do_not_recommend] + impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, + > IncludesSuboffsets for PyBufferFlags + { + } + pub trait Sealed {} impl< const FORMAT: bool, @@ -895,6 +1112,35 @@ pub trait PyBufferRequestType: py_buffer_flags::Sealed { /// Whether these flags require a writable buffer. const WRITABLE: bool; + + /// The state after requesting format information. + type WithFormat: PyBufferRequestType + py_buffer_flags::IncludesFormat; + + /// The state after requesting shape information. + type WithShape: PyBufferRequestType + py_buffer_flags::IncludesShape; + + /// The state after requesting strides information. + type WithStrides: PyBufferRequestType + + py_buffer_flags::IncludesShape + + py_buffer_flags::IncludesStrides; + + /// The state after requesting indirect / suboffset information. + type WithIndirect: PyBufferRequestType + + py_buffer_flags::IncludesShape + + py_buffer_flags::IncludesStrides + + py_buffer_flags::IncludesSuboffsets; + + /// The state after requesting writability. + type WithWritable: PyBufferRequestType; + + /// The state after requesting C contiguity. + type WithCContiguous: PyBufferRequestType; + + /// The state after requesting Fortran contiguity. + type WithFContiguous: PyBufferRequestType; + + /// The state after requesting either C or Fortran contiguity. + type WithAnyContiguous: PyBufferRequestType; } impl< @@ -909,104 +1155,78 @@ impl< { const CONTIGUITY: u8 = CONTIGUITY_REQ; const WRITABLE: bool = WRITABLE; + + type WithFormat = RequestFlags; + type WithShape = RequestFlags; + type WithStrides = RequestFlags; + type WithIndirect = RequestFlags; + type WithWritable = RequestFlags; + type WithCContiguous = RequestFlags; + type WithFContiguous = RequestFlags; + type WithAnyContiguous = RequestFlags; } -impl< - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestFormat, { /// Request format information. - pub const fn format( - self, - ) -> PyBufferRequest> { + pub const fn format(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_FORMAT, PhantomData) } } -impl< - const FORMAT: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestShape, { /// Request shape information. - pub const fn nd( - self, - ) -> PyBufferRequest> { + pub const fn nd(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_ND, PhantomData) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestStrides, { /// Request strides information. Implies shape. - pub const fn strides( - self, - ) -> PyBufferRequest> { + pub const fn strides(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_STRIDES, PhantomData) } } -impl - PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestIndirect, { /// Request suboffsets (indirect). Implies shape and strides. - pub const fn indirect( - self, - ) -> PyBufferRequest> - { + pub const fn indirect(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_INDIRECT, PhantomData) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const CONTIGUITY: u8, - > PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestWritable, { /// Request a writable buffer. - pub const fn writable( - self, - ) -> PyBufferRequest> { + pub const fn writable(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_WRITABLE, PhantomData) } } -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > - PyBufferRequest> +impl PyBufferRequest +where + Flags: PyBufferRequestType + py_buffer_flags::CanRequestContiguity, { /// Require C-contiguous layout. Implies shape and strides. - pub const fn c_contiguous( - self, - ) -> PyBufferRequest> { + pub const fn c_contiguous(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) } /// Require Fortran-contiguous layout. Implies shape and strides. - pub const fn f_contiguous( - self, - ) -> PyBufferRequest> { + pub const fn f_contiguous(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) } @@ -1014,9 +1234,7 @@ impl< /// /// The specific contiguity order is not known at compile time, /// so this does not unlock non-Option slice accessors. - pub const fn any_contiguous( - self, - ) -> PyBufferRequest> { + pub const fn any_contiguous(self) -> PyBufferRequest { PyBufferRequest(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) } } @@ -1168,97 +1386,79 @@ impl PyUntypedBufferView { } } -impl< - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyUntypedBufferView> -{ +impl PyUntypedBufferView { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. #[inline] - pub fn format(&self) -> &CStr { + pub fn format(&self) -> &CStr + where + Flags: py_buffer_flags::IncludesFormat, + { + if Flags::ASSUME_U8 { + return ffi::c_str!("B"); + } + debug_assert!(!self.raw.format.is_null()); unsafe { CStr::from_ptr(self.raw.format) } } /// Attempt to interpret this untyped view as containing elements of type `T`. - pub fn as_typed( - &self, - ) -> PyResult<&PyBufferView>> + pub fn as_typed(&self) -> PyResult<&PyBufferView> + where + Flags: py_buffer_flags::IncludesFormat, { self.ensure_compatible_with::()?; // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> Ok(unsafe { NonNull::from(self) - .cast::, - >>() + .cast::>() .as_ref() }) } - fn ensure_compatible_with(&self) -> PyResult<()> { + fn ensure_compatible_with(&self) -> PyResult<()> + where + Flags: py_buffer_flags::IncludesFormat, + { check_buffer_compatibility::(self.raw.buf, self.item_size(), self.format()) } -} -impl< - const FORMAT: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyUntypedBufferView> -{ /// Returns the shape array. `shape[i]` is the length of dimension `i`. /// /// Despite Python using an array of signed integers, the values are guaranteed to be /// non-negative. However, dimensions of length 0 are possible and might need special /// attention. #[inline] - pub fn shape(&self) -> &[usize] { + pub fn shape(&self) -> &[usize] + where + Flags: py_buffer_flags::IncludesShape, + { debug_assert!(!self.raw.shape.is_null()); unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) } } -} -impl< - const FORMAT: bool, - const SHAPE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyUntypedBufferView> -{ /// Returns the strides array. /// /// Stride values can be any integer. For regular arrays, strides are usually positive, /// but a consumer MUST be able to handle the case `strides[n] <= 0`. #[inline] - pub fn strides(&self) -> &[isize] { + pub fn strides(&self) -> &[isize] + where + Flags: py_buffer_flags::IncludesStrides, + { debug_assert!(!self.raw.strides.is_null()); unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) } } -} -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > PyUntypedBufferView> -{ /// Returns the suboffsets array. /// /// May return `None` even when suboffsets were requested if the exporter sets /// `suboffsets` to `NULL`. #[inline] - pub fn suboffsets(&self) -> Option<&[isize]> { + pub fn suboffsets(&self) -> Option<&[isize]> + where + Flags: py_buffer_flags::IncludesSuboffsets, + { if self.raw.suboffsets.is_null() { return None; } @@ -1267,17 +1467,6 @@ impl< } } -// SIMPLE and WRITABLE requests guarantee the implicit "B" format. -impl - PyUntypedBufferView> -{ - /// Returns the format string for a simple byte buffer, which is always `"B"`. - #[inline] - pub fn format(&self) -> &CStr { - ffi::c_str!("B") - } -} - /// Check that a buffer is compatible with element type `T`. fn check_buffer_compatibility( buf: *mut c_void, @@ -1310,22 +1499,10 @@ impl PyUntypedBufferView { /// /// The requested flags constrain what exporters may return. For example, without shape /// information only 1-dimensional buffers are permitted. - pub fn with_flags< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - R, - >( + pub fn with_flags( obj: &Bound<'_, PyAny>, - flags: PyBufferRequest>, - f: impl FnOnce( - &PyUntypedBufferView< - RequestFlags, - >, - ) -> R, + flags: PyBufferRequest, + f: impl FnOnce(&PyUntypedBufferView) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1368,20 +1545,10 @@ impl PyBufferView { /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. As with /// [`PyUntypedBufferView::with_flags`], the requested flags also constrain what exporters /// may return. - pub fn with_flags< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - R, - >( + pub fn with_flags( obj: &Bound<'_, PyAny>, - flags: PyBufferRequest>, - f: impl FnOnce( - &PyBufferView>, - ) -> R, + flags: PyBufferRequest, + f: impl FnOnce(&PyBufferView) -> R, ) -> PyResult { let mut raw = mem::MaybeUninit::::uninit(); @@ -1389,9 +1556,7 @@ impl PyBufferView { ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) })?; - let view = PyUntypedBufferView::< - RequestFlags, - > { + let view = PyUntypedBufferView:: { raw: unsafe { raw.assume_init() }, _flags: PhantomData, }; @@ -1431,14 +1596,9 @@ impl PyBufferView { } // C-contiguous guaranteed — no contiguity check needed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > PyBufferView> +impl PyBufferView +where + Flags: PyBufferRequestType + py_buffer_flags::GuaranteesCContiguous, { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1447,13 +1607,11 @@ impl< } // C-contiguous + writable guaranteed — no checks needed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - > PyBufferView> +impl PyBufferView +where + Flags: PyBufferRequestType + + py_buffer_flags::GuaranteesCContiguous + + py_buffer_flags::GuaranteesWritable, { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. @@ -1463,14 +1621,9 @@ impl< } // Fortran-contiguous guaranteed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > PyBufferView> +impl PyBufferView +where + Flags: PyBufferRequestType + py_buffer_flags::GuaranteesFContiguous, { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { @@ -1479,13 +1632,11 @@ impl< } // Fortran-contiguous + writable guaranteed. -impl< - T: Element, - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - > PyBufferView> +impl PyBufferView +where + Flags: PyBufferRequestType + + py_buffer_flags::GuaranteesFContiguous + + py_buffer_flags::GuaranteesWritable, { /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. diff --git a/tests/ui/invalid_buffer_flags.rs b/tests/ui/invalid_buffer_flags.rs new file mode 100644 index 00000000000..6cae9e5c14a --- /dev/null +++ b/tests/ui/invalid_buffer_flags.rs @@ -0,0 +1,13 @@ +use pyo3::buffer::{PyBufferRequest, PyUntypedBufferView}; +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +fn main() { + Python::attach(|py| { + let bytes = PyBytes::new(py, &[1, 2, 3]); + PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::strided(), |view| { + view.format(); + }) + .unwrap(); + }); +} diff --git a/tests/ui/invalid_buffer_flags.stderr b/tests/ui/invalid_buffer_flags.stderr new file mode 100644 index 00000000000..d6dc840151a --- /dev/null +++ b/tests/ui/invalid_buffer_flags.stderr @@ -0,0 +1,17 @@ +error[E0277]: format information is not available with the requested buffer flags + --> tests/ui/invalid_buffer_flags.rs:9:18 + | +9 | view.format(); + | ^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: use `.format()` when building a buffer request to request format information + = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format +note: required by a bound in `PyUntypedBufferView::::format` + --> src/buffer.rs + | + | pub fn format(&self) -> &CStr + | ------ required by a bound in this associated function + | where + | Flags: py_buffer_flags::IncludesFormat, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyUntypedBufferView::::format` diff --git a/tests/ui/invalid_buffer_flags_contiguity.rs b/tests/ui/invalid_buffer_flags_contiguity.rs new file mode 100644 index 00000000000..6dd86f871b2 --- /dev/null +++ b/tests/ui/invalid_buffer_flags_contiguity.rs @@ -0,0 +1,5 @@ +use pyo3::buffer::PyBufferRequest; + +fn main() { + let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); +} diff --git a/tests/ui/invalid_buffer_flags_contiguity.stderr b/tests/ui/invalid_buffer_flags_contiguity.stderr new file mode 100644 index 00000000000..a620306f102 --- /dev/null +++ b/tests/ui/invalid_buffer_flags_contiguity.stderr @@ -0,0 +1,19 @@ +error[E0599]: the method `f_contiguous` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags_contiguity.rs:4:54 + | +4 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + | ^^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestContiguity` + | + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestContiguity` diff --git a/tests/ui/invalid_buffer_flags_duplicate_format.rs b/tests/ui/invalid_buffer_flags_duplicate_format.rs new file mode 100644 index 00000000000..d7fbbc063b4 --- /dev/null +++ b/tests/ui/invalid_buffer_flags_duplicate_format.rs @@ -0,0 +1,5 @@ +use pyo3::buffer::PyBufferRequest; + +fn main() { + let _ = PyBufferRequest::strided().format().format(); +} diff --git a/tests/ui/invalid_buffer_flags_duplicate_format.stderr b/tests/ui/invalid_buffer_flags_duplicate_format.stderr new file mode 100644 index 00000000000..23833a66b5a --- /dev/null +++ b/tests/ui/invalid_buffer_flags_duplicate_format.stderr @@ -0,0 +1,19 @@ +error[E0599]: the method `format` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags_duplicate_format.rs:4:49 + | +4 | let _ = PyBufferRequest::strided().format().format(); + | ^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestFormat` + | + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestFormat` diff --git a/tests/ui/invalid_buffer_flags_indirect.rs b/tests/ui/invalid_buffer_flags_indirect.rs new file mode 100644 index 00000000000..d3a03f62c34 --- /dev/null +++ b/tests/ui/invalid_buffer_flags_indirect.rs @@ -0,0 +1,5 @@ +use pyo3::buffer::PyBufferRequest; + +fn main() { + let _ = PyBufferRequest::simple().indirect().indirect(); +} diff --git a/tests/ui/invalid_buffer_flags_indirect.stderr b/tests/ui/invalid_buffer_flags_indirect.stderr new file mode 100644 index 00000000000..e38e6edf8e9 --- /dev/null +++ b/tests/ui/invalid_buffer_flags_indirect.stderr @@ -0,0 +1,19 @@ +error[E0599]: the method `indirect` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags_indirect.rs:4:50 + | +4 | let _ = PyBufferRequest::simple().indirect().indirect(); + | ^^^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestIndirect` + | + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestIndirect` From 8604ba765d836b84ff22a70c2888cbbc60369d48 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 13 Jul 2026 07:30:46 +0800 Subject: [PATCH 18/25] test: combine ui tests --- src/buffer.rs | 6 +- tests/ui/invalid_buffer_flags.rs | 8 ++ tests/ui/invalid_buffer_flags.stderr | 79 +++++++++++++++++-- tests/ui/invalid_buffer_flags_contiguity.rs | 5 -- .../ui/invalid_buffer_flags_contiguity.stderr | 19 ----- .../invalid_buffer_flags_duplicate_format.rs | 5 -- ...valid_buffer_flags_duplicate_format.stderr | 19 ----- tests/ui/invalid_buffer_flags_indirect.rs | 5 -- tests/ui/invalid_buffer_flags_indirect.stderr | 19 ----- 9 files changed, 83 insertions(+), 82 deletions(-) delete mode 100644 tests/ui/invalid_buffer_flags_contiguity.rs delete mode 100644 tests/ui/invalid_buffer_flags_contiguity.stderr delete mode 100644 tests/ui/invalid_buffer_flags_duplicate_format.rs delete mode 100644 tests/ui/invalid_buffer_flags_duplicate_format.stderr delete mode 100644 tests/ui/invalid_buffer_flags_indirect.rs delete mode 100644 tests/ui/invalid_buffer_flags_indirect.stderr diff --git a/src/buffer.rs b/src/buffer.rs index 2d9c1ea881a..773dd3882b5 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1526,7 +1526,7 @@ impl Drop for PyUntypedBufferView { } impl Debug for PyUntypedBufferView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { debug_buffer("PyUntypedBufferView", &self.raw, f) } } @@ -1645,7 +1645,7 @@ where } } -impl std::ops::Deref for PyBufferView { +impl core::ops::Deref for PyBufferView { type Target = PyUntypedBufferView; fn deref(&self) -> &Self::Target { @@ -1654,7 +1654,7 @@ impl std::ops::Deref for PyBufferView { } impl Debug for PyBufferView { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { debug_buffer("PyBufferView", &self.0.raw, f) } } diff --git a/tests/ui/invalid_buffer_flags.rs b/tests/ui/invalid_buffer_flags.rs index 6cae9e5c14a..c6e3d6ee259 100644 --- a/tests/ui/invalid_buffer_flags.rs +++ b/tests/ui/invalid_buffer_flags.rs @@ -3,10 +3,18 @@ use pyo3::prelude::*; use pyo3::types::PyBytes; fn main() { + let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + //~^ ERROR: the method `f_contiguous` exists + let _ = PyBufferRequest::strided().format().format(); + //~^ ERROR: the method `format` exists + let _ = PyBufferRequest::simple().indirect().indirect(); + //~^ ERROR: the method `indirect` exists + Python::attach(|py| { let bytes = PyBytes::new(py, &[1, 2, 3]); PyUntypedBufferView::with_flags(&bytes, PyBufferRequest::strided(), |view| { view.format(); + //~^ ERROR: format information is not available with the requested buffer flags }) .unwrap(); }); diff --git a/tests/ui/invalid_buffer_flags.stderr b/tests/ui/invalid_buffer_flags.stderr index d6dc840151a..6f6b116b093 100644 --- a/tests/ui/invalid_buffer_flags.stderr +++ b/tests/ui/invalid_buffer_flags.stderr @@ -1,12 +1,72 @@ -error[E0277]: format information is not available with the requested buffer flags - --> tests/ui/invalid_buffer_flags.rs:9:18 +error[E0599]: the method `f_contiguous` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags.rs:6:54 + | +6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + | ^^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestContiguity` | -9 | view.format(); - | ^^^^^^ unsatisfied trait bound + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestContiguity` + +error[E0599]: the method `format` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags.rs:8:49 | - = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: use `.format()` when building a buffer request to request format information - = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format +8 | let _ = PyBufferRequest::strided().format().format(); + | ^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestFormat` + | + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestFormat` + +error[E0599]: the method `indirect` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied + --> tests/ui/invalid_buffer_flags.rs:10:50 + | +10 | let _ = PyBufferRequest::simple().indirect().indirect(); + | ^^^^^^^^ method cannot be called due to unsatisfied trait bounds + | + ::: src/buffer.rs + | + | / pub struct PyBufferFlags< + | | const FORMAT: bool, + | | const SHAPE: bool, + | | const STRIDE: bool, +... | + | | const CONTIGUITY: u8, + | | >; + | |_____- doesn't satisfy `_: CanRequestIndirect` + | + = note: the following trait bounds were not satisfied: + `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestIndirect` + +error[E0277]: format information is not available with the requested buffer flags + --> tests/ui/invalid_buffer_flags.rs:16:18 + | +16 | view.format(); + | ^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: use `.format()` when building a buffer request to request format information + = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format note: required by a bound in `PyUntypedBufferView::::format` --> src/buffer.rs | @@ -15,3 +75,8 @@ note: required by a bound in `PyUntypedBufferView::::format` | where | Flags: py_buffer_flags::IncludesFormat, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyUntypedBufferView::::format` + +error: aborting due to 4 previous errors + +Some errors have detailed explanations: E0277, E0599. +For more information about an error, try `rustc --explain E0277`. diff --git a/tests/ui/invalid_buffer_flags_contiguity.rs b/tests/ui/invalid_buffer_flags_contiguity.rs deleted file mode 100644 index 6dd86f871b2..00000000000 --- a/tests/ui/invalid_buffer_flags_contiguity.rs +++ /dev/null @@ -1,5 +0,0 @@ -use pyo3::buffer::PyBufferRequest; - -fn main() { - let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); -} diff --git a/tests/ui/invalid_buffer_flags_contiguity.stderr b/tests/ui/invalid_buffer_flags_contiguity.stderr deleted file mode 100644 index a620306f102..00000000000 --- a/tests/ui/invalid_buffer_flags_contiguity.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error[E0599]: the method `f_contiguous` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied - --> tests/ui/invalid_buffer_flags_contiguity.rs:4:54 - | -4 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); - | ^^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds - | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestContiguity` - | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestContiguity` diff --git a/tests/ui/invalid_buffer_flags_duplicate_format.rs b/tests/ui/invalid_buffer_flags_duplicate_format.rs deleted file mode 100644 index d7fbbc063b4..00000000000 --- a/tests/ui/invalid_buffer_flags_duplicate_format.rs +++ /dev/null @@ -1,5 +0,0 @@ -use pyo3::buffer::PyBufferRequest; - -fn main() { - let _ = PyBufferRequest::strided().format().format(); -} diff --git a/tests/ui/invalid_buffer_flags_duplicate_format.stderr b/tests/ui/invalid_buffer_flags_duplicate_format.stderr deleted file mode 100644 index 23833a66b5a..00000000000 --- a/tests/ui/invalid_buffer_flags_duplicate_format.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error[E0599]: the method `format` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied - --> tests/ui/invalid_buffer_flags_duplicate_format.rs:4:49 - | -4 | let _ = PyBufferRequest::strided().format().format(); - | ^^^^^^ method cannot be called due to unsatisfied trait bounds - | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestFormat` - | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestFormat` diff --git a/tests/ui/invalid_buffer_flags_indirect.rs b/tests/ui/invalid_buffer_flags_indirect.rs deleted file mode 100644 index d3a03f62c34..00000000000 --- a/tests/ui/invalid_buffer_flags_indirect.rs +++ /dev/null @@ -1,5 +0,0 @@ -use pyo3::buffer::PyBufferRequest; - -fn main() { - let _ = PyBufferRequest::simple().indirect().indirect(); -} diff --git a/tests/ui/invalid_buffer_flags_indirect.stderr b/tests/ui/invalid_buffer_flags_indirect.stderr deleted file mode 100644 index e38e6edf8e9..00000000000 --- a/tests/ui/invalid_buffer_flags_indirect.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error[E0599]: the method `indirect` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied - --> tests/ui/invalid_buffer_flags_indirect.rs:4:50 - | -4 | let _ = PyBufferRequest::simple().indirect().indirect(); - | ^^^^^^^^ method cannot be called due to unsatisfied trait bounds - | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestIndirect` - | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestIndirect` From 897ce69ae6cbafbea4cf03a43e7bef1afa9d3a2d Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 13 Jul 2026 07:41:21 +0800 Subject: [PATCH 19/25] refactor: make method always exist --- src/buffer.rs | 118 +++++++++++---------------- tests/ui/invalid_buffer_flags.rs | 6 +- tests/ui/invalid_buffer_flags.stderr | 86 +++++++++---------- 3 files changed, 90 insertions(+), 120 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 773dd3882b5..cdef8f6986a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1166,67 +1166,60 @@ impl< type WithAnyContiguous = RequestFlags; } -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestFormat, -{ +impl PyBufferRequest { /// Request format information. - pub const fn format(self) -> PyBufferRequest { + pub const fn format(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestFormat, + { PyBufferRequest(self.0 | ffi::PyBUF_FORMAT, PhantomData) } -} -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestShape, -{ /// Request shape information. - pub const fn nd(self) -> PyBufferRequest { + pub const fn nd(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestShape, + { PyBufferRequest(self.0 | ffi::PyBUF_ND, PhantomData) } -} -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestStrides, -{ /// Request strides information. Implies shape. - pub const fn strides(self) -> PyBufferRequest { + pub const fn strides(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestStrides, + { PyBufferRequest(self.0 | ffi::PyBUF_STRIDES, PhantomData) } -} -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestIndirect, -{ /// Request suboffsets (indirect). Implies shape and strides. - pub const fn indirect(self) -> PyBufferRequest { + pub const fn indirect(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestIndirect, + { PyBufferRequest(self.0 | ffi::PyBUF_INDIRECT, PhantomData) } -} -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestWritable, -{ /// Request a writable buffer. - pub const fn writable(self) -> PyBufferRequest { + pub const fn writable(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestWritable, + { PyBufferRequest(self.0 | ffi::PyBUF_WRITABLE, PhantomData) } -} -impl PyBufferRequest -where - Flags: PyBufferRequestType + py_buffer_flags::CanRequestContiguity, -{ /// Require C-contiguous layout. Implies shape and strides. - pub const fn c_contiguous(self) -> PyBufferRequest { + pub const fn c_contiguous(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestContiguity, + { PyBufferRequest(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) } /// Require Fortran-contiguous layout. Implies shape and strides. - pub const fn f_contiguous(self) -> PyBufferRequest { + pub const fn f_contiguous(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestContiguity, + { PyBufferRequest(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) } @@ -1234,7 +1227,10 @@ where /// /// The specific contiguity order is not known at compile time, /// so this does not unlock non-Option slice accessors. - pub const fn any_contiguous(self) -> PyBufferRequest { + pub const fn any_contiguous(self) -> PyBufferRequest + where + Flags: py_buffer_flags::CanRequestContiguity, + { PyBufferRequest(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) } } @@ -1593,54 +1589,38 @@ impl PyBufferView { Some(unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) }) } -} -// C-contiguous guaranteed — no contiguity check needed. -impl PyBufferView -where - Flags: PyBufferRequestType + py_buffer_flags::GuaranteesCContiguous, -{ /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. - pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { + pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + where + Flags: py_buffer_flags::GuaranteesCContiguous, + { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } -} -// C-contiguous + writable guaranteed — no checks needed. -impl PyBufferView -where - Flags: PyBufferRequestType - + py_buffer_flags::GuaranteesCContiguous - + py_buffer_flags::GuaranteesWritable, -{ /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed C-contiguous and writable. - pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] { + pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + where + Flags: py_buffer_flags::GuaranteesCContiguous + py_buffer_flags::GuaranteesWritable, + { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } -} -// Fortran-contiguous guaranteed. -impl PyBufferView -where - Flags: PyBufferRequestType + py_buffer_flags::GuaranteesFContiguous, -{ /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. - pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] { + pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + where + Flags: py_buffer_flags::GuaranteesFContiguous, + { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } -} -// Fortran-contiguous + writable guaranteed. -impl PyBufferView -where - Flags: PyBufferRequestType - + py_buffer_flags::GuaranteesFContiguous - + py_buffer_flags::GuaranteesWritable, -{ /// Gets the buffer memory as a mutable slice. /// The buffer is guaranteed Fortran-contiguous and writable. - pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] { + pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + where + Flags: py_buffer_flags::GuaranteesFContiguous + py_buffer_flags::GuaranteesWritable, + { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } } diff --git a/tests/ui/invalid_buffer_flags.rs b/tests/ui/invalid_buffer_flags.rs index c6e3d6ee259..51b2f1feae6 100644 --- a/tests/ui/invalid_buffer_flags.rs +++ b/tests/ui/invalid_buffer_flags.rs @@ -4,11 +4,11 @@ use pyo3::types::PyBytes; fn main() { let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); - //~^ ERROR: the method `f_contiguous` exists + //~^ ERROR: contiguity has already been constrained for this buffer request let _ = PyBufferRequest::strided().format().format(); - //~^ ERROR: the method `format` exists + //~^ ERROR: format information has already been requested for this buffer request let _ = PyBufferRequest::simple().indirect().indirect(); - //~^ ERROR: the method `indirect` exists + //~^ ERROR: suboffsets can only be requested on a direct unconstrained buffer request Python::attach(|py| { let bytes = PyBytes::new(py, &[1, 2, 3]); diff --git a/tests/ui/invalid_buffer_flags.stderr b/tests/ui/invalid_buffer_flags.stderr index 6f6b116b093..09e5b56752c 100644 --- a/tests/ui/invalid_buffer_flags.stderr +++ b/tests/ui/invalid_buffer_flags.stderr @@ -1,62 +1,53 @@ -error[E0599]: the method `f_contiguous` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied +error[E0277]: contiguity has already been constrained for this buffer request --> tests/ui/invalid_buffer_flags.rs:6:54 | -6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); - | ^^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds +6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + | ^^^^^^^^^^^^ unsatisfied trait bound | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestContiguity` + = help: the trait `buffer::py_buffer_flags::CanRequestContiguity` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used +note: required by a bound in `PyBufferRequest::::f_contiguous` + --> src/buffer.rs | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestContiguity` + | pub const fn f_contiguous(self) -> PyBufferRequest + | ------------ required by a bound in this associated function + | where + | Flags: py_buffer_flags::CanRequestContiguity, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::f_contiguous` -error[E0599]: the method `format` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied +error[E0277]: format information has already been requested for this buffer request --> tests/ui/invalid_buffer_flags.rs:8:49 | -8 | let _ = PyBufferRequest::strided().format().format(); - | ^^^^^^ method cannot be called due to unsatisfied trait bounds +8 | let _ = PyBufferRequest::strided().format().format(); + | ^^^^^^ unsatisfied trait bound | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestFormat` + = help: the trait `buffer::py_buffer_flags::CanRequestFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: remove the extra `.format()` call +note: required by a bound in `PyBufferRequest::::format` + --> src/buffer.rs | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestFormat` + | pub const fn format(self) -> PyBufferRequest + | ------ required by a bound in this associated function + | where + | Flags: py_buffer_flags::CanRequestFormat, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::format` -error[E0599]: the method `indirect` exists for struct `PyBufferRequest>`, but its trait bounds were not satisfied +error[E0277]: suboffsets can only be requested on a direct unconstrained buffer request --> tests/ui/invalid_buffer_flags.rs:10:50 | -10 | let _ = PyBufferRequest::simple().indirect().indirect(); - | ^^^^^^^^ method cannot be called due to unsatisfied trait bounds +10 | let _ = PyBufferRequest::simple().indirect().indirect(); + | ^^^^^^^^ unsatisfied trait bound | - ::: src/buffer.rs - | - | / pub struct PyBufferFlags< - | | const FORMAT: bool, - | | const SHAPE: bool, - | | const STRIDE: bool, -... | - | | const CONTIGUITY: u8, - | | >; - | |_____- doesn't satisfy `_: CanRequestIndirect` - | - = note: the following trait bounds were not satisfied: - `buffer::py_buffer_flags::PyBufferFlags: buffer::py_buffer_flags::CanRequestIndirect` + = help: the trait `buffer::py_buffer_flags::CanRequestIndirect` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: call `.indirect()` before any contiguity builder, and only once +note: required by a bound in `PyBufferRequest::::indirect` + --> src/buffer.rs + | + | pub const fn indirect(self) -> PyBufferRequest + | -------- required by a bound in this associated function + | where + | Flags: py_buffer_flags::CanRequestIndirect, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::indirect` error[E0277]: format information is not available with the requested buffer flags --> tests/ui/invalid_buffer_flags.rs:16:18 @@ -78,5 +69,4 @@ note: required by a bound in `PyUntypedBufferView::::format` error: aborting due to 4 previous errors -Some errors have detailed explanations: E0277, E0599. -For more information about an error, try `rustc --explain E0277`. +For more information about this error, try `rustc --explain E0277`. From b07f899b629169f7b3e1ca1c56e33f8ae4ba85dd Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 13 Jul 2026 07:52:13 +0800 Subject: [PATCH 20/25] feat: expose contiguity as a public enum --- src/buffer.rs | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index cdef8f6986a..26391cc7848 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -833,11 +833,17 @@ impl_element!(isize, SignedInteger); impl_element!(f32, Float); impl_element!(f64, Float); +/// Contiguity constraint encoded by a [`PyBufferRequest`]. #[repr(u8)] -enum PyBufferContiguity { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PyBufferContiguity { + /// No contiguity constraint was requested. Undefined = 0, + /// C-contiguous layout was requested. C = 1, + /// Fortran-contiguous layout was requested. F = 2, + /// Either C- or Fortran-contiguous layout was requested. Any = 3, } @@ -1108,7 +1114,7 @@ use self::py_buffer_flags::PyBufferFlags as RequestFlags; /// Trait implemented by all hidden [`PyBufferRequest`] states. pub trait PyBufferRequestType: py_buffer_flags::Sealed { /// The contiguity requirement encoded by these flags. - const CONTIGUITY: u8; + const CONTIGUITY: PyBufferContiguity; /// Whether these flags require a writable buffer. const WRITABLE: bool; @@ -1153,7 +1159,13 @@ impl< > PyBufferRequestType for RequestFlags { - const CONTIGUITY: u8 = CONTIGUITY_REQ; + const CONTIGUITY: PyBufferContiguity = match CONTIGUITY_REQ { + CONTIGUITY_UNDEFINED => PyBufferContiguity::Undefined, + CONTIGUITY_C => PyBufferContiguity::C, + CONTIGUITY_F => PyBufferContiguity::F, + CONTIGUITY_ANY => PyBufferContiguity::Any, + _ => panic!("invalid buffer contiguity"), + }; const WRITABLE: bool = WRITABLE; type WithFormat = RequestFlags; @@ -1370,14 +1382,14 @@ impl PyUntypedBufferView { /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { - Flags::CONTIGUITY == CONTIGUITY_C + Flags::CONTIGUITY == PyBufferContiguity::C || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { - Flags::CONTIGUITY == CONTIGUITY_F + Flags::CONTIGUITY == PyBufferContiguity::F || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } } } @@ -2254,6 +2266,13 @@ mod tests { #[test] fn test_flag_builders() { + fn assert_contiguity( + _: PyBufferRequest, + expected: PyBufferContiguity, + ) { + assert_eq!(Flags::CONTIGUITY, expected); + } + fn assert_direct< const FORMAT: bool, const SHAPE: bool, @@ -2286,6 +2305,20 @@ mod tests { assert_direct(PyBufferRequest::full_ro().c_contiguous()); assert_direct(PyBufferRequest::full().c_contiguous()); + assert_contiguity(PyBufferRequest::simple(), PyBufferContiguity::Undefined); + assert_contiguity( + PyBufferRequest::simple().c_contiguous(), + PyBufferContiguity::C, + ); + assert_contiguity( + PyBufferRequest::simple().f_contiguous(), + PyBufferContiguity::F, + ); + assert_contiguity( + PyBufferRequest::simple().any_contiguous(), + PyBufferContiguity::Any, + ); + Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); let array = py From 5d95b4a05d6d5adc3c8cad684d1c0ac8b1db77b7 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Mon, 13 Jul 2026 08:14:33 +0800 Subject: [PATCH 21/25] test: update stderr --- src/buffer.rs | 6 +-- tests/ui/invalid_buffer_flags.stderr | 58 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 26391cc7848..08b87a75ec0 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1383,14 +1383,14 @@ impl PyUntypedBufferView { #[inline] pub fn is_c_contiguous(&self) -> bool { Flags::CONTIGUITY == PyBufferContiguity::C - || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as std::ffi::c_char) != 0 } + || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as core::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { Flags::CONTIGUITY == PyBufferContiguity::F - || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as std::ffi::c_char) != 0 } + || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as core::ffi::c_char) != 0 } } } @@ -1481,7 +1481,7 @@ fn check_buffer_compatibility( itemsize: usize, format: &CStr, ) -> PyResult<()> { - let name = std::any::type_name::(); + let name = core::any::type_name::(); if mem::size_of::() != itemsize || !T::is_compatible_format(format) { return Err(PyBufferError::new_err(format!( diff --git a/tests/ui/invalid_buffer_flags.stderr b/tests/ui/invalid_buffer_flags.stderr index 09e5b56752c..200b4896dcd 100644 --- a/tests/ui/invalid_buffer_flags.stderr +++ b/tests/ui/invalid_buffer_flags.stderr @@ -1,11 +1,11 @@ error[E0277]: contiguity has already been constrained for this buffer request - --> tests/ui/invalid_buffer_flags.rs:6:54 - | -6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); - | ^^^^^^^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestContiguity` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used + --> tests/ui/invalid_buffer_flags.rs:6:54 + | + 6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + | ^^^^^^^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::CanRequestContiguity` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used note: required by a bound in `PyBufferRequest::::f_contiguous` --> src/buffer.rs | @@ -16,13 +16,13 @@ note: required by a bound in `PyBufferRequest::::f_contiguous` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::f_contiguous` error[E0277]: format information has already been requested for this buffer request - --> tests/ui/invalid_buffer_flags.rs:8:49 - | -8 | let _ = PyBufferRequest::strided().format().format(); - | ^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: remove the extra `.format()` call + --> tests/ui/invalid_buffer_flags.rs:8:49 + | + 8 | let _ = PyBufferRequest::strided().format().format(); + | ^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::CanRequestFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: remove the extra `.format()` call note: required by a bound in `PyBufferRequest::::format` --> src/buffer.rs | @@ -33,13 +33,13 @@ note: required by a bound in `PyBufferRequest::::format` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::format` error[E0277]: suboffsets can only be requested on a direct unconstrained buffer request - --> tests/ui/invalid_buffer_flags.rs:10:50 - | -10 | let _ = PyBufferRequest::simple().indirect().indirect(); - | ^^^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestIndirect` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: call `.indirect()` before any contiguity builder, and only once + --> tests/ui/invalid_buffer_flags.rs:10:50 + | + 10 | let _ = PyBufferRequest::simple().indirect().indirect(); + | ^^^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::CanRequestIndirect` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: call `.indirect()` before any contiguity builder, and only once note: required by a bound in `PyBufferRequest::::indirect` --> src/buffer.rs | @@ -50,14 +50,14 @@ note: required by a bound in `PyBufferRequest::::indirect` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::indirect` error[E0277]: format information is not available with the requested buffer flags - --> tests/ui/invalid_buffer_flags.rs:16:18 - | -16 | view.format(); - | ^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: use `.format()` when building a buffer request to request format information - = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format + --> tests/ui/invalid_buffer_flags.rs:16:18 + | + 16 | view.format(); + | ^^^^^^ unsatisfied trait bound + | + = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` + = note: use `.format()` when building a buffer request to request format information + = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format note: required by a bound in `PyUntypedBufferView::::format` --> src/buffer.rs | From 47f6cf42e141cce489bfcc198ae42635eb18a5e9 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Fri, 2 Oct 2026 08:39:16 +0800 Subject: [PATCH 22/25] refactor: move flags into separate script --- src/buffer.rs | 699 ++++++++-------------- src/buffer/flags.rs | 314 ++++++++++ tests/test_buffer_protocol.rs | 107 +++- tests/ui/invalid_buffer_flags.rs | 5 +- tests/ui/invalid_buffer_flags.stderr | 93 +-- tests/ui/invalid_buffer_view_slice.rs | 36 ++ tests/ui/invalid_buffer_view_slice.stderr | 99 +++ 7 files changed, 874 insertions(+), 479 deletions(-) create mode 100644 src/buffer/flags.rs create mode 100644 tests/ui/invalid_buffer_view_slice.rs create mode 100644 tests/ui/invalid_buffer_view_slice.stderr diff --git a/src/buffer.rs b/src/buffer.rs index 08b87a75ec0..12b0e98327b 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -35,6 +35,12 @@ use core::ptr::NonNull; use core::{cell, mem, ptr, slice}; use core::{ffi::CStr, fmt::Debug}; +mod flags; + +use self::flags::{ + CONTIGUITY_C, CONTIGUITY_UNDEFINED, PyBufferFlags as RequestFlags, PyBufferRequestType, +}; + /// A typed form of [`PyUntypedBuffer`]. #[repr(transparent)] pub struct PyBuffer(PyUntypedBuffer, PhantomData<[T]>); @@ -847,11 +853,6 @@ pub enum PyBufferContiguity { Any = 3, } -const CONTIGUITY_UNDEFINED: u8 = PyBufferContiguity::Undefined as u8; -const CONTIGUITY_C: u8 = PyBufferContiguity::C as u8; -const CONTIGUITY_F: u8 = PyBufferContiguity::F as u8; -const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; - /// Type-safe buffer request. The state parameter is intentionally hidden /// behind this wrapper so the internal encoding can evolve. /// @@ -859,7 +860,7 @@ const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; /// without shape information, only 1-dimensional buffers are permitted, and accessors /// for unrequested metadata are unavailable on the typed view. pub struct PyBufferRequest< - Flags: PyBufferRequestType = RequestFlags< + Flag: PyBufferRequestType = RequestFlags< false, false, false, @@ -867,370 +868,61 @@ pub struct PyBufferRequest< false, CONTIGUITY_UNDEFINED, >, ->(c_int, PhantomData); - -mod py_buffer_flags { - pub struct PyBufferFlags< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - >; - - #[diagnostic::on_unimplemented( - message = "format information has already been requested for this buffer request", - note = "remove the extra `.format()` call" - )] - pub trait CanRequestFormat {} - #[diagnostic::do_not_recommend] - impl< - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > CanRequestFormat for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "shape information has already been requested for this buffer request", - note = "remove the extra `.nd()` call" - )] - pub trait CanRequestShape {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > CanRequestShape for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "stride information has already been requested for this buffer request", - note = "remove the extra `.strides()` call" - )] - pub trait CanRequestStrides {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > CanRequestStrides - for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "suboffsets can only be requested on a direct unconstrained buffer request", - note = "call `.indirect()` before any contiguity builder, and only once" - )] - pub trait CanRequestIndirect {} - #[diagnostic::do_not_recommend] - impl - CanRequestIndirect - for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "writability has already been requested for this buffer request", - note = "remove the extra `.writable()` call" - )] - pub trait CanRequestWritable {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const CONTIGUITY: u8, - > CanRequestWritable for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "contiguity has already been constrained for this buffer request", - note = "only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used" - )] - pub trait CanRequestContiguity {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > CanRequestContiguity - for PyBufferFlags< - FORMAT, - SHAPE, - STRIDE, - INDIRECT, - WRITABLE, - { super::CONTIGUITY_UNDEFINED }, - > - { - } - - pub trait GuaranteesWritable {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const CONTIGUITY: u8, - > GuaranteesWritable for PyBufferFlags - { - } - - pub trait GuaranteesCContiguous {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > GuaranteesCContiguous - for PyBufferFlags - { - } - - pub trait GuaranteesFContiguous {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - > GuaranteesFContiguous - for PyBufferFlags - { - } - - /// Marker trait for buffer flags which have requested format information. - #[diagnostic::on_unimplemented( - message = "format information is not available with the requested buffer flags", - note = "use `.format()` when building a buffer request to request format information", - note = "`PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format" - )] - pub trait IncludesFormat { - const ASSUME_U8: bool; - } - - #[diagnostic::do_not_recommend] - impl< - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > IncludesFormat for PyBufferFlags - { - const ASSUME_U8: bool = false; - } - - // Simple (maybe writable) buffers also have an implied u8 format. - #[diagnostic::do_not_recommend] - impl IncludesFormat - for PyBufferFlags - { - const ASSUME_U8: bool = true; - } - - #[diagnostic::on_unimplemented( - message = "shape information is not available with the requested buffer flags", - note = "use `.nd()` when building a buffer request to request shape information" - )] - pub trait IncludesShape {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > IncludesShape for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "strides information is not available with the requested buffer flags", - note = "use `.strides()` when building a buffer request to request stride information" - )] - pub trait IncludesStrides {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > IncludesStrides for PyBufferFlags - { - } - - #[diagnostic::on_unimplemented( - message = "suboffsets information is not available with the requested buffer flags", - note = "use `.indirect()` when building a buffer request to request suboffset information" - )] - pub trait IncludesSuboffsets {} - #[diagnostic::do_not_recommend] - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > IncludesSuboffsets for PyBufferFlags - { - } - - pub trait Sealed {} - impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY: u8, - > Sealed for PyBufferFlags - { - } -} - -use self::py_buffer_flags::PyBufferFlags as RequestFlags; - -/// Trait implemented by all hidden [`PyBufferRequest`] states. -pub trait PyBufferRequestType: py_buffer_flags::Sealed { - /// The contiguity requirement encoded by these flags. - const CONTIGUITY: PyBufferContiguity; - - /// Whether these flags require a writable buffer. - const WRITABLE: bool; - - /// The state after requesting format information. - type WithFormat: PyBufferRequestType + py_buffer_flags::IncludesFormat; - - /// The state after requesting shape information. - type WithShape: PyBufferRequestType + py_buffer_flags::IncludesShape; - - /// The state after requesting strides information. - type WithStrides: PyBufferRequestType - + py_buffer_flags::IncludesShape - + py_buffer_flags::IncludesStrides; - - /// The state after requesting indirect / suboffset information. - type WithIndirect: PyBufferRequestType - + py_buffer_flags::IncludesShape - + py_buffer_flags::IncludesStrides - + py_buffer_flags::IncludesSuboffsets; +>(c_int, PhantomData); - /// The state after requesting writability. - type WithWritable: PyBufferRequestType; - - /// The state after requesting C contiguity. - type WithCContiguous: PyBufferRequestType; - - /// The state after requesting Fortran contiguity. - type WithFContiguous: PyBufferRequestType; - - /// The state after requesting either C or Fortran contiguity. - type WithAnyContiguous: PyBufferRequestType; -} - -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const WRITABLE: bool, - const CONTIGUITY_REQ: u8, - > PyBufferRequestType - for RequestFlags -{ - const CONTIGUITY: PyBufferContiguity = match CONTIGUITY_REQ { - CONTIGUITY_UNDEFINED => PyBufferContiguity::Undefined, - CONTIGUITY_C => PyBufferContiguity::C, - CONTIGUITY_F => PyBufferContiguity::F, - CONTIGUITY_ANY => PyBufferContiguity::Any, - _ => panic!("invalid buffer contiguity"), - }; - const WRITABLE: bool = WRITABLE; - - type WithFormat = RequestFlags; - type WithShape = RequestFlags; - type WithStrides = RequestFlags; - type WithIndirect = RequestFlags; - type WithWritable = RequestFlags; - type WithCContiguous = RequestFlags; - type WithFContiguous = RequestFlags; - type WithAnyContiguous = RequestFlags; -} - -impl PyBufferRequest { +impl PyBufferRequest { /// Request format information. - pub const fn format(self) -> PyBufferRequest + pub const fn format(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestFormat, + Flag: flags::CanRequestFormat, { PyBufferRequest(self.0 | ffi::PyBUF_FORMAT, PhantomData) } /// Request shape information. - pub const fn nd(self) -> PyBufferRequest + pub const fn nd(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestShape, + Flag: flags::CanRequestShape, { PyBufferRequest(self.0 | ffi::PyBUF_ND, PhantomData) } /// Request strides information. Implies shape. - pub const fn strides(self) -> PyBufferRequest + pub const fn strides(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestStrides, + Flag: flags::CanRequestStrides, { PyBufferRequest(self.0 | ffi::PyBUF_STRIDES, PhantomData) } /// Request suboffsets (indirect). Implies shape and strides. - pub const fn indirect(self) -> PyBufferRequest + pub const fn indirect(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestIndirect, + Flag: flags::CanRequestIndirect, { PyBufferRequest(self.0 | ffi::PyBUF_INDIRECT, PhantomData) } /// Request a writable buffer. - pub const fn writable(self) -> PyBufferRequest + pub const fn writable(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestWritable, + Flag: flags::CanRequestWritable, { PyBufferRequest(self.0 | ffi::PyBUF_WRITABLE, PhantomData) } /// Require C-contiguous layout. Implies shape and strides. - pub const fn c_contiguous(self) -> PyBufferRequest + pub const fn c_contiguous(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestContiguity, + Flag: flags::CanRequestContiguity, { PyBufferRequest(self.0 | ffi::PyBUF_C_CONTIGUOUS, PhantomData) } /// Require Fortran-contiguous layout. Implies shape and strides. - pub const fn f_contiguous(self) -> PyBufferRequest + pub const fn f_contiguous(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestContiguity, + Flag: flags::CanRequestContiguity, { PyBufferRequest(self.0 | ffi::PyBUF_F_CONTIGUOUS, PhantomData) } @@ -1239,9 +931,9 @@ impl PyBufferRequest { /// /// The specific contiguity order is not known at compile time, /// so this does not unlock non-Option slice accessors. - pub const fn any_contiguous(self) -> PyBufferRequest + pub const fn any_contiguous(self) -> PyBufferRequest where - Flags: py_buffer_flags::CanRequestContiguity, + Flag: flags::CanRequestContiguity, { PyBufferRequest(self.0 | ffi::PyBUF_ANY_CONTIGUOUS, PhantomData) } @@ -1249,57 +941,56 @@ impl PyBufferRequest { impl PyBufferRequest { /// Create a base buffer request. Chain builder methods to add flags. - pub const fn simple( - ) -> PyBufferRequest> - { + pub const fn simple() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_SIMPLE, PhantomData) } /// Create a writable request for all buffer information including suboffsets. - pub const fn full( - ) -> PyBufferRequest> { + pub const fn full() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_FULL, PhantomData) } /// Create a read-only request for all buffer information including suboffsets. - pub const fn full_ro( - ) -> PyBufferRequest> { + pub const fn full_ro() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_FULL_RO, PhantomData) } /// Create a writable request for format, shape, and strides. - pub const fn records( - ) -> PyBufferRequest> { + pub const fn records() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_RECORDS, PhantomData) } /// Create a read-only request for format, shape, and strides. - pub const fn records_ro( - ) -> PyBufferRequest> { + pub const fn records_ro() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_RECORDS_RO, PhantomData) } /// Create a writable request for shape and strides. - pub const fn strided( - ) -> PyBufferRequest> { + pub const fn strided() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_STRIDED, PhantomData) } /// Create a read-only request for shape and strides. - pub const fn strided_ro( - ) -> PyBufferRequest> { + pub const fn strided_ro() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_STRIDED_RO, PhantomData) } /// Create a writable C-contiguous request. - pub const fn contig( - ) -> PyBufferRequest> { + pub const fn contig() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_CONTIG, PhantomData) } /// Create a read-only C-contiguous request. - pub const fn contig_ro( - ) -> PyBufferRequest> { + pub const fn contig_ro() + -> PyBufferRequest> { PyBufferRequest(ffi::PyBUF_CONTIG_RO, PhantomData) } } @@ -1309,8 +1000,8 @@ impl PyBufferRequest { #[repr(transparent)] pub struct PyBufferView< T, - Flags: PyBufferRequestType = RequestFlags, ->(PyUntypedBufferView, PhantomData<[T]>); + Flag: PyBufferRequestType = RequestFlags, +>(PyUntypedBufferView, PhantomData<[T]>); /// Stack-allocated untyped buffer view. /// @@ -1319,8 +1010,9 @@ pub struct PyBufferView< /// /// Use [`with_flags()`](Self::with_flags) with a [`PyBufferRequest`] value to acquire a view. /// The available accessors depend on the flags used. +#[repr(transparent)] pub struct PyUntypedBufferView< - Flags: PyBufferRequestType = RequestFlags< + Flag: PyBufferRequestType = RequestFlags< false, false, false, @@ -1330,10 +1022,11 @@ pub struct PyUntypedBufferView< >, > { raw: ffi::Py_buffer, - _flags: PhantomData, + _flags: PhantomData, + _pin: PhantomPinned, } -impl PyUntypedBufferView { +impl PyUntypedBufferView { /// Gets the pointer to the start of the buffer memory. #[inline] pub fn buf_ptr(&self) -> *mut c_void { @@ -1349,19 +1042,23 @@ impl PyUntypedBufferView { /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { - !Flags::WRITABLE && self.raw.readonly != 0 + !Flag::WRITABLE && self.raw.readonly != 0 } /// Gets the size of a single element, in bytes. #[inline] pub fn item_size(&self) -> usize { - self.raw.itemsize as usize + if const { Flag::IS_SIMPLE } { + 1 + } else { + self.raw.itemsize as usize + } } /// Gets the total number of items. #[inline] pub fn item_count(&self) -> usize { - (self.raw.len as usize) / (self.raw.itemsize as usize) + self.len_bytes() / self.item_size() } /// `item_size() * item_count()`. @@ -1373,7 +1070,7 @@ impl PyUntypedBufferView { /// Gets the number of dimensions. /// - /// May be 0 to indicate a single scalar value. + /// Always at least 1. Scalar buffers are rejected during acquisition. #[inline] pub fn dimensions(&self) -> usize { self.raw.ndim as usize @@ -1382,27 +1079,27 @@ impl PyUntypedBufferView { /// Gets whether the buffer is contiguous in C-style order. #[inline] pub fn is_c_contiguous(&self) -> bool { - Flags::CONTIGUITY == PyBufferContiguity::C + Flag::CONTIGUITY == PyBufferContiguity::C || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'C' as core::ffi::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order. #[inline] pub fn is_fortran_contiguous(&self) -> bool { - Flags::CONTIGUITY == PyBufferContiguity::F + Flag::CONTIGUITY == PyBufferContiguity::F || unsafe { ffi::PyBuffer_IsContiguous(&self.raw, b'F' as core::ffi::c_char) != 0 } } } -impl PyUntypedBufferView { +impl PyUntypedBufferView { /// A [struct module style](https://docs.python.org/3/c-api/buffer.html#c.Py_buffer.format) /// string describing the contents of a single item. #[inline] pub fn format(&self) -> &CStr where - Flags: py_buffer_flags::IncludesFormat, + Flag: flags::IncludesFormat, { - if Flags::ASSUME_U8 { + if Flag::ASSUME_U8 { return ffi::c_str!("B"); } @@ -1411,22 +1108,20 @@ impl PyUntypedBufferView { } /// Attempt to interpret this untyped view as containing elements of type `T`. - pub fn as_typed(&self) -> PyResult<&PyBufferView> + pub fn as_typed(&self) -> PyResult<&PyBufferView> where - Flags: py_buffer_flags::IncludesFormat, + Flag: flags::IncludesFormat, { self.ensure_compatible_with::()?; - // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..> - Ok(unsafe { - NonNull::from(self) - .cast::>() - .as_ref() - }) + // SAFETY: PyBufferView is repr(transparent) around PyUntypedBufferView<..>. + // Validation establishes its element compatibility, non-null data pointer, + // and alignment invariants before any safe slice accessor can be called. + Ok(unsafe { NonNull::from(self).cast::>().as_ref() }) } fn ensure_compatible_with(&self) -> PyResult<()> where - Flags: py_buffer_flags::IncludesFormat, + Flag: flags::IncludesFormat, { check_buffer_compatibility::(self.raw.buf, self.item_size(), self.format()) } @@ -1439,7 +1134,7 @@ impl PyUntypedBufferView { #[inline] pub fn shape(&self) -> &[usize] where - Flags: py_buffer_flags::IncludesShape, + Flag: flags::IncludesShape, { debug_assert!(!self.raw.shape.is_null()); unsafe { slice::from_raw_parts(self.raw.shape.cast(), self.raw.ndim as usize) } @@ -1452,7 +1147,7 @@ impl PyUntypedBufferView { #[inline] pub fn strides(&self) -> &[isize] where - Flags: py_buffer_flags::IncludesStrides, + Flag: flags::IncludesStrides, { debug_assert!(!self.raw.strides.is_null()); unsafe { slice::from_raw_parts(self.raw.strides, self.raw.ndim as usize) } @@ -1465,7 +1160,7 @@ impl PyUntypedBufferView { #[inline] pub fn suboffsets(&self) -> Option<&[isize]> where - Flags: py_buffer_flags::IncludesSuboffsets, + Flag: flags::IncludesSuboffsets, { if self.raw.suboffsets.is_null() { return None; @@ -1489,7 +1184,10 @@ fn check_buffer_compatibility( ))); } - if buf.align_offset(mem::align_of::()) != 0 { + let buf = + NonNull::new(buf).ok_or_else(|| PyBufferError::new_err("buffer data pointer is null"))?; + + if buf.as_ptr().align_offset(mem::align_of::()) != 0 { return Err(PyBufferError::new_err(format!( "buffer contents are insufficiently aligned for {name}" ))); @@ -1507,33 +1205,53 @@ impl PyUntypedBufferView { /// /// The requested flags constrain what exporters may return. For example, without shape /// information only 1-dimensional buffers are permitted. - pub fn with_flags( + pub fn with_flags( obj: &Bound<'_, PyAny>, - flags: PyBufferRequest, - f: impl FnOnce(&PyUntypedBufferView) -> R, + flags: PyBufferRequest, + f: impl FnOnce(&PyUntypedBufferView) -> R, ) -> PyResult { - let mut raw = mem::MaybeUninit::::uninit(); + let mut view = mem::MaybeUninit::>::uninit(); + // SAFETY: PyUntypedBufferView is repr(transparent) around ffi::Py_buffer, + // so the pointer has the layout and alignment expected by PyObject_GetBuffer. err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0) + ffi::PyObject_GetBuffer(obj.as_ptr(), view.as_mut_ptr().cast(), flags.0) })?; - let view = PyUntypedBufferView { - raw: unsafe { raw.assume_init() }, - _flags: PhantomData, - }; + // SAFETY: PyObject_GetBuffer initialized the raw buffer successfully, and + // the zero-sized marker fields need no initialization. Borrow the view + // without moving it, since the exporter may have made it self-referential. + let guard = BufferViewGuard(unsafe { view.assume_init_mut() }); + + if guard.0.raw.ndim == 0 { + return Err(PyBufferError::new_err("scalar buffers are not supported")); + } + + Ok(f(guard.0)) + } +} - Ok(f(&view)) +/// Drops an initialized view in place, including when the callback panics. +/// The view's storage is a MaybeUninit, so it will not be dropped a second time. +struct BufferViewGuard<'a, Flag: PyBufferRequestType>(&'a mut PyUntypedBufferView); + +impl Drop for BufferViewGuard<'_, Flag> { + fn drop(&mut self) { + // SAFETY: The view is initialized and exclusively borrowed by this guard. + // Its MaybeUninit storage will not be accessed again after this drop. + unsafe { ptr::drop_in_place(self.0) } } } -impl Drop for PyUntypedBufferView { +impl Drop for PyUntypedBufferView { fn drop(&mut self) { - unsafe { ffi::PyBuffer_Release(&mut self.raw) } + // Avoid creating a mutable reference to the Unpin raw buffer, which would + // invalidate self-references the exporter may still use during release. + unsafe { ffi::PyBuffer_Release(&raw mut self.raw) } } } -impl Debug for PyUntypedBufferView { +impl Debug for PyUntypedBufferView { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { debug_buffer("PyUntypedBufferView", &self.raw, f) } @@ -1550,30 +1268,22 @@ impl PyBufferView { /// Acquire a typed buffer view with the given flags. /// - /// [`ffi::PyBUF_FORMAT`] is implicitly added for type validation. As with - /// [`PyUntypedBufferView::with_flags`], the requested flags also constrain what exporters - /// may return. - pub fn with_flags( + /// The flags are passed to the exporter as requested. Format information must be + /// available for type validation: [`PyBufferRequest::simple()`] and its writable + /// variant imply unsigned bytes, while other requests must include [`.format()`](PyBufferRequest::format). + pub fn with_flags( obj: &Bound<'_, PyAny>, - flags: PyBufferRequest, - f: impl FnOnce(&PyBufferView) -> R, - ) -> PyResult { - let mut raw = mem::MaybeUninit::::uninit(); - - err::error_on_minusone(obj.py(), unsafe { - ffi::PyObject_GetBuffer(obj.as_ptr(), raw.as_mut_ptr(), flags.0 | ffi::PyBUF_FORMAT) - })?; - - let view = PyUntypedBufferView:: { - raw: unsafe { raw.assume_init() }, - _flags: PhantomData, - }; - - view.as_typed::().map(f) + flags: PyBufferRequest, + f: impl FnOnce(&PyBufferView) -> R, + ) -> PyResult + where + Flag: PyBufferRequestType + flags::IncludesFormat, + { + PyUntypedBufferView::with_flags(obj, flags, |view| view.as_typed::().map(f))? } } -impl PyBufferView { +impl PyBufferView { /// Gets the buffer memory as a slice. /// /// Returns `None` if the buffer is not C-contiguous. @@ -1605,7 +1315,7 @@ impl PyBufferView { /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] where - Flags: py_buffer_flags::GuaranteesCContiguous, + Flag: flags::GuaranteesCContiguous, { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } @@ -1614,7 +1324,7 @@ impl PyBufferView { /// The buffer is guaranteed C-contiguous and writable. pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] where - Flags: py_buffer_flags::GuaranteesCContiguous + py_buffer_flags::GuaranteesWritable, + Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } @@ -1622,7 +1332,7 @@ impl PyBufferView { /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] where - Flags: py_buffer_flags::GuaranteesFContiguous, + Flag: flags::GuaranteesFContiguous, { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } @@ -1631,21 +1341,21 @@ impl PyBufferView { /// The buffer is guaranteed Fortran-contiguous and writable. pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] where - Flags: py_buffer_flags::GuaranteesFContiguous + py_buffer_flags::GuaranteesWritable, + Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, { unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } } } -impl core::ops::Deref for PyBufferView { - type Target = PyUntypedBufferView; +impl core::ops::Deref for PyBufferView { + type Target = PyUntypedBufferView; fn deref(&self) -> &Self::Target { &self.0 } } -impl Debug for PyBufferView { +impl Debug for PyBufferView { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { debug_buffer("PyBufferView", &self.0.raw, f) } @@ -2184,6 +1894,97 @@ mod tests { }); } + #[test] + fn test_simple_buffer_view_array() { + Python::attach(|py| { + let array = py + .import("array") + .unwrap() + .call_method("array", ("I", (1, 2, 3)), None) + .unwrap(); + let byte_count = 3 * mem::size_of::(); + + PyUntypedBufferView::with_flags(&array, PyBufferRequest::simple(), |view| { + assert_eq!(view.format(), c"B"); + assert_eq!(view.item_size(), 1); + assert_eq!(view.item_count(), byte_count); + assert_eq!( + view.as_typed::().unwrap().as_slice(py).unwrap().len(), + byte_count + ); + }) + .unwrap(); + + PyUntypedBufferView::with_flags(&array, PyBufferRequest::simple().writable(), |view| { + assert_eq!(view.item_size(), 1); + assert_eq!(view.item_count(), byte_count); + assert_eq!( + view.as_typed::() + .unwrap() + .as_mut_slice(py) + .unwrap() + .len(), + byte_count + ); + }) + .unwrap(); + + let memoryview = py + .import("builtins") + .unwrap() + .call_method1("memoryview", (&array,)) + .unwrap(); + PyBufferView::::with_flags(&memoryview, PyBufferRequest::simple(), |view| { + assert!(view.0.raw.format.is_null()); + assert!(view.0.raw.shape.is_null()); + assert_eq!(view.as_slice(py).unwrap().len(), byte_count); + }) + .unwrap(); + }); + } + + #[test] + fn test_buffer_view_rejects_scalar() { + Python::attach(|py| { + let scalar = py + .eval(c"memoryview(b'x').cast('B', shape=[])", None, None) + .unwrap(); + + let err = PyBufferView::::with(&scalar, |_| panic!("scalar buffer")).unwrap_err(); + assert_eq!( + err.to_string(), + "BufferError: scalar buffers are not supported" + ); + + // Rejection must release the export. + scalar.call_method0("release").unwrap(); + }); + } + + #[test] + #[cfg(not(PyPy))] + fn test_buffer_view_rejects_null_data() { + Python::attach(|py| { + let empty = py + .eval( + c"memoryview((__import__('ctypes').c_ubyte * 0).from_address(0)).cast('B')", + None, + None, + ) + .unwrap(); + + let err = + PyBufferView::::with(&empty, |_| panic!("null data pointer")).unwrap_err(); + assert_eq!(err.to_string(), "BufferError: buffer data pointer is null"); + + let empty_bytes = PyBytes::new(py, b""); + PyBufferView::::with(&empty_bytes, |view| { + assert!(view.as_slice(py).unwrap().is_empty()); + }) + .unwrap(); + }); + } + #[test] fn test_typed_buffer_view_with_flags() { Python::attach(|py| { @@ -2192,20 +1993,39 @@ mod tests { .unwrap() .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); + let view = py + .import("builtins") + .unwrap() + .call_method1("memoryview", (&array,)) + .unwrap(); - PyBufferView::::with_flags(&array, PyBufferRequest::simple().nd(), |view| { - assert_eq!(view.item_count(), 4); - assert_eq!(view.format().to_str().unwrap(), "f"); - assert_eq!(view.shape(), [4]); + PyUntypedBufferView::with_flags( + &view, + PyBufferRequest::simple().nd().format(), + |view| { + assert_eq!(view.format(), c"f"); + assert_eq!(view.shape(), [4]); + }, + ) + .unwrap(); - let slice = view.as_slice(py).unwrap(); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[3].get(), 2.5); + PyBufferView::::with_flags( + &view, + PyBufferRequest::simple().nd().format(), + |view| { + assert_eq!(view.item_count(), 4); + assert_eq!(view.format().to_str().unwrap(), "f"); + assert_eq!(view.shape(), [4]); - let mut_slice = view.as_mut_slice(py).unwrap(); - mut_slice[0].set(9.0); - assert_eq!(slice[0].get(), 9.0); - }) + let slice = view.as_slice(py).unwrap(); + assert_eq!(slice[0].get(), 1.0); + assert_eq!(slice[3].get(), 2.5); + + let mut_slice = view.as_mut_slice(py).unwrap(); + mut_slice[0].set(9.0); + assert_eq!(slice[0].get(), 9.0); + }, + ) .unwrap(); }); } @@ -2214,8 +2034,11 @@ mod tests { fn test_typed_buffer_view_with_flags_incompatible() { Python::attach(|py| { let bytes = PyBytes::new(py, b"abcde"); - let result = - PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().nd(), |_view| {}); + let result = PyBufferView::::with_flags( + &bytes, + PyBufferRequest::simple().nd().format(), + |_view| {}, + ); assert!(result.is_err()); }); } @@ -2232,7 +2055,7 @@ mod tests { // C_CONTIGUOUS: guaranteed contiguous readonly access (no Option) PyBufferView::::with_flags( &array, - PyBufferRequest::simple().c_contiguous(), + PyBufferRequest::simple().c_contiguous().format(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice.len(), 3); @@ -2266,11 +2089,11 @@ mod tests { #[test] fn test_flag_builders() { - fn assert_contiguity( - _: PyBufferRequest, + fn assert_contiguity( + _: PyBufferRequest, expected: PyBufferContiguity, ) { - assert_eq!(Flags::CONTIGUITY, expected); + assert_eq!(Flag::CONTIGUITY, expected); } fn assert_direct< @@ -2534,14 +2357,14 @@ mod tests { }) .unwrap(); - PyBufferView::::with_flags(&array, PyBufferRequest::contig(), |view| { + PyBufferView::::with_flags(&array, PyBufferRequest::contig().format(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice[0].get(), 1.0); }) .unwrap(); // Writable + contiguity on typed view - PyBufferView::::with_flags(&array, PyBufferRequest::contig(), |view| { + PyBufferView::::with_flags(&array, PyBufferRequest::contig().format(), |view| { let slice = view.as_contiguous_slice(py); assert_eq!(slice[0].get(), 1.0); let mut_slice = view.as_contiguous_mut_slice(py); diff --git a/src/buffer/flags.rs b/src/buffer/flags.rs new file mode 100644 index 00000000000..19ade85ecfe --- /dev/null +++ b/src/buffer/flags.rs @@ -0,0 +1,314 @@ +use super::PyBufferContiguity; + +pub(super) const CONTIGUITY_UNDEFINED: u8 = PyBufferContiguity::Undefined as u8; +pub(super) const CONTIGUITY_C: u8 = PyBufferContiguity::C as u8; +pub(super) const CONTIGUITY_F: u8 = PyBufferContiguity::F as u8; +pub(super) const CONTIGUITY_ANY: u8 = PyBufferContiguity::Any as u8; + +pub struct PyBufferFlags< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +>; + +#[diagnostic::on_unimplemented( + message = "format information has already been requested for this buffer request", + note = "remove the extra `.format()` call" +)] +pub trait CanRequestFormat {} +#[diagnostic::do_not_recommend] +impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> CanRequestFormat for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "shape information has already been requested for this buffer request", + note = "remove the extra `.nd()` call" +)] +pub trait CanRequestShape {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> CanRequestShape for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "stride information has already been requested for this buffer request", + note = "remove the extra `.strides()` call" +)] +pub trait CanRequestStrides {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> CanRequestStrides for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "suboffsets can only be requested on a direct unconstrained buffer request", + note = "call `.indirect()` before any contiguity builder, and only once" +)] +pub trait CanRequestIndirect {} +#[diagnostic::do_not_recommend] +impl + CanRequestIndirect + for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "writability has already been requested for this buffer request", + note = "remove the extra `.writable()` call" +)] +pub trait CanRequestWritable {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const CONTIGUITY: u8, +> CanRequestWritable for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "contiguity has already been constrained for this buffer request", + note = "only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used" +)] +pub trait CanRequestContiguity {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, +> CanRequestContiguity + for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "writability is not guaranteed by the requested buffer flags", + note = "use `.writable()` when building a buffer request to guarantee writability" +)] +pub trait GuaranteesWritable {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const CONTIGUITY: u8, +> GuaranteesWritable for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "C contiguity is not guaranteed by the requested buffer flags", + note = "use `.c_contiguous()` when building a buffer request to guarantee C contiguity" +)] +pub trait GuaranteesCContiguous {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, +> GuaranteesCContiguous + for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "Fortran contiguity is not guaranteed by the requested buffer flags", + note = "use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity" +)] +pub trait GuaranteesFContiguous {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, +> GuaranteesFContiguous + for PyBufferFlags +{ +} + +/// Marker trait for buffer flags which have requested format information. +#[diagnostic::on_unimplemented( + message = "format information is not available with the requested buffer flags", + note = "use `.format()` when building a buffer request to request format information", + note = "`PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format" +)] +pub trait IncludesFormat { + const ASSUME_U8: bool; +} + +#[diagnostic::do_not_recommend] +impl< + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> IncludesFormat for PyBufferFlags +{ + const ASSUME_U8: bool = false; +} + +// Simple (maybe writable) buffers also have an implied u8 format. +#[diagnostic::do_not_recommend] +impl IncludesFormat + for PyBufferFlags +{ + const ASSUME_U8: bool = true; +} + +#[diagnostic::on_unimplemented( + message = "shape information is not available with the requested buffer flags", + note = "use `.nd()` when building a buffer request to request shape information" +)] +pub trait IncludesShape {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> IncludesShape for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "strides information is not available with the requested buffer flags", + note = "use `.strides()` when building a buffer request to request stride information" +)] +pub trait IncludesStrides {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> IncludesStrides for PyBufferFlags +{ +} + +#[diagnostic::on_unimplemented( + message = "suboffsets information is not available with the requested buffer flags", + note = "use `.indirect()` when building a buffer request to request suboffset information" +)] +pub trait IncludesSuboffsets {} +#[diagnostic::do_not_recommend] +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> IncludesSuboffsets for PyBufferFlags +{ +} + +pub trait Sealed {} +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY: u8, +> Sealed for PyBufferFlags +{ +} + +/// Trait implemented by all hidden [`super::PyBufferRequest`] states. +pub trait PyBufferRequestType: Sealed { + /// Whether this is a simple request, which treats the buffer as unsigned bytes. + const IS_SIMPLE: bool; + + /// The contiguity requirement encoded by these flags. + const CONTIGUITY: PyBufferContiguity; + + /// Whether these flags require a writable buffer. + const WRITABLE: bool; + + /// The state after requesting format information. + type WithFormat: PyBufferRequestType + IncludesFormat; + + /// The state after requesting shape information. + type WithShape: PyBufferRequestType + IncludesShape; + + /// The state after requesting strides information. + type WithStrides: PyBufferRequestType + IncludesShape + IncludesStrides; + + /// The state after requesting indirect / suboffset information. + type WithIndirect: PyBufferRequestType + IncludesShape + IncludesStrides + IncludesSuboffsets; + + /// The state after requesting writability. + type WithWritable: PyBufferRequestType; + + /// The state after requesting C contiguity. + type WithCContiguous: PyBufferRequestType; + + /// The state after requesting Fortran contiguity. + type WithFContiguous: PyBufferRequestType; + + /// The state after requesting either C or Fortran contiguity. + type WithAnyContiguous: PyBufferRequestType; +} + +impl< + const FORMAT: bool, + const SHAPE: bool, + const STRIDE: bool, + const INDIRECT: bool, + const WRITABLE: bool, + const CONTIGUITY_REQ: u8, +> PyBufferRequestType for PyBufferFlags +{ + const IS_SIMPLE: bool = !FORMAT && !SHAPE; + + const CONTIGUITY: PyBufferContiguity = match CONTIGUITY_REQ { + CONTIGUITY_UNDEFINED => PyBufferContiguity::Undefined, + CONTIGUITY_C => PyBufferContiguity::C, + CONTIGUITY_F => PyBufferContiguity::F, + CONTIGUITY_ANY => PyBufferContiguity::Any, + _ => panic!("invalid buffer contiguity"), + }; + const WRITABLE: bool = WRITABLE; + + type WithFormat = PyBufferFlags; + type WithShape = PyBufferFlags; + type WithStrides = PyBufferFlags; + type WithIndirect = PyBufferFlags; + type WithWritable = PyBufferFlags; + type WithCContiguous = PyBufferFlags; + type WithFContiguous = PyBufferFlags; + type WithAnyContiguous = PyBufferFlags; +} diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 00720dc25f3..f5a34c80c1b 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -3,7 +3,7 @@ #![cfg(feature = "macros")] #![cfg(any(not(Py_LIMITED_API), Py_3_11))] -use pyo3::buffer::PyBuffer; +use pyo3::buffer::{PyBuffer, PyBufferRequest, PyBufferView, PyUntypedBufferView}; use pyo3::exceptions::PyBufferError; use pyo3::ffi; use pyo3::prelude::*; @@ -137,6 +137,111 @@ fn test_releasebuffer_unraisable_error() { }); } +#[pyclass] +#[derive(Default)] +struct SelfReferentialBuffer { + releases: usize, + moved_on_release: bool, +} + +#[pymethods] +impl SelfReferentialBuffer { + unsafe fn __getbuffer__( + slf: Bound<'_, Self>, + view: *mut ffi::Py_buffer, + flags: c_int, + ) -> PyResult<()> { + // This helper sets shape and strides to point into the Py_buffer itself. + unsafe { fill_view_from_readonly_data(view, flags, b"abc", slf.into_any()) }?; + // Remember the address passed to __getbuffer__ for comparison on release. + unsafe { (*view).internal = view.cast() }; + Ok(()) + } + + unsafe fn __releasebuffer__(&mut self, view: *mut ffi::Py_buffer) { + self.releases += 1; + self.moved_on_release |= unsafe { (*view).internal != view.cast() }; + if !unsafe { (*view).format.is_null() } { + drop(unsafe { CString::from_raw((*view).format) }); + } + } +} + +fn assert_view_released(instance: &Bound<'_, SelfReferentialBuffer>, releases: usize) { + let exporter = instance.borrow(); + assert_eq!(exporter.releases, releases); + assert!(!exporter.moved_on_release); + drop(exporter); + // SAFETY: The bound instance is a live Python object on this attached thread. + assert_eq!(unsafe { ffi::Py_REFCNT(instance.as_ptr()) }, 1); +} + +#[test] +fn test_buffer_views_keep_export_in_place() { + Python::attach(|py| { + let instance = Bound::new(py, SelfReferentialBuffer::default()).unwrap(); + + PyUntypedBufferView::with_flags(&instance, PyBufferRequest::full_ro(), |view| { + assert_eq!(view.shape(), [3]); + assert_eq!(view.strides(), [1]); + }) + .unwrap(); + assert_view_released(&instance, 1); + + PyBufferView::::with(&instance, |_| {}).unwrap(); + assert_view_released(&instance, 2); + + PyBufferView::::with_flags( + &instance, + PyBufferRequest::simple().strides().format(), + |_| {}, + ) + .unwrap(); + assert_view_released(&instance, 3); + }); +} + +#[test] +fn test_buffer_views_release_after_errors() { + Python::attach(|py| { + let instance = Bound::new(py, SelfReferentialBuffer::default()).unwrap(); + + let result = PyBufferView::::with_flags( + &instance, + PyBufferRequest::simple().strides().format(), + |_| panic!("incompatible buffer"), + ); + assert!(result.unwrap_err().is_instance_of::(py)); + assert_view_released(&instance, 1); + + let result = PyUntypedBufferView::with_flags( + &instance, + PyBufferRequest::simple().writable(), + |_| panic!("read-only buffer"), + ); + assert!(result.unwrap_err().is_instance_of::(py)); + assert_view_released(&instance, 1); + }); +} + +#[test] +#[cfg(panic = "unwind")] +fn test_buffer_views_release_on_panic() { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + Python::attach(|py| { + let instance = Bound::new(py, SelfReferentialBuffer::default()).unwrap(); + + let result = catch_unwind(AssertUnwindSafe(|| { + PyUntypedBufferView::with_flags(&instance, PyBufferRequest::full_ro(), |_| { + panic!("buffer callback panicked") + }) + })); + assert!(result.is_err()); + assert_view_released(&instance, 1); + }); +} + /// # Safety /// /// `view` must be a valid pointer to ffi::Py_buffer, or null diff --git a/tests/ui/invalid_buffer_flags.rs b/tests/ui/invalid_buffer_flags.rs index 51b2f1feae6..3abd27bb0ae 100644 --- a/tests/ui/invalid_buffer_flags.rs +++ b/tests/ui/invalid_buffer_flags.rs @@ -1,4 +1,4 @@ -use pyo3::buffer::{PyBufferRequest, PyUntypedBufferView}; +use pyo3::buffer::{PyBufferRequest, PyBufferView, PyUntypedBufferView}; use pyo3::prelude::*; use pyo3::types::PyBytes; @@ -17,5 +17,8 @@ fn main() { //~^ ERROR: format information is not available with the requested buffer flags }) .unwrap(); + + PyBufferView::::with_flags(&bytes, PyBufferRequest::strided(), |_| {}); + //~^ ERROR: format information is not available with the requested buffer flags }); } diff --git a/tests/ui/invalid_buffer_flags.stderr b/tests/ui/invalid_buffer_flags.stderr index 200b4896dcd..4194808d9d3 100644 --- a/tests/ui/invalid_buffer_flags.stderr +++ b/tests/ui/invalid_buffer_flags.stderr @@ -1,72 +1,87 @@ error[E0277]: contiguity has already been constrained for this buffer request - --> tests/ui/invalid_buffer_flags.rs:6:54 - | - 6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); - | ^^^^^^^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestContiguity` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used -note: required by a bound in `PyBufferRequest::::f_contiguous` + --> tests/ui/invalid_buffer_flags.rs:6:54 + | + 6 | let _ = PyBufferRequest::simple().c_contiguous().f_contiguous(); + | ^^^^^^^^^^^^ the trait `buffer::flags::CanRequestContiguity` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: only one of `.c_contiguous()`, `.f_contiguous()`, or `.any_contiguous()` may be used +note: required by a bound in `PyBufferRequest::::f_contiguous` --> src/buffer.rs | - | pub const fn f_contiguous(self) -> PyBufferRequest + | pub const fn f_contiguous(self) -> PyBufferRequest | ------------ required by a bound in this associated function | where - | Flags: py_buffer_flags::CanRequestContiguity, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::f_contiguous` + | Flag: flags::CanRequestContiguity, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::f_contiguous` error[E0277]: format information has already been requested for this buffer request - --> tests/ui/invalid_buffer_flags.rs:8:49 - | - 8 | let _ = PyBufferRequest::strided().format().format(); - | ^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: remove the extra `.format()` call -note: required by a bound in `PyBufferRequest::::format` + --> tests/ui/invalid_buffer_flags.rs:8:49 + | + 8 | let _ = PyBufferRequest::strided().format().format(); + | ^^^^^^ the trait `buffer::flags::CanRequestFormat` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: remove the extra `.format()` call +note: required by a bound in `PyBufferRequest::::format` --> src/buffer.rs | - | pub const fn format(self) -> PyBufferRequest + | pub const fn format(self) -> PyBufferRequest | ------ required by a bound in this associated function | where - | Flags: py_buffer_flags::CanRequestFormat, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::format` + | Flag: flags::CanRequestFormat, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::format` error[E0277]: suboffsets can only be requested on a direct unconstrained buffer request - --> tests/ui/invalid_buffer_flags.rs:10:50 - | - 10 | let _ = PyBufferRequest::simple().indirect().indirect(); - | ^^^^^^^^ unsatisfied trait bound - | - = help: the trait `buffer::py_buffer_flags::CanRequestIndirect` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` - = note: call `.indirect()` before any contiguity builder, and only once -note: required by a bound in `PyBufferRequest::::indirect` + --> tests/ui/invalid_buffer_flags.rs:10:50 + | + 10 | let _ = PyBufferRequest::simple().indirect().indirect(); + | ^^^^^^^^ the trait `buffer::flags::CanRequestIndirect` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: call `.indirect()` before any contiguity builder, and only once +note: required by a bound in `PyBufferRequest::::indirect` --> src/buffer.rs | - | pub const fn indirect(self) -> PyBufferRequest + | pub const fn indirect(self) -> PyBufferRequest | -------- required by a bound in this associated function | where - | Flags: py_buffer_flags::CanRequestIndirect, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::indirect` + | Flag: flags::CanRequestIndirect, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferRequest::::indirect` error[E0277]: format information is not available with the requested buffer flags --> tests/ui/invalid_buffer_flags.rs:16:18 | 16 | view.format(); - | ^^^^^^ unsatisfied trait bound + | ^^^^^^ the trait `buffer::flags::IncludesFormat` is not implemented for `buffer::flags::PyBufferFlags` | - = help: the trait `buffer::py_buffer_flags::IncludesFormat` is not implemented for `buffer::py_buffer_flags::PyBufferFlags` = note: use `.format()` when building a buffer request to request format information = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format -note: required by a bound in `PyUntypedBufferView::::format` +note: required by a bound in `PyUntypedBufferView::::format` --> src/buffer.rs | | pub fn format(&self) -> &CStr | ------ required by a bound in this associated function | where - | Flags: py_buffer_flags::IncludesFormat, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyUntypedBufferView::::format` + | Flag: flags::IncludesFormat, + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyUntypedBufferView::::format` + +error[E0277]: format information is not available with the requested buffer flags + --> tests/ui/invalid_buffer_flags.rs:21:48 + | + 21 | PyBufferView::::with_flags(&bytes, PyBufferRequest::strided(), |_| {}); + | ------------------------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::IncludesFormat` is not implemented for `buffer::flags::PyBufferFlags` + | | + | required by a bound introduced by this call + | + = note: use `.format()` when building a buffer request to request format information + = note: `PyBufferRequest::simple()` and `PyBufferRequest::simple().writable()` also imply u8 format +note: required by a bound in `PyBufferView::::with_flags` + --> src/buffer.rs + | + | pub fn with_flags( + | ---------- required by a bound in this associated function +... + | Flag: PyBufferRequestType + flags::IncludesFormat, + | ^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::with_flags` -error: aborting due to 4 previous errors +error: aborting due to 5 previous errors For more information about this error, try `rustc --explain E0277`. diff --git a/tests/ui/invalid_buffer_view_slice.rs b/tests/ui/invalid_buffer_view_slice.rs new file mode 100644 index 00000000000..7122133cfc7 --- /dev/null +++ b/tests/ui/invalid_buffer_view_slice.rs @@ -0,0 +1,36 @@ +use pyo3::buffer::{PyBufferRequest, PyBufferView}; +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +fn main() { + Python::attach(|py| { + let bytes = PyBytes::new(py, &[1, 2, 3]); + PyBufferView::::with(&bytes, |view| { + view.as_contiguous_slice(py); + //~^ ERROR: C contiguity is not guaranteed by the requested buffer flags + view.as_fortran_contiguous_slice(py); + //~^ ERROR: Fortran contiguity is not guaranteed by the requested buffer flags + }) + .unwrap(); + + PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().writable(), |view| { + view.as_contiguous_mut_slice(py); + //~^ ERROR: C contiguity is not guaranteed by the requested buffer flags + view.as_fortran_contiguous_mut_slice(py); + //~^ ERROR: Fortran contiguity is not guaranteed by the requested buffer flags + }) + .unwrap(); + + PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().c_contiguous().format(), |view| { + view.as_contiguous_mut_slice(py); + //~^ ERROR: writability is not guaranteed by the requested buffer flags + }) + .unwrap(); + + PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().f_contiguous().format(), |view| { + view.as_fortran_contiguous_mut_slice(py); + //~^ ERROR: writability is not guaranteed by the requested buffer flags + }) + .unwrap(); + }); +} diff --git a/tests/ui/invalid_buffer_view_slice.stderr b/tests/ui/invalid_buffer_view_slice.stderr new file mode 100644 index 00000000000..b54fbe1e7e0 --- /dev/null +++ b/tests/ui/invalid_buffer_view_slice.stderr @@ -0,0 +1,99 @@ +error[E0277]: C contiguity is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:9:18 + | + 9 | view.as_contiguous_slice(py); + | ^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.c_contiguous()` when building a buffer request to guarantee C contiguity +note: required by a bound in `PyBufferView::::as_contiguous_slice` + --> src/buffer.rs + | + | pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + | ------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesCContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_slice` + +error[E0277]: Fortran contiguity is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:11:18 + | + 11 | view.as_fortran_contiguous_slice(py); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity +note: required by a bound in `PyBufferView::::as_fortran_contiguous_slice` + --> src/buffer.rs + | + | pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + | --------------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesFContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_slice` + +error[E0277]: C contiguity is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:17:18 + | + 17 | view.as_contiguous_mut_slice(py); + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.c_contiguous()` when building a buffer request to guarantee C contiguity +note: required by a bound in `PyBufferView::::as_contiguous_mut_slice` + --> src/buffer.rs + | + | pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | ----------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_mut_slice` + +error[E0277]: Fortran contiguity is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:19:18 + | + 19 | view.as_fortran_contiguous_mut_slice(py); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity +note: required by a bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + --> src/buffer.rs + | + | pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | ------------------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + +error[E0277]: writability is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:25:18 + | + 25 | view.as_contiguous_mut_slice(py); + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesWritable` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.writable()` when building a buffer request to guarantee writability +note: required by a bound in `PyBufferView::::as_contiguous_mut_slice` + --> src/buffer.rs + | + | pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | ----------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_mut_slice` + +error[E0277]: writability is not guaranteed by the requested buffer flags + --> tests/ui/invalid_buffer_view_slice.rs:31:18 + | + 31 | view.as_fortran_contiguous_mut_slice(py); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesWritable` is not implemented for `buffer::flags::PyBufferFlags` + | + = note: use `.writable()` when building a buffer request to guarantee writability +note: required by a bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + --> src/buffer.rs + | + | pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | ------------------------------- required by a bound in this associated function + | where + | Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + +error: aborting due to 6 previous errors + +For more information about this error, try `rustc --explain E0277`. From cc8f9a32460086ef9011a4bd85c0635d708d1830 Mon Sep 17 00:00:00 2001 From: winstxnhdw Date: Fri, 2 Oct 2026 19:19:05 +0800 Subject: [PATCH 23/25] test: update UI expectations for Rust 1.99 --- tests/ui/invalid_intopy_with.stderr | 8 ++++---- tests/ui/invalid_pymodule_in_root.rs | 3 +-- tests/ui/invalid_pymodule_in_root.stderr | 12 +----------- tests/ui/pyclass_send.stderr | 8 ++++---- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/tests/ui/invalid_intopy_with.stderr b/tests/ui/invalid_intopy_with.stderr index f0d6a80c250..e101f16f14b 100644 --- a/tests/ui/invalid_intopy_with.stderr +++ b/tests/ui/invalid_intopy_with.stderr @@ -7,8 +7,8 @@ error[E0308]: mismatched types 5 | #[pyo3(into_py_with = into)] | ^^^^ expected fn pointer, found fn item | - = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, _>` - found fn item `for<'a> fn(String, Python<'a>) -> Result, _> {into}` + = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, _>` + found fn item `for<'a> fn(String, Python<'a>) -> Result, _> {into}` error[E0308]: mismatched types --> tests/ui/invalid_intopy_with.rs:5:27 @@ -19,8 +19,8 @@ error[E0308]: mismatched types 5 | #[pyo3(into_py_with = into)] | ^^^^ expected fn pointer, found fn item | - = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, _>` - found fn item `for<'a> fn(String, Python<'a>) -> Result, _> {into}` + = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, _>` + found fn item `for<'a> fn(String, Python<'a>) -> Result, _> {into}` error: aborting due to 2 previous errors diff --git a/tests/ui/invalid_pymodule_in_root.rs b/tests/ui/invalid_pymodule_in_root.rs index 046759f98db..f1b0e505fd5 100644 --- a/tests/ui/invalid_pymodule_in_root.rs +++ b/tests/ui/invalid_pymodule_in_root.rs @@ -3,7 +3,6 @@ use pyo3::prelude::*; #[pymodule] #[path = "empty.rs"] // to silence error related to missing file mod invalid_pymodule_in_root_module; -//~^ ERROR: file modules in proc macro input are unstable -//~| ERROR: `#[pymodule]` can only be used on inline modules +//~^ ERROR: `#[pymodule]` can only be used on inline modules fn main() {} diff --git a/tests/ui/invalid_pymodule_in_root.stderr b/tests/ui/invalid_pymodule_in_root.stderr index ae89fda1e5c..6b7b3a61917 100644 --- a/tests/ui/invalid_pymodule_in_root.stderr +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -1,17 +1,7 @@ -error[E0658]: file modules in proc macro input are unstable - --> tests/ui/invalid_pymodule_in_root.rs:5:1 - | -5 | mod invalid_pymodule_in_root_module; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: see issue #54727 for more information - error: `#[pymodule]` can only be used on inline modules --> tests/ui/invalid_pymodule_in_root.rs:5:1 | 5 | mod invalid_pymodule_in_root_module; | ^^^ -error: aborting due to 2 previous errors - -For more information about this error, try `rustc --explain E0658`. +error: aborting due to 1 previous error diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 351552f0c99..3c5e57d35eb 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -10,7 +10,7 @@ note: required because it appears within the type `NotSyncNotSend` | 5 | struct NotSyncNotSend(*mut c_void); | ^^^^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_send_sync` +note: required by a bound in `pyo3::impl_::pyclass::assert_pyclass_send_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_send_sync() @@ -31,7 +31,7 @@ note: required because it appears within the type `NotSyncNotSend` | 5 | struct NotSyncNotSend(*mut c_void); | ^^^^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_send_sync` +note: required by a bound in `pyo3::impl_::pyclass::assert_pyclass_send_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_send_sync() @@ -52,7 +52,7 @@ note: required because it appears within the type `SendNotSync` | 10 | struct SendNotSync(*mut c_void); | ^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_send_sync` +note: required by a bound in `pyo3::impl_::pyclass::assert_pyclass_send_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_send_sync() @@ -73,7 +73,7 @@ note: required because it appears within the type `SyncNotSend` | 15 | struct SyncNotSend(*mut c_void); | ^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_send_sync` +note: required by a bound in `pyo3::impl_::pyclass::assert_pyclass_send_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_send_sync() From c18b6a5e668d4f40f203a4366439b5ac7a2beffa Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 24 Oct 2026 20:39:44 +0100 Subject: [PATCH 24/25] apply new style of slice accessors --- src/buffer.rs | 196 ++++++++++------------ src/buffer/flags.rs | 16 -- tests/ui/invalid_buffer_view_slice.rs | 16 +- tests/ui/invalid_buffer_view_slice.stderr | 80 ++++----- 4 files changed, 138 insertions(+), 170 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 7dfbbc62586..d5efa43dc23 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -364,25 +364,25 @@ impl PyBuffer { /// /// Returns null if the buffer is not C-style contiguous pub fn as_slice_ptr(&self) -> Option> { - self.slice_pointer_internal(self.is_c_contiguous()) + self.is_c_contiguous() + .then(|| self.slice_pointer_internal()) + .flatten() } /// Gets the buffer memory as a slice. /// /// This function returns null if the buffer is not Fortran-style contiguous pub fn as_fortran_slice_ptr(&self) -> Option> { - self.slice_pointer_internal(self.is_fortran_contiguous()) + self.is_fortran_contiguous() + .then(|| self.slice_pointer_internal()) + .flatten() } - fn slice_pointer_internal(&self, contiguous: bool) -> Option> { - if contiguous { - NonNull::new(ptr::slice_from_raw_parts_mut( - self.raw().buf.cast(), - self.item_count(), - )) - } else { - None - } + fn slice_pointer_internal(&self) -> Option> { + NonNull::new(ptr::slice_from_raw_parts_mut( + self.raw().buf.cast(), + self.item_count(), + )) } /// Copies the buffer elements to the specified slice. @@ -1044,6 +1044,9 @@ pub struct PyUntypedBufferView< impl PyUntypedBufferView { /// Gets the pointer to the start of the buffer memory. + /// + /// See [`PyUntypedBuffer::buf_ptr()`] for considerations about how + /// to safely read and write data through this pointer. #[inline] pub fn buf_ptr(&self) -> *mut c_void { self.raw.buf @@ -1230,7 +1233,7 @@ impl PyUntypedBufferView { // SAFETY: PyUntypedBufferView is repr(transparent) around ffi::Py_buffer, // so the pointer has the layout and alignment expected by PyObject_GetBuffer. - err::error_on_minusone(obj.py(), unsafe { + err::error_on_negative(obj.py(), unsafe { ffi::PyObject_GetBuffer(obj.as_ptr(), view.as_mut_ptr().cast(), flags.0) })?; @@ -1304,62 +1307,52 @@ impl PyBufferView { /// /// Returns `None` if the buffer is not C-contiguous. /// - /// The returned slice uses type [`ReadOnlyCell`] because it's theoretically possible - /// for any call into the Python runtime to modify the values in the slice. - pub fn as_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [ReadOnlyCell]> { - if !self.is_c_contiguous() { - return None; - } - - Some(unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) }) + /// Accessing the memory requires upholding the [buffer memory access + /// requirements](PyUntypedBufferView::buf_ptr). + pub fn as_slice_ptr(&self) -> Option> { + self.is_c_contiguous() + .then(|| self.slice_pointer_internal()) } - /// Gets the buffer memory as a mutable slice. + /// Gets the buffer memory as a slice. /// - /// Returns `None` if the buffer is read-only or not C-contiguous. + /// Returns `None` if the buffer is not Fortran-contiguous. /// - /// The returned slice uses type [`Cell`](cell::Cell) because it's theoretically possible - /// for any call into the Python runtime to modify the values in the slice. - pub fn as_mut_slice<'a>(&'a self, _py: Python<'a>) -> Option<&'a [cell::Cell]> { - if self.readonly() || !self.is_c_contiguous() { - return None; - } - - Some(unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) }) + /// Accessing the memory requires upholding the [buffer memory access + /// requirements](PyUntypedBufferView::buf_ptr). + pub fn as_fortran_slice_ptr(&self) -> Option> { + self.is_fortran_contiguous() + .then(|| self.slice_pointer_internal()) } /// Gets the buffer memory as a slice. The buffer is guaranteed C-contiguous. - pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + /// + /// Accessing the memory requires upholding the [buffer memory access + /// requirements](PyUntypedBufferView::buf_ptr). + pub fn as_contiguous_slice_ptr(&self) -> NonNull<[T]> where Flag: flags::GuaranteesCContiguous, { - unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } - } - - /// Gets the buffer memory as a mutable slice. - /// The buffer is guaranteed C-contiguous and writable. - pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] - where - Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, - { - unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + self.slice_pointer_internal() } - /// Gets the buffer memory as a slice. The buffer is guaranteed Fortran-contiguous. - pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] + /// Gets the buffer memory as a slice pointer. The buffer is guaranteed Fortran-contiguous. + /// + /// Accessing the memory requires upholding the [buffer memory access + /// requirements](PyUntypedBufferView::buf_ptr). + pub fn as_fortran_contiguous_slice_ptr(&self) -> NonNull<[T]> where Flag: flags::GuaranteesFContiguous, { - unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + self.slice_pointer_internal() } - /// Gets the buffer memory as a mutable slice. - /// The buffer is guaranteed Fortran-contiguous and writable. - pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] - where - Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, - { - unsafe { slice::from_raw_parts(self.0.raw.buf.cast(), self.item_count()) } + fn slice_pointer_internal(&self) -> NonNull<[T]> { + NonNull::new(ptr::slice_from_raw_parts_mut( + self.0.raw.buf.cast(), + self.item_count(), + )) + .expect("buffer data pointer is non-null") } } @@ -1832,13 +1825,14 @@ mod tests { assert_eq!(view.shape(), [5]); assert!(view.suboffsets().is_none()); - let slice = view.as_slice(py).unwrap(); - assert_eq!(slice.len(), 5); - assert_eq!(slice[0].get(), b'a'); - assert_eq!(slice[4].get(), b'e'); + let slice_ptr = view.as_slice_ptr().unwrap(); + assert_eq!(slice_ptr.len(), 5); + // SAFETY: bytes are immutable and the export is still alive. + assert_eq!(unsafe { slice_ptr.as_ref() }, b"abcde"); + assert!(view.readonly()); - // bytes are read-only - assert!(view.as_mut_slice(py).is_none()); + let fortran_slice_ptr = view.as_fortran_slice_ptr().unwrap(); + assert_eq!(fortran_slice_ptr, slice_ptr); }) .unwrap(); }); @@ -1858,16 +1852,14 @@ mod tests { assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); - let slice = view.as_slice(py).unwrap(); - assert_eq!(slice.len(), 4); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[3].get(), 2.5); - - // array.array is writable - let mut_slice = view.as_mut_slice(py).unwrap(); - assert_eq!(mut_slice[0].get(), 1.0); - mut_slice[3].set(2.75); - assert_eq!(slice[3].get(), 2.75); + let mut slice_ptr = view.as_slice_ptr().unwrap(); + assert_eq!(slice_ptr.len(), 4); + assert!(!view.readonly()); + // SAFETY: the array is writable, not shared, and the export is still alive. + let slice = unsafe { slice_ptr.as_mut() }; + assert_eq!(slice, &[1.0, 1.5, 2.0, 2.5]); + slice[3] = 2.75; + assert_eq!(slice[3], 2.75); }) .unwrap(); }); @@ -1927,7 +1919,7 @@ mod tests { assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), byte_count); assert_eq!( - view.as_typed::().unwrap().as_slice(py).unwrap().len(), + view.as_typed::().unwrap().as_slice_ptr().unwrap().len(), byte_count ); }) @@ -1937,11 +1929,7 @@ mod tests { assert_eq!(view.item_size(), 1); assert_eq!(view.item_count(), byte_count); assert_eq!( - view.as_typed::() - .unwrap() - .as_mut_slice(py) - .unwrap() - .len(), + view.as_typed::().unwrap().as_slice_ptr().unwrap().len(), byte_count ); }) @@ -1955,7 +1943,7 @@ mod tests { PyBufferView::::with_flags(&memoryview, PyBufferRequest::simple(), |view| { assert!(view.0.raw.format.is_null()); assert!(view.0.raw.shape.is_null()); - assert_eq!(view.as_slice(py).unwrap().len(), byte_count); + assert_eq!(view.as_slice_ptr().unwrap().len(), byte_count); }) .unwrap(); }); @@ -1997,7 +1985,7 @@ mod tests { let empty_bytes = PyBytes::new(py, b""); PyBufferView::::with(&empty_bytes, |view| { - assert!(view.as_slice(py).unwrap().is_empty()); + assert_eq!(view.as_slice_ptr().unwrap().len(), 0); }) .unwrap(); }); @@ -2035,13 +2023,14 @@ mod tests { assert_eq!(view.format().to_str().unwrap(), "f"); assert_eq!(view.shape(), [4]); - let slice = view.as_slice(py).unwrap(); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[3].get(), 2.5); - - let mut_slice = view.as_mut_slice(py).unwrap(); - mut_slice[0].set(9.0); - assert_eq!(slice[0].get(), 9.0); + let mut slice_ptr = view.as_slice_ptr().unwrap(); + assert!(!view.readonly()); + // SAFETY: the array is writable, not shared, and the export is still alive. + let slice = unsafe { slice_ptr.as_mut() }; + assert_eq!(slice[0], 1.0); + assert_eq!(slice[3], 2.5); + slice[0] = 9.0; + assert_eq!(slice[0], 9.0); }, ) .unwrap(); @@ -2062,7 +2051,7 @@ mod tests { } #[test] - fn test_c_contiguous_slice() { + fn test_c_contiguous_slice_ptr() { Python::attach(|py| { let array = py .import("array") @@ -2070,26 +2059,26 @@ mod tests { .call_method("array", ("f", (1.0, 1.5, 2.0)), None) .unwrap(); - // C_CONTIGUOUS: guaranteed contiguous readonly access (no Option) PyBufferView::::with_flags( &array, PyBufferRequest::simple().c_contiguous().format(), |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice.len(), 3); - assert_eq!(slice[0].get(), 1.0); - assert_eq!(slice[2].get(), 2.0); + let slice_ptr = view.as_contiguous_slice_ptr(); + assert_eq!(slice_ptr.len(), 3); + assert_eq!(Some(slice_ptr), view.as_slice_ptr()); + // SAFETY: the array is not shared and the export is still alive. + assert_eq!(unsafe { slice_ptr.as_ref() }, &[1.0, 1.5, 2.0]); }, ) .unwrap(); - // C_CONTIGUOUS | WRITABLE (via CONTIG combined with STRIDES-level): - // no predefined constant, but we can use PyBufferView::with on a writable array - // and the Option-based as_mut_slice still works PyBufferView::::with(&array, |view| { - let mut_slice = view.as_mut_slice(py).unwrap(); - mut_slice[2].set(9.0); - assert_eq!(view.as_slice(py).unwrap()[2].get(), 9.0); + let mut slice_ptr = view.as_slice_ptr().unwrap(); + assert!(!view.readonly()); + // SAFETY: the array is writable, not shared, and the export is still alive. + let slice = unsafe { slice_ptr.as_mut() }; + slice[2] = 9.0; + assert_eq!(slice[2], 9.0); }) .unwrap(); }); @@ -2376,18 +2365,13 @@ mod tests { .unwrap(); PyBufferView::::with_flags(&array, PyBufferRequest::contig().format(), |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice[0].get(), 1.0); - }) - .unwrap(); - - // Writable + contiguity on typed view - PyBufferView::::with_flags(&array, PyBufferRequest::contig().format(), |view| { - let slice = view.as_contiguous_slice(py); - assert_eq!(slice[0].get(), 1.0); - let mut_slice = view.as_contiguous_mut_slice(py); - mut_slice[0].set(9.0); - assert_eq!(slice[0].get(), 9.0); + let mut slice_ptr = view.as_contiguous_slice_ptr(); + assert!(!view.readonly()); + // SAFETY: the array is writable, not shared, and the export is still alive. + let slice = unsafe { slice_ptr.as_mut() }; + assert_eq!(slice[0], 1.0); + slice[0] = 9.0; + assert_eq!(slice[0], 9.0); }) .unwrap(); diff --git a/src/buffer/flags.rs b/src/buffer/flags.rs index 19ade85ecfe..a05fe00ff2a 100644 --- a/src/buffer/flags.rs +++ b/src/buffer/flags.rs @@ -107,22 +107,6 @@ impl< { } -#[diagnostic::on_unimplemented( - message = "writability is not guaranteed by the requested buffer flags", - note = "use `.writable()` when building a buffer request to guarantee writability" -)] -pub trait GuaranteesWritable {} -#[diagnostic::do_not_recommend] -impl< - const FORMAT: bool, - const SHAPE: bool, - const STRIDE: bool, - const INDIRECT: bool, - const CONTIGUITY: u8, -> GuaranteesWritable for PyBufferFlags -{ -} - #[diagnostic::on_unimplemented( message = "C contiguity is not guaranteed by the requested buffer flags", note = "use `.c_contiguous()` when building a buffer request to guarantee C contiguity" diff --git a/tests/ui/invalid_buffer_view_slice.rs b/tests/ui/invalid_buffer_view_slice.rs index 7122133cfc7..5e25f6dce0f 100644 --- a/tests/ui/invalid_buffer_view_slice.rs +++ b/tests/ui/invalid_buffer_view_slice.rs @@ -6,30 +6,30 @@ fn main() { Python::attach(|py| { let bytes = PyBytes::new(py, &[1, 2, 3]); PyBufferView::::with(&bytes, |view| { - view.as_contiguous_slice(py); + view.as_contiguous_slice_ptr(); //~^ ERROR: C contiguity is not guaranteed by the requested buffer flags - view.as_fortran_contiguous_slice(py); + view.as_fortran_contiguous_slice_ptr(); //~^ ERROR: Fortran contiguity is not guaranteed by the requested buffer flags }) .unwrap(); PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().writable(), |view| { - view.as_contiguous_mut_slice(py); + view.as_contiguous_slice_ptr(); //~^ ERROR: C contiguity is not guaranteed by the requested buffer flags - view.as_fortran_contiguous_mut_slice(py); + view.as_fortran_contiguous_slice_ptr(); //~^ ERROR: Fortran contiguity is not guaranteed by the requested buffer flags }) .unwrap(); PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().c_contiguous().format(), |view| { - view.as_contiguous_mut_slice(py); - //~^ ERROR: writability is not guaranteed by the requested buffer flags + view.as_fortran_contiguous_slice_ptr(); + //~^ ERROR: Fortran contiguity is not guaranteed by the requested buffer flags }) .unwrap(); PyBufferView::::with_flags(&bytes, PyBufferRequest::simple().f_contiguous().format(), |view| { - view.as_fortran_contiguous_mut_slice(py); - //~^ ERROR: writability is not guaranteed by the requested buffer flags + view.as_contiguous_slice_ptr(); + //~^ ERROR: C contiguity is not guaranteed by the requested buffer flags }) .unwrap(); }); diff --git a/tests/ui/invalid_buffer_view_slice.stderr b/tests/ui/invalid_buffer_view_slice.stderr index b54fbe1e7e0..ff8b1a15c70 100644 --- a/tests/ui/invalid_buffer_view_slice.stderr +++ b/tests/ui/invalid_buffer_view_slice.stderr @@ -1,98 +1,98 @@ error[E0277]: C contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:9:18 | - 9 | view.as_contiguous_slice(py); - | ^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` + 9 | view.as_contiguous_slice_ptr(); + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` | = note: use `.c_contiguous()` when building a buffer request to guarantee C contiguity -note: required by a bound in `PyBufferView::::as_contiguous_slice` +note: required by a bound in `PyBufferView::::as_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] - | ------------------- required by a bound in this associated function + | pub fn as_contiguous_slice_ptr(&self) -> NonNull<[T]> + | ----------------------- required by a bound in this associated function | where | Flag: flags::GuaranteesCContiguous, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_slice` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_slice_ptr` error[E0277]: Fortran contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:11:18 | - 11 | view.as_fortran_contiguous_slice(py); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` + 11 | view.as_fortran_contiguous_slice_ptr(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` | = note: use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity -note: required by a bound in `PyBufferView::::as_fortran_contiguous_slice` +note: required by a bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_fortran_contiguous_slice<'a>(&'a self, _py: Python<'a>) -> &'a [ReadOnlyCell] - | --------------------------- required by a bound in this associated function + | pub fn as_fortran_contiguous_slice_ptr(&self) -> NonNull<[T]> + | ------------------------------- required by a bound in this associated function | where | Flag: flags::GuaranteesFContiguous, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_slice` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` error[E0277]: C contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:17:18 | - 17 | view.as_contiguous_mut_slice(py); + 17 | view.as_contiguous_slice_ptr(); | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` | = note: use `.c_contiguous()` when building a buffer request to guarantee C contiguity -note: required by a bound in `PyBufferView::::as_contiguous_mut_slice` +note: required by a bound in `PyBufferView::::as_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | pub fn as_contiguous_slice_ptr(&self) -> NonNull<[T]> | ----------------------- required by a bound in this associated function | where - | Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_mut_slice` + | Flag: flags::GuaranteesCContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_slice_ptr` error[E0277]: Fortran contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:19:18 | - 19 | view.as_fortran_contiguous_mut_slice(py); + 19 | view.as_fortran_contiguous_slice_ptr(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` | = note: use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity -note: required by a bound in `PyBufferView::::as_fortran_contiguous_mut_slice` +note: required by a bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] + | pub fn as_fortran_contiguous_slice_ptr(&self) -> NonNull<[T]> | ------------------------------- required by a bound in this associated function | where - | Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + | Flag: flags::GuaranteesFContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` -error[E0277]: writability is not guaranteed by the requested buffer flags +error[E0277]: Fortran contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:25:18 | - 25 | view.as_contiguous_mut_slice(py); - | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesWritable` is not implemented for `buffer::flags::PyBufferFlags` + 25 | view.as_fortran_contiguous_slice_ptr(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesFContiguous` is not implemented for `buffer::flags::PyBufferFlags` | - = note: use `.writable()` when building a buffer request to guarantee writability -note: required by a bound in `PyBufferView::::as_contiguous_mut_slice` + = note: use `.f_contiguous()` when building a buffer request to guarantee Fortran contiguity +note: required by a bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] - | ----------------------- required by a bound in this associated function + | pub fn as_fortran_contiguous_slice_ptr(&self) -> NonNull<[T]> + | ------------------------------- required by a bound in this associated function | where - | Flag: flags::GuaranteesCContiguous + flags::GuaranteesWritable, - | ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_mut_slice` + | Flag: flags::GuaranteesFContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_slice_ptr` -error[E0277]: writability is not guaranteed by the requested buffer flags +error[E0277]: C contiguity is not guaranteed by the requested buffer flags --> tests/ui/invalid_buffer_view_slice.rs:31:18 | - 31 | view.as_fortran_contiguous_mut_slice(py); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesWritable` is not implemented for `buffer::flags::PyBufferFlags` + 31 | view.as_contiguous_slice_ptr(); + | ^^^^^^^^^^^^^^^^^^^^^^^ the trait `buffer::flags::GuaranteesCContiguous` is not implemented for `buffer::flags::PyBufferFlags` | - = note: use `.writable()` when building a buffer request to guarantee writability -note: required by a bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + = note: use `.c_contiguous()` when building a buffer request to guarantee C contiguity +note: required by a bound in `PyBufferView::::as_contiguous_slice_ptr` --> src/buffer.rs | - | pub fn as_fortran_contiguous_mut_slice<'a>(&'a self, _py: Python<'a>) -> &'a [cell::Cell] - | ------------------------------- required by a bound in this associated function + | pub fn as_contiguous_slice_ptr(&self) -> NonNull<[T]> + | ----------------------- required by a bound in this associated function | where - | Flag: flags::GuaranteesFContiguous + flags::GuaranteesWritable, - | ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_fortran_contiguous_mut_slice` + | Flag: flags::GuaranteesCContiguous, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyBufferView::::as_contiguous_slice_ptr` error: aborting due to 6 previous errors From fad74bea3e6e03b3dac82ed9220e2fda7686fa74 Mon Sep 17 00:00:00 2001 From: "Winston H." Date: Wed, 28 Oct 2026 18:24:39 +0800 Subject: [PATCH 25/25] fix: ignore failing test on WASI --- src/buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buffer.rs b/src/buffer.rs index d5efa43dc23..5ead4243e89 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1968,7 +1968,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, target_os = "wasi")))] fn test_buffer_view_rejects_null_data() { Python::attach(|py| { let empty = py