You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by ti...@apache.org on 2026/07/31 01:09:46 UTC
(opendal) branch main updated: perf(bindings/dotnet): copy read payloads once (#7993)
This is an automated email from the ASF dual-hosted git repository.
tisonkun 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 086e9853e perf(bindings/dotnet): copy read payloads once (#7993)
086e9853e is described below
commit 086e9853efb00ad92d27200874f1e9b829d56125
Author: Fatorin <sk...@gmail.com>
AuthorDate: Fri Jul 31 09:09:38 2026 +0800
perf(bindings/dotnet): copy read payloads once (#7993)
---
bindings/dotnet/Cargo.toml | 5 +
.../OpenDAL.Tests/Behavior/ReadBehaviorTest.cs | 40 +++++
.../NativeObject/OpenDALBuffer.cs} | 44 ++++--
.../OpenDAL/Interop/Result/OpenDALReadResult.cs | 3 +-
bindings/dotnet/OpenDAL/NativeMethods.cs | 15 +-
bindings/dotnet/OpenDAL/Operator.cs | 29 ++--
bindings/dotnet/src/buffer.rs | 171 +++++++++++++++++++++
bindings/dotnet/src/byte_buffer.rs | 95 ------------
bindings/dotnet/src/lib.rs | 2 +-
bindings/dotnet/src/operator.rs | 37 +++--
bindings/dotnet/src/result.rs | 22 +--
11 files changed, 302 insertions(+), 161 deletions(-)
diff --git a/bindings/dotnet/Cargo.toml b/bindings/dotnet/Cargo.toml
index 717a17448..89adee7ab 100644
--- a/bindings/dotnet/Cargo.toml
+++ b/bindings/dotnet/Cargo.toml
@@ -106,6 +106,11 @@ opendal = { version = ">=0", path = "../../core", features = [
] }
tokio = { version = "1.49.0", features = ["full"] }
+[dev-dependencies]
+# Only needed to build multi-part buffers in the buffer copy tests. Already an
+# indirect dependency through opendal, so this adds nothing to the shipped library.
+bytes = "1.12.1"
+
# This is not optimal. See also the Cargo issue:
# https://github.com/rust-lang/cargo/issues/1197#issuecomment-1641086954
[target.'cfg(unix)'.dependencies.opendal]
diff --git a/bindings/dotnet/OpenDAL.Tests/Behavior/ReadBehaviorTest.cs b/bindings/dotnet/OpenDAL.Tests/Behavior/ReadBehaviorTest.cs
index 4aa72ddf1..ce4318173 100644
--- a/bindings/dotnet/OpenDAL.Tests/Behavior/ReadBehaviorTest.cs
+++ b/bindings/dotnet/OpenDAL.Tests/Behavior/ReadBehaviorTest.cs
@@ -65,6 +65,46 @@ public sealed class ReadBehaviorTest : BehaviorTestBase
Assert.Equal(content, actual);
}
+ [Fact]
+ public void ReadBehavior_LargePayload_RoundTripsExactly()
+ {
+ if (!Supports(c => c.Read && c.Write))
+ {
+ return;
+ }
+
+ // Covers the whole path end to end at a size worth measuring. The chunk
+ // walking itself is pinned by the unit tests in src/buffer.rs, which build
+ // multi-part buffers a backend may or may not produce.
+ const int size = 8 * 1024 * 1024;
+ var path = NewPath("read-large");
+ var content = RandomBytes(size);
+
+ Op.Write(path, content);
+ var actual = Op.Read(path);
+
+ Assert.Equal(content.Length, actual.Length);
+ Assert.True(content.AsSpan().SequenceEqual(actual));
+ }
+
+ [Fact]
+ public async Task ReadBehavior_LargePayload_RoundTripsExactlyAsync()
+ {
+ if (!Supports(c => c.Read && c.Write))
+ {
+ return;
+ }
+
+ var path = NewPath("read-large-async");
+ var content = RandomBytes(8 * 1024 * 1024);
+
+ await Op.WriteAsync(path, content, CT);
+ var actual = await Op.ReadAsync(path, CT);
+
+ Assert.Equal(content.Length, actual.Length);
+ Assert.True(content.AsSpan().SequenceEqual(actual));
+ }
+
[Fact]
public void ReadBehavior_WithRange_ReturnsExpectedBytes()
{
diff --git a/bindings/dotnet/OpenDAL/ByteBuffer.cs b/bindings/dotnet/OpenDAL/Interop/NativeObject/OpenDALBuffer.cs
similarity index 52%
rename from bindings/dotnet/OpenDAL/ByteBuffer.cs
rename to bindings/dotnet/OpenDAL/Interop/NativeObject/OpenDALBuffer.cs
index 8bfab4865..33d8873c9 100644
--- a/bindings/dotnet/OpenDAL/ByteBuffer.cs
+++ b/bindings/dotnet/OpenDAL/Interop/NativeObject/OpenDALBuffer.cs
@@ -19,42 +19,56 @@
using System.Runtime.InteropServices;
-namespace OpenDAL;
+namespace OpenDAL.Interop.NativeObject;
[StructLayout(LayoutKind.Sequential)]
/// <summary>
-/// FFI representation of a Rust byte buffer.
+/// Read payload still owned by the native side.
/// </summary>
-public struct ByteBuffer
+/// <remarks>
+/// Holds the native buffer itself rather than a flattened copy, so
+/// <see cref="ToManagedBytes"/> can copy straight into its final array. Released
+/// through the owning result's release call, not from here.
+/// </remarks>
+internal struct OpenDALBuffer
{
/// <summary>
- /// Pointer to the first byte in unmanaged memory.
+ /// Opaque handle to the native buffer, or zero when there is no payload.
/// </summary>
- public IntPtr Data;
+ public IntPtr Handle;
/// <summary>
- /// Number of valid bytes in <see cref="Data"/>.
+ /// Total readable bytes across every chunk.
/// </summary>
public nuint Len;
/// <summary>
- /// Total allocated capacity in bytes.
- /// </summary>
- public nuint Capacity;
-
- /// <summary>
- /// Copies the unmanaged bytes into a managed array.
+ /// Copies the payload into a new managed array.
/// </summary>
+ /// <returns>The payload, or an empty array when there is none.</returns>
public readonly unsafe byte[] ToManagedBytes()
{
- if (Data == IntPtr.Zero || Len == 0)
+ if (Handle == IntPtr.Zero || Len == 0)
{
return Array.Empty<byte>();
}
var size = checked((int)Len);
var managed = GC.AllocateUninitializedArray<byte>(size);
- new ReadOnlySpan<byte>((void*)Data, size).CopyTo(managed);
+ nuint written;
+ fixed (byte* destination = managed)
+ {
+ written = NativeMethods.buffer_copy_to(Handle, destination, (nuint)size);
+ }
+
+ // The array is uninitialized, so a short copy would hand back whatever the
+ // heap held rather than the payload.
+ if (written != (nuint)size)
+ {
+ throw new InvalidOperationException(
+ $"Native buffer reported {Len} bytes but produced {written}.");
+ }
+
return managed;
}
-}
\ No newline at end of file
+}
diff --git a/bindings/dotnet/OpenDAL/Interop/Result/OpenDALReadResult.cs b/bindings/dotnet/OpenDAL/Interop/Result/OpenDALReadResult.cs
index 33b89fa21..fdb92ad28 100644
--- a/bindings/dotnet/OpenDAL/Interop/Result/OpenDALReadResult.cs
+++ b/bindings/dotnet/OpenDAL/Interop/Result/OpenDALReadResult.cs
@@ -18,6 +18,7 @@
*/
using System.Runtime.InteropServices;
+using OpenDAL.Interop.NativeObject;
using OpenDAL.Interop.Result.Abstractions;
namespace OpenDAL.Interop.Result;
@@ -31,7 +32,7 @@ internal struct OpenDALReadResult : INativeValueResult<byte[]>
/// <summary>
/// Byte buffer payload on success.
/// </summary>
- public ByteBuffer Buffer;
+ public OpenDALBuffer Buffer;
/// <summary>
/// Error details for the operation.
diff --git a/bindings/dotnet/OpenDAL/NativeMethods.cs b/bindings/dotnet/OpenDAL/NativeMethods.cs
index 2b397bd81..0efa14d1d 100644
--- a/bindings/dotnet/OpenDAL/NativeMethods.cs
+++ b/bindings/dotnet/OpenDAL/NativeMethods.cs
@@ -187,6 +187,18 @@ internal partial class NativeMethods
#endregion
+ #region Buffer
+
+ [LibraryImport(__DllName, EntryPoint = "buffer_copy_to")]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ internal static unsafe partial nuint buffer_copy_to(
+ IntPtr handle,
+ byte* destination,
+ nuint destinationLen
+ );
+
+ #endregion
+
#region IO Operations
#region Write
@@ -208,7 +220,8 @@ internal partial class NativeMethods
Operator op,
IntPtr executor,
string path,
- ByteBuffer data,
+ [In] byte[] data,
+ nuint len,
IntPtr options,
delegate* unmanaged[Cdecl]<long, OpenDALResult, void> callback,
long context
diff --git a/bindings/dotnet/OpenDAL/Operator.cs b/bindings/dotnet/OpenDAL/Operator.cs
index 8bb04631e..0668fa46d 100644
--- a/bindings/dotnet/OpenDAL/Operator.cs
+++ b/bindings/dotnet/OpenDAL/Operator.cs
@@ -269,25 +269,16 @@ public partial class Operator : SafeHandle
{
unsafe
{
- fixed (byte* ptr = content)
- {
- var buffer = new ByteBuffer
- {
- Data = (IntPtr)ptr,
- Len = (nuint)content.Length,
- Capacity = (nuint)content.Length,
- };
-
- return NativeMethods.operator_write_with_options_async(
- this,
- executorHandle,
- path,
- buffer,
- optionsHandle,
- &OnWriteCompleted,
- context
- );
- }
+ return NativeMethods.operator_write_with_options_async(
+ this,
+ executorHandle,
+ path,
+ content,
+ (nuint)content.Length,
+ optionsHandle,
+ &OnWriteCompleted,
+ context
+ );
}
}
}
diff --git a/bindings/dotnet/src/buffer.rs b/bindings/dotnet/src/buffer.rs
new file mode 100644
index 000000000..0a015d82e
--- /dev/null
+++ b/bindings/dotnet/src/buffer.rs
@@ -0,0 +1,171 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Read payloads travelling from this crate to the caller.
+//!
+//! Bytes going the other way, such as write payloads, are passed as a plain
+//! pointer and length, because the caller already owns that memory.
+
+use std::ffi::c_void;
+use std::ptr;
+
+#[repr(C)]
+/// Read payload owned by this crate.
+///
+/// Holds the [`opendal::Buffer`] itself rather than a flattened copy, so the
+/// caller can copy straight into its final destination. `handle` must be
+/// released with `opendal_read_result_release` exactly once.
+pub struct OpendalBuffer {
+ /// Leaked `Box<opendal::Buffer>`, or null when there is no payload.
+ pub handle: *mut c_void,
+ /// Total readable bytes across every chunk.
+ pub len: usize,
+}
+
+impl OpendalBuffer {
+ /// Create an empty payload that does not own anything.
+ pub fn empty() -> Self {
+ Self {
+ handle: ptr::null_mut(),
+ len: 0,
+ }
+ }
+
+ /// Take ownership of a buffer without flattening or copying it.
+ pub fn from_buffer(value: opendal::Buffer) -> Self {
+ let len = value.len();
+ if len == 0 {
+ return Self::empty();
+ }
+
+ Self {
+ handle: Box::into_raw(Box::new(value)) as *mut c_void,
+ len,
+ }
+ }
+}
+
+/// Copy the whole payload into `dest`, returning the number of bytes written.
+///
+/// Copies at most `dest_len` bytes. An `opendal::Buffer` may be split across
+/// several chunks, so this walks them and writes each one directly into the
+/// destination; that is the only copy a read performs.
+/// # Safety
+///
+/// - `handle` must be null or come from `OpendalBuffer::from_buffer`.
+/// - `dest` must be valid for writes of `dest_len` bytes.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn buffer_copy_to(
+ handle: *const c_void,
+ dest: *mut u8,
+ dest_len: usize,
+) -> usize {
+ if handle.is_null() || dest.is_null() || dest_len == 0 {
+ return 0;
+ }
+
+ // Iterating consumes the buffer, so work on a clone. `Buffer` is backed by
+ // `Bytes`, meaning this only bumps reference counts.
+ let chunks = unsafe { &*(handle as *const opendal::Buffer) }.clone();
+
+ let mut written = 0usize;
+ for chunk in chunks {
+ if written == dest_len {
+ break;
+ }
+
+ let take = chunk.len().min(dest_len - written);
+ unsafe {
+ ptr::copy_nonoverlapping(chunk.as_ptr(), dest.add(written), take);
+ }
+ written += take;
+ }
+
+ written
+}
+
+/// # Safety
+///
+/// - `handle` must be null or come from `OpendalBuffer::from_buffer`.
+/// - This function must be called at most once for the same handle.
+/// - Callers must not use `handle` after this function returns.
+pub unsafe fn buffer_free(handle: *mut c_void) {
+ if handle.is_null() {
+ return;
+ }
+
+ drop(unsafe { Box::from_raw(handle as *mut opendal::Buffer) });
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Three parts, so the copy has to advance the destination across chunks.
+ /// Backends that hand back a single contiguous buffer never exercise this.
+ fn split_buffer() -> opendal::Buffer {
+ opendal::Buffer::from(vec![
+ bytes::Bytes::from_static(b"abc"),
+ bytes::Bytes::from_static(b"de"),
+ bytes::Bytes::from_static(b"fgh"),
+ ])
+ }
+
+ fn copy(buffer: &opendal::Buffer, dest: &mut [u8]) -> usize {
+ let owned = OpendalBuffer::from_buffer(buffer.clone());
+ let written = unsafe { buffer_copy_to(owned.handle, dest.as_mut_ptr(), dest.len()) };
+ unsafe { buffer_free(owned.handle) };
+ written
+ }
+
+ #[test]
+ fn copies_every_chunk_in_order() {
+ let buffer = split_buffer();
+ let mut dest = vec![0u8; buffer.len()];
+
+ assert_eq!(copy(&buffer, &mut dest), 8);
+ assert_eq!(&dest, b"abcdefgh");
+ }
+
+ #[test]
+ fn stops_at_the_destination_length() {
+ let mut dest = vec![0u8; 4];
+
+ assert_eq!(copy(&split_buffer(), &mut dest), 4);
+ assert_eq!(&dest, b"abcd");
+ }
+
+ #[test]
+ fn honors_a_slice_that_starts_mid_chunk() {
+ // Ranged reads produce exactly this: a non-contiguous buffer whose logical
+ // start sits inside the second part.
+ let buffer = split_buffer().slice(4..7);
+ let mut dest = vec![0u8; buffer.len()];
+
+ assert_eq!(copy(&buffer, &mut dest), 3);
+ assert_eq!(&dest, b"efg");
+ }
+
+ #[test]
+ fn empty_payload_owns_nothing() {
+ let owned = OpendalBuffer::from_buffer(opendal::Buffer::new());
+
+ assert!(owned.handle.is_null());
+ assert_eq!(owned.len, 0);
+ unsafe { buffer_free(owned.handle) };
+ }
+}
diff --git a/bindings/dotnet/src/byte_buffer.rs b/bindings/dotnet/src/byte_buffer.rs
deleted file mode 100644
index d3e8c2678..000000000
--- a/bindings/dotnet/src/byte_buffer.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-#[repr(C)]
-/// FFI-safe representation of a Rust `Vec<u8>` buffer.
-///
-/// The buffer ownership is transferred to the caller and must be released by
-/// calling `opendal_read_result_release` exactly once.
-pub struct ByteBuffer {
- /// Pointer to the start of the allocated bytes.
- pub data: *mut u8,
- /// Number of initialized bytes.
- pub len: usize,
- /// Total allocated capacity in bytes.
- pub capacity: usize,
-}
-
-impl ByteBuffer {
- /// Create an empty buffer that does not own any allocation.
- pub fn empty() -> Self {
- Self {
- data: std::ptr::null_mut(),
- len: 0,
- capacity: 0,
- }
- }
-
- /// Convert a vector into a raw FFI buffer without copying.
- ///
- /// The returned memory must be released by the C# side via
- /// `opendal_read_result_release`.
- pub fn from_vec(mut value: Vec<u8>) -> Self {
- if value.is_empty() {
- return Self::empty();
- }
-
- let data = value.as_mut_ptr();
- let len = value.len();
- let capacity = value.capacity();
- std::mem::forget(value);
-
- Self {
- data,
- len,
- capacity,
- }
- }
-}
-
-/// # Safety
-///
-/// - `data`, `len`, and `capacity` must come from `ByteBuffer::from_vec`.
-/// - This function must be called at most once for the same allocation.
-/// - Callers must not access `data` after this function returns.
-pub unsafe fn buffer_free(data: *mut u8, len: usize, capacity: usize) {
- if data.is_null() {
- debug_assert_eq!(len, 0, "len must be zero when data is null");
- debug_assert_eq!(capacity, 0, "capacity must be zero when data is null");
- return;
- }
-
- if capacity == 0 {
- debug_assert!(
- capacity > 0,
- "capacity must be greater than zero when data is not null"
- );
- return;
- }
-
- if capacity < len {
- debug_assert!(
- capacity >= len,
- "capacity must be greater than or equal to len"
- );
- return;
- }
-
- unsafe {
- drop(Vec::from_raw_parts(data, len, capacity));
- }
-}
diff --git a/bindings/dotnet/src/lib.rs b/bindings/dotnet/src/lib.rs
index 42c80c680..88b9a5331 100644
--- a/bindings/dotnet/src/lib.rs
+++ b/bindings/dotnet/src/lib.rs
@@ -20,7 +20,7 @@
//! This crate exposes `extern "C"` APIs consumed by C# via P/Invoke and keeps
//! interop memory ownership explicit through dedicated release functions.
-mod byte_buffer;
+mod buffer;
mod capability;
mod entry;
mod error;
diff --git a/bindings/dotnet/src/operator.rs b/bindings/dotnet/src/operator.rs
index 4faccd691..656689a61 100644
--- a/bindings/dotnet/src/operator.rs
+++ b/bindings/dotnet/src/operator.rs
@@ -16,7 +16,7 @@
// under the License.
use crate::{
- byte_buffer::ByteBuffer,
+ buffer::OpendalBuffer,
entry::into_entry_list_ptr,
error::OpenDALError,
executor::executor_or_default,
@@ -1305,7 +1305,7 @@ pub extern "C" fn operator_input_stream_read_next(
fn operator_input_stream_read_next_inner(
stream: *mut opendal::blocking::StdBytesIterator,
-) -> Result<ByteBuffer, OpenDALError> {
+) -> Result<OpendalBuffer, OpenDALError> {
if stream.is_null() {
return Err(crate::utils::config_invalid_error(
"input stream pointer is null",
@@ -1323,8 +1323,8 @@ fn operator_input_stream_read_next_inner(
err.to_string(),
))
})?
- .map(|v| ByteBuffer::from_vec(v.to_vec()))
- .unwrap_or_else(ByteBuffer::empty);
+ .map(|v| OpendalBuffer::from_buffer(v.into()))
+ .unwrap_or_else(OpendalBuffer::empty);
Ok(value)
}
@@ -1564,31 +1564,37 @@ fn operator_write_with_options_inner(
///
/// - `op` must be a valid operator pointer from `operator_construct`.
/// - `path` must be a valid null-terminated UTF-8 string.
-/// - `data.data` must be non-null and readable for `data.len` bytes when `data.len > 0`.
+/// - `data` must be non-null and readable for `len` bytes when `len > 0`.
/// - `callback` must be a valid function pointer and remain callable until invoked.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn operator_write_with_options_async(
op: *const opendal::Operator,
executor: *const c_void,
path: *const c_char,
- data: ByteBuffer,
+ data: *const u8,
+ len: usize,
options: *const opendal::options::WriteOptions,
callback: Option<WriteCallback>,
context: i64,
) -> OpendalResult {
match operator_write_with_options_async_inner(
- op, executor, path, data, options, callback, context,
+ op, executor, path, data, len, options, callback, context,
) {
Ok(()) => OpendalResult::ok(),
Err(error) => OpendalResult::from_error(error),
}
}
+// Mirrors the `extern "C"` signature above, which clippy exempts because a C ABI
+// is not a design choice. This helper exists only to give that function a body it
+// can write with `?`.
+#[allow(clippy::too_many_arguments)]
fn operator_write_with_options_async_inner(
op: *const opendal::Operator,
executor: *const c_void,
path: *const c_char,
- data: ByteBuffer,
+ data: *const u8,
+ len: usize,
options: *const opendal::options::WriteOptions,
callback: Option<WriteCallback>,
context: i64,
@@ -1596,7 +1602,7 @@ fn operator_write_with_options_async_inner(
let op = require_operator(op)?;
let executor = executor_or_default(executor)?;
let path = require_cstr(path, "path")?.to_string();
- require_data_ptr(data.data.cast_const(), data.len)?;
+ require_data_ptr(data, len)?;
let callback = require_callback(callback)?;
let options = if options.is_null() {
opendal::options::WriteOptions::default()
@@ -1604,10 +1610,12 @@ fn operator_write_with_options_async_inner(
unsafe { (&*options).clone() }
};
- let payload = if data.len == 0 {
+ // Copied before the task is spawned, so the caller only has to keep its
+ // array alive for the duration of this call.
+ let payload = if len == 0 {
Vec::new()
} else {
- unsafe { std::slice::from_raw_parts(data.data.cast_const(), data.len) }.to_vec()
+ unsafe { std::slice::from_raw_parts(data, len) }.to_vec()
};
let op = op.clone();
@@ -1656,7 +1664,7 @@ fn operator_read_with_options_inner(
executor: *const c_void,
path: *const c_char,
options: *const opendal::options::ReadOptions,
-) -> Result<ByteBuffer, OpenDALError> {
+) -> Result<OpendalBuffer, OpenDALError> {
let op = require_operator(op)?;
let executor = executor_or_default(executor)?;
let path = require_cstr(path, "path")?;
@@ -1668,10 +1676,9 @@ fn operator_read_with_options_inner(
let value = executor
.block_on(op.read_options(path, options))
- .map(|v| v.to_vec())
.map_err(OpenDALError::from_opendal_error)?;
- Ok(ByteBuffer::from_vec(value))
+ Ok(OpendalBuffer::from_buffer(value))
}
/// Read bytes from `path` asynchronously with options.
@@ -1722,7 +1729,7 @@ fn operator_read_with_options_async_inner(
let result = op
.read_options(&path, options)
.await
- .map(|v| ByteBuffer::from_vec(v.to_vec()))
+ .map(OpendalBuffer::from_buffer)
.map_err(OpenDALError::from_opendal_error);
callback(
diff --git a/bindings/dotnet/src/result.rs b/bindings/dotnet/src/result.rs
index 9568246cf..061d701bd 100644
--- a/bindings/dotnet/src/result.rs
+++ b/bindings/dotnet/src/result.rs
@@ -17,7 +17,7 @@
use std::ffi::c_void;
-use crate::byte_buffer::{ByteBuffer, buffer_free};
+use crate::buffer::{OpendalBuffer, buffer_free};
use crate::entry::entry_list_free;
use crate::error::OpenDALError;
use crate::metadata::metadata_free;
@@ -75,7 +75,7 @@ pub struct OpendalEntryListResult {
#[repr(C)]
/// Result for operations returning a byte buffer payload.
pub struct OpendalReadResult {
- pub buffer: ByteBuffer,
+ pub buffer: OpendalBuffer,
pub error: OpenDALError,
}
@@ -164,8 +164,8 @@ define_result!(
define_result!(
OpendalReadResult,
- field = buffer: ByteBuffer,
- error_value = ByteBuffer::empty()
+ field = buffer: OpendalBuffer,
+ error_value = OpendalBuffer::empty()
);
define_result!(
@@ -255,18 +255,12 @@ pub extern "C" fn opendal_entry_list_result_release(mut result: OpendalEntryList
/// This function is idempotent for empty/null buffers.
/// # Safety
///
-/// - `result.buffer` must originate from `ByteBuffer::from_vec` in this crate.
-/// - The buffer memory must not be accessed after this call.
+/// - `result.buffer` must originate from `OpendalBuffer::from_buffer` in this crate.
+/// - The buffer must not be accessed after this call.
#[unsafe(no_mangle)]
pub extern "C" fn opendal_read_result_release(mut result: OpendalReadResult) {
- if !result.buffer.data.is_null() {
- unsafe {
- buffer_free(
- result.buffer.data,
- result.buffer.len,
- result.buffer.capacity,
- );
- }
+ unsafe {
+ buffer_free(result.buffer.handle);
}
release_error_message(&mut result.error);