From ddec6dd6c13a88068ccda61f0ea5d3ed6e7a13f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 2 Apr 2026 12:59:27 +0000 Subject: [PATCH] Add PyMemoryView::from_owned_buffer for zero-copy memoryview creation Adds a new method to create a Python memoryview that exposes a read-only view of byte data owned by a frozen PyClass instance without copying. This is useful for libraries like pyca/cryptography that need to expose internal buffers efficiently. The method uses PyBuffer_FillInfo + PyMemoryView_FromBuffer to create a memoryview backed by the owner's data, with the owner kept alive via the buffer's obj reference. Safety is enforced at compile time: - T: PyClass prevents mutation that could invalidate pointers - for<'a> FnOnce(&'a T) -> &'a [u8] ensures the slice borrows from T or is 'static Closes #5871 https://claude.ai/code/session_01EEP1DaqJwHGCoNufi2JT9H --- newsfragments/5937.added.md | 1 + src/types/memoryview.rs | 129 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 newsfragments/5937.added.md diff --git a/newsfragments/5937.added.md b/newsfragments/5937.added.md new file mode 100644 index 00000000000..44e7d38e587 --- /dev/null +++ b/newsfragments/5937.added.md @@ -0,0 +1 @@ +Added `PyMemoryView::from_owned_buffer` to create a read-only `memoryview` from data owned by a frozen `PyClass` instance without copying. diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 9fd245b4691..53660411ab3 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -22,6 +22,76 @@ impl PyMemoryView { .cast_into_unchecked() } } + + /// Creates a new Python `memoryview` that exposes a read-only view of the + /// byte data owned by a frozen `PyClass` instance, without copying. + /// + /// `getbuf` is a closure that receives `T` borrowed from `owner` and + /// returns the byte slice to expose. The higher-ranked lifetime ensures + /// the slice is derived from `T` (or is `'static`), preventing dangling + /// pointers. `T` must be a `frozen` pyclass to guarantee the byte slice + /// cannot be mutated. + /// + /// # Example + /// + /// ```rust + /// # #[cfg(any(Py_3_11, not(Py_LIMITED_API)))] + /// # { + /// use pyo3::prelude::*; + /// use pyo3::types::PyMemoryView; + /// + /// #[pyclass(frozen)] + /// struct MyData { + /// data: Vec, + /// } + /// + /// Python::attach(|py| { + /// let obj = Bound::new(py, MyData { data: vec![1, 2, 3] }).unwrap(); + /// let view = PyMemoryView::from_owned_buffer(&obj, |data| &data.data).unwrap(); + /// assert_eq!(view.len().unwrap(), 3); + /// }); + /// # } + /// ``` + #[cfg(any(Py_3_11, not(Py_LIMITED_API)))] + pub fn from_owned_buffer<'py, T>( + owner: &Bound<'py, T>, + getbuf: impl for<'a> FnOnce(&'a T) -> &'a [u8], + ) -> PyResult> + where + T: crate::PyClass + Sync, + { + let py = owner.py(); + let buf = getbuf(owner.get()); + + let mut view = std::mem::MaybeUninit::::uninit(); + + // SAFETY: `view` points to a valid (uninitialized) `Py_buffer`. + // `PyBuffer_FillInfo` fully initializes every field on success, and + // increfs `owner` into `view.obj`. `owner` outlives the call because + // it is held on the stack by `Bound`. + let rc = unsafe { + ffi::PyBuffer_FillInfo( + view.as_mut_ptr(), + owner.as_ptr(), + buf.as_ptr() as *mut std::ffi::c_void, + buf.len() as ffi::Py_ssize_t, + 1, // readonly + ffi::PyBUF_FULL_RO, + ) + }; + crate::err::error_on_minusone(py, rc)?; + + // SAFETY: `PyBuffer_FillInfo` returned success, so `view` is now + // fully initialized. + let view = unsafe { view.assume_init() }; + + // SAFETY: `view` is a fully initialized `Py_buffer`, as required. + unsafe { + ffi::PyMemoryView_FromBuffer(&view) + .assume_owned_or_err(py) + .cast_into_unchecked() + } + } } impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { @@ -33,3 +103,62 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { PyMemoryView::from(value) } } + +#[cfg(all(test, feature = "macros", any(Py_3_11, not(Py_LIMITED_API))))] +mod tests { + use super::*; + use crate::types::PyAnyMethods; + use crate::{Bound, Python}; + + #[crate::pyclass(frozen, crate = "crate")] + struct ByteOwner { + data: Vec, + } + + #[test] + fn test_from_owned_buffer_basic() { + Python::attach(|py| { + let owner = Bound::new( + py, + ByteOwner { + data: vec![1, 2, 3, 4, 5], + }, + ) + .unwrap(); + let view = PyMemoryView::from_owned_buffer(&owner, |o| &o.data).unwrap(); + assert_eq!(view.len().unwrap(), 5); + let bytes: Vec = view.call_method0("tobytes").unwrap().extract().unwrap(); + assert_eq!(bytes, vec![1, 2, 3, 4, 5]); + }); + } + + #[test] + fn test_from_owned_buffer_readonly() { + Python::attach(|py| { + let owner = Bound::new(py, ByteOwner { data: vec![42] }).unwrap(); + let view = PyMemoryView::from_owned_buffer(&owner, |o| &o.data).unwrap(); + let readonly: bool = view.getattr("readonly").unwrap().extract().unwrap(); + assert!(readonly); + }); + } + + #[test] + fn test_from_owned_buffer_empty() { + Python::attach(|py| { + let owner = Bound::new(py, ByteOwner { data: vec![] }).unwrap(); + let view = PyMemoryView::from_owned_buffer(&owner, |o| &o.data).unwrap(); + assert_eq!(view.len().unwrap(), 0); + }); + } + + #[test] + fn test_from_owned_buffer_static_data() { + Python::attach(|py| { + let owner = Bound::new(py, ByteOwner { data: vec![] }).unwrap(); + let view = + PyMemoryView::from_owned_buffer(&owner, |_o| b"static data" as &[u8]).unwrap(); + let bytes: Vec = view.call_method0("tobytes").unwrap().extract().unwrap(); + assert_eq!(bytes, b"static data"); + }); + } +}