You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/08/26 15:00:58 UTC

(opendal) branch main updated: perf(bindings/python): avoid an extra copy when reading bytes (#8160)

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 053739c76 perf(bindings/python): avoid an extra copy when reading bytes (#8160)
053739c76 is described below

commit 053739c76bfa8b127e62cdb8efb52b86130a059f
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Wed Aug 26 23:00:52 2026 +0800

    perf(bindings/python): avoid an extra copy when reading bytes (#8160)
    
    perf(bindings/python): avoid extra read copy
---
 bindings/python/src/operator.rs | 10 +++-----
 bindings/python/src/utils.rs    | 56 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 60 insertions(+), 6 deletions(-)

diff --git a/bindings/python/src/operator.rs b/bindings/python/src/operator.rs
index 7f017f81c..ebdcea662 100644
--- a/bindings/python/src/operator.rs
+++ b/bindings/python/src/operator.rs
@@ -472,10 +472,9 @@ impl Operator {
         let buffer = self
             .core
             .read_options(&path, opts.into())
-            .map_err(format_pyerr)?
-            .to_vec();
+            .map_err(format_pyerr)?;
 
-        Buffer::new(buffer).into_bytes_ref(py)
+        Ok(buffer_into_py_bytes(py, buffer)?.into_any())
     }
 
     /// Write bytes to a file at the given path.
@@ -1219,9 +1218,8 @@ impl AsyncOperator {
                 .map_err(format_pyerr)?
                 .read(range.to_range())
                 .await
-                .map_err(format_pyerr)?
-                .to_vec();
-            Python::attach(|py| Buffer::new(res).into_bytes(py))
+                .map_err(format_pyerr)?;
+            Python::attach(|py| buffer_into_py_bytes(py, res).map(Bound::unbind))
         })
     }
 
diff --git a/bindings/python/src/utils.rs b/bindings/python/src/utils.rs
index dd86ce358..bbd0d3563 100644
--- a/bindings/python/src/utils.rs
+++ b/bindings/python/src/utils.rs
@@ -17,9 +17,65 @@
 
 use std::os::raw::c_int;
 
+use bytes::Buf;
 use pyo3::IntoPyObjectExt;
+use pyo3::exceptions::PyOverflowError;
 use pyo3::ffi;
 use pyo3::prelude::*;
+use pyo3::types::PyBytes;
+
+use crate::ocore;
+
+/// Copy an OpenDAL buffer directly into a new Python `bytes` object.
+pub fn buffer_into_py_bytes<'py>(
+    py: Python<'py>,
+    mut buffer: ocore::Buffer,
+) -> PyResult<Bound<'py, PyBytes>> {
+    let len = buffer.remaining();
+    let py_len = len
+        .try_into()
+        .map_err(|_| PyOverflowError::new_err("buffer is too large for Python bytes"))?;
+
+    unsafe {
+        let bytes = Bound::from_owned_ptr_or_err(
+            py,
+            ffi::PyBytes_FromStringAndSize(std::ptr::null(), py_len),
+        )?
+        .cast_into_unchecked::<PyBytes>();
+
+        // PyBytes_FromStringAndSize with a null source allocates an uninitialized
+        // buffer. `buffer` has exactly `len` remaining bytes, so copy_to_slice
+        // initializes the entire Python object before it becomes observable.
+        let dst =
+            std::slice::from_raw_parts_mut(ffi::PyBytes_AsString(bytes.as_ptr()).cast::<u8>(), len);
+        buffer.copy_to_slice(dst);
+
+        Ok(bytes)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use bytes::Bytes;
+    use pyo3::types::PyBytesMethods;
+
+    use super::*;
+
+    #[test]
+    fn test_buffer_into_py_bytes() {
+        Python::initialize();
+        Python::attach(|py| {
+            let buffer = [Bytes::from_static(b"hello, "), Bytes::from_static(b"world")]
+                .into_iter()
+                .collect();
+            let result = buffer_into_py_bytes(py, buffer).unwrap();
+            assert_eq!(result.as_bytes(), b"hello, world");
+
+            let result = buffer_into_py_bytes(py, ocore::Buffer::new()).unwrap();
+            assert!(result.as_bytes().is_empty());
+        });
+    }
+}
 
 /// A bytes-like object that implements buffer protocol.
 #[pyclass(module = "opendal")]