blob: 18762a217a606a295e072c196bd012a491e32b10 [file]
// 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.
//! Native payload buffers crossing the FFI boundary in both directions.
//!
//! Read payloads keep the [`opendal::Buffer`] behind a handle so the caller
//! copies (or views) chunks straight from native memory. Write buffers grow
//! segment by segment — segments never move, so handed-out pointers stay
//! valid — and a write consumes them, frozen into one
//! non-contiguous buffer without copying.
use std::ffi::c_void;
use std::mem::MaybeUninit;
use std::ptr;
use crate::chunk::OpendalChunk;
use crate::error::OpenDALError;
use crate::result::OpendalWriteBufferResult;
use crate::utils::config_invalid_error;
#[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 OpendalReadBuffer {
/// 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 OpendalReadBuffer {
/// 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 payload from `src_offset` onwards 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. An offset at or past
/// the end of the payload writes nothing.
/// # Safety
///
/// - `handle` must be null or come from `OpendalReadBuffer::from_buffer`.
/// - `dest` must be valid for writes of `dest_len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn read_buffer_copy_to(
handle: *const c_void,
src_offset: usize,
dest: *mut u8,
dest_len: usize,
) -> usize {
if handle.is_null() || dest.is_null() || dest_len == 0 {
return 0;
}
let buffer = unsafe { &*(handle as *const opendal::Buffer) };
if src_offset >= buffer.len() {
return 0;
}
// Slicing bumps reference counts on the backing `Bytes` instead of
// copying; iterating then consumes the sliced view, not the original.
let chunks = buffer.slice(src_offset..);
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
}
/// Enumerate the chunks of a read payload without copying.
///
/// Fills `out` with up to `cap` descriptors and returns the total number of
/// chunks; call with `cap` 0 first to size the array. Descriptor pointers
/// stay valid until the owning handle is released.
/// # Safety
///
/// - `handle` must be null or come from `OpendalReadBuffer::from_buffer`.
/// - When `cap > 0`, `out` must be valid for writes of `cap` descriptors.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn read_buffer_chunks(
handle: *const c_void,
out: *mut OpendalChunk,
cap: usize,
) -> usize {
if handle.is_null() {
return 0;
}
let buffer = unsafe { &*(handle as *const opendal::Buffer) };
// Iterated chunks are `Bytes` clones sharing storage with the handle's
// buffer, so the recorded pointers outlive this call.
let mut count = 0usize;
for chunk in buffer.clone() {
if count < cap && !out.is_null() {
unsafe {
*out.add(count) = OpendalChunk {
data: chunk.as_ptr(),
len: chunk.len(),
};
}
}
count += 1;
}
count
}
/// # Safety
///
/// - `handle` must be null or come from `OpendalReadBuffer::from_buffer`.
/// - This function must be called at most once for the same handle.
/// - Callers must not use `handle` after this function returns.
pub(crate) unsafe fn read_buffer_free(handle: *mut c_void) {
if handle.is_null() {
return;
}
drop(unsafe { Box::from_raw(handle as *mut opendal::Buffer) });
}
#[repr(C)]
/// Writable segment handed to the caller.
///
/// `data` points at `capacity` writable bytes owned by `handle`. The handle
/// must be released exactly once with `write_buffer_free`, whether or not its
/// contents were consumed.
pub struct OpendalWriteBuffer {
/// Leaked `Box<WriteBufferSlot>` owning every segment.
pub handle: *mut c_void,
/// Raw pointer to the newest segment, valid until the handle is freed.
pub data: *mut u8,
/// Writable bytes behind `data`.
pub capacity: usize,
}
impl OpendalWriteBuffer {
pub fn empty() -> Self {
Self {
handle: std::ptr::null_mut(),
data: std::ptr::null_mut(),
capacity: 0,
}
}
}
/// Sealed segments with their committed lengths, plus the segment currently
/// exposed to the caller.
pub(crate) struct FillingState {
sealed: Vec<(Vec<MaybeUninit<u8>>, usize)>,
current: Vec<MaybeUninit<u8>>,
}
/// Allocate a segment without initializing it, like a pooled array: contents
/// are unspecified until the caller writes them.
fn uninit_segment(capacity: usize) -> Vec<MaybeUninit<u8>> {
let mut segment = Vec::with_capacity(capacity);
// SAFETY: `MaybeUninit<u8>` requires no initialization.
unsafe { segment.set_len(capacity) };
segment
}
/// Freeze the committed prefix of a segment into `Bytes`.
///
/// The caller wrote the first `committed` bytes through the segment's raw
/// pointer, so that prefix is initialized. The tail past it is freed without
/// ever being read.
fn freeze_committed(segment: Vec<MaybeUninit<u8>>, committed: usize) -> bytes::Bytes {
debug_assert!(committed <= segment.len());
let mut segment = std::mem::ManuallyDrop::new(segment);
let (ptr, capacity) = (segment.as_mut_ptr(), segment.capacity());
// SAFETY: same allocation and layout (`MaybeUninit<u8>` mirrors `u8`),
// length capped to the initialized prefix, capacity preserved for the
// eventual free.
let vec = unsafe { Vec::from_raw_parts(ptr.cast::<u8>(), committed, capacity) };
bytes::Bytes::from(vec)
}
/// Lifecycle of an allocated write buffer.
///
/// Consumption keeps a guard clone of the assembled buffer until
/// `write_buffer_free`, so a stale caller-side view can never dangle.
pub(crate) enum WriteBufferSlot {
Filling(FillingState),
Consumed { _guard: opendal::Buffer },
}
/// Seal the current segment and expose a fresh one of `min_capacity` bytes.
fn grow(
slot: &mut WriteBufferSlot,
committed_in_current: usize,
min_capacity: usize,
) -> Result<(*mut u8, usize), OpenDALError> {
if min_capacity == 0 {
return Err(config_invalid_error(
"write buffer segment capacity must be greater than 0",
));
}
match slot {
WriteBufferSlot::Filling(state) => {
if committed_in_current > state.current.len() {
return Err(config_invalid_error(
"committed length exceeds the current segment capacity",
));
}
let mut next = uninit_segment(min_capacity);
let data = next.as_mut_ptr().cast::<u8>();
let sealed = std::mem::replace(&mut state.current, next);
state.sealed.push((sealed, committed_in_current));
Ok((data, min_capacity))
}
WriteBufferSlot::Consumed { .. } => Err(config_invalid_error(
"write buffer contents were already consumed",
)),
}
}
/// Assemble the committed bytes of every segment into one buffer.
///
/// On success the slot moves to `Consumed`; errors leave it untouched, so
/// the caller keeps full ownership.
pub(crate) fn take_payload(
slot: &mut WriteBufferSlot,
committed_in_current: usize,
) -> Result<opendal::Buffer, OpenDALError> {
match slot {
WriteBufferSlot::Filling(state) => {
if committed_in_current > state.current.len() {
return Err(config_invalid_error(
"committed length exceeds the current segment capacity",
));
}
let mut parts: Vec<bytes::Bytes> = Vec::with_capacity(state.sealed.len() + 1);
for (segment, committed) in state.sealed.drain(..) {
if committed == 0 {
continue;
}
parts.push(freeze_committed(segment, committed));
}
if committed_in_current > 0 {
let current = std::mem::take(&mut state.current);
parts.push(freeze_committed(current, committed_in_current));
}
let buffer = if parts.is_empty() {
opendal::Buffer::new()
} else {
opendal::Buffer::from(parts)
};
*slot = WriteBufferSlot::Consumed {
_guard: buffer.clone(),
};
Ok(buffer)
}
WriteBufferSlot::Consumed { .. } => Err(config_invalid_error(
"write buffer contents were already consumed",
)),
}
}
/// Allocate a buffer exposing `capacity` writable bytes. The memory is not
/// initialized: contents are unspecified until the caller writes them.
///
/// The returned handle must eventually be released with `write_buffer_free`.
#[unsafe(no_mangle)]
pub extern "C" fn write_buffer_create(capacity: usize) -> OpendalWriteBufferResult {
if capacity == 0 {
return OpendalWriteBufferResult::from_error(config_invalid_error(
"write buffer capacity must be greater than 0",
));
}
let mut current = uninit_segment(capacity);
let data = current.as_mut_ptr().cast::<u8>();
let slot = WriteBufferSlot::Filling(FillingState {
sealed: Vec::new(),
current,
});
let handle = Box::into_raw(Box::new(slot)) as *mut c_void;
OpendalWriteBufferResult::ok(OpendalWriteBuffer {
handle,
data,
capacity,
})
}
/// Seal the current segment at `committed_in_current` bytes and expose a new
/// segment of at least `min_capacity` bytes. Earlier segments keep their
/// addresses.
/// # Safety
///
/// - `handle` must come from `write_buffer_create` and not have been freed.
/// - Calls for the same handle must not run concurrently.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn write_buffer_add_segment(
handle: *mut c_void,
committed_in_current: usize,
min_capacity: usize,
) -> OpendalWriteBufferResult {
if handle.is_null() {
return OpendalWriteBufferResult::from_error(config_invalid_error(
"write buffer handle is null",
));
}
let slot = unsafe { &mut *(handle as *mut WriteBufferSlot) };
match grow(slot, committed_in_current, min_capacity) {
Ok((data, capacity)) => OpendalWriteBufferResult::ok(OpendalWriteBuffer {
handle,
data,
capacity,
}),
Err(error) => OpendalWriteBufferResult::from_error(error),
}
}
/// Release an allocated write buffer handle. After the contents are consumed this only
/// drops the guard clone; the backend keeps the payload alive for as long as it needs.
/// # Safety
///
/// - `handle` must be null or come from `write_buffer_create`.
/// - Must be called at most once for the same handle.
/// - Segment pointers must not be used after this call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn write_buffer_free(handle: *mut c_void) {
if handle.is_null() {
return;
}
drop(unsafe { Box::from_raw(handle as *mut WriteBufferSlot) });
}
#[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_at(buffer: &opendal::Buffer, src_offset: usize, dest: &mut [u8]) -> usize {
let owned = OpendalReadBuffer::from_buffer(buffer.clone());
let written =
unsafe { read_buffer_copy_to(owned.handle, src_offset, dest.as_mut_ptr(), dest.len()) };
unsafe { read_buffer_free(owned.handle) };
written
}
fn copy(buffer: &opendal::Buffer, dest: &mut [u8]) -> usize {
copy_at(buffer, 0, dest)
}
#[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 resumes_from_an_offset_inside_a_chunk() {
// Streaming reads drain one chunk across several calls, so the copy
// must be able to start mid-chunk and keep walking the later parts.
let buffer = split_buffer();
let mut dest = vec![0u8; 4];
assert_eq!(copy_at(&buffer, 4, &mut dest), 4);
assert_eq!(&dest, b"efgh");
}
#[test]
fn writes_nothing_at_or_past_the_end() {
let buffer = split_buffer();
let mut dest = vec![0u8; 4];
assert_eq!(copy_at(&buffer, buffer.len(), &mut dest), 0);
assert_eq!(copy_at(&buffer, buffer.len() + 1, &mut dest), 0);
assert_eq!(&dest, &[0u8; 4]);
}
#[test]
fn chunks_reports_every_part_and_respects_capacity() {
let owned = OpendalReadBuffer::from_buffer(split_buffer());
// Sizing call: no output array, just the count.
let count = unsafe { read_buffer_chunks(owned.handle, std::ptr::null_mut(), 0) };
assert_eq!(count, 3);
let mut chunks = vec![
OpendalChunk {
data: std::ptr::null(),
len: 0,
},
OpendalChunk {
data: std::ptr::null(),
len: 0,
},
OpendalChunk {
data: std::ptr::null(),
len: 0,
},
];
let filled = unsafe { read_buffer_chunks(owned.handle, chunks.as_mut_ptr(), chunks.len()) };
assert_eq!(filled, 3);
let collected: Vec<u8> = chunks
.iter()
.flat_map(|chunk| unsafe { std::slice::from_raw_parts(chunk.data, chunk.len) })
.copied()
.collect();
assert_eq!(&collected, b"abcdefgh");
unsafe { read_buffer_free(owned.handle) };
}
#[test]
fn empty_payload_owns_nothing() {
let owned = OpendalReadBuffer::from_buffer(opendal::Buffer::new());
assert!(owned.handle.is_null());
assert_eq!(owned.len, 0);
unsafe { read_buffer_free(owned.handle) };
}
fn as_uninit(bytes: Vec<u8>) -> Vec<MaybeUninit<u8>> {
bytes.into_iter().map(MaybeUninit::new).collect()
}
fn filling(segments: Vec<(Vec<u8>, usize)>, current: Vec<u8>) -> WriteBufferSlot {
WriteBufferSlot::Filling(FillingState {
sealed: segments
.into_iter()
.map(|(segment, committed)| (as_uninit(segment), committed))
.collect(),
current: as_uninit(current),
})
}
#[test]
fn take_assembles_committed_bytes_across_segments_in_order() {
// Sealed segments carry their own committed lengths; the tail commit
// arrives as an argument. Uncommitted capacity must never leak.
let mut slot = filling(
vec![(b"abcX".to_vec(), 3), (b"deXX".to_vec(), 2)],
b"fghXX".to_vec(),
);
let payload = take_payload(&mut slot, 3).unwrap();
assert_eq!(payload.to_vec(), b"abcdefgh");
match &slot {
WriteBufferSlot::Consumed { _guard } => assert_eq!(_guard.len(), 8),
WriteBufferSlot::Filling(_) => panic!("slot must move to Consumed"),
}
}
#[test]
fn take_skips_segments_with_nothing_committed() {
let mut slot = filling(vec![(b"XXXX".to_vec(), 0)], b"ab".to_vec());
let payload = take_payload(&mut slot, 2).unwrap();
assert_eq!(payload.to_vec(), b"ab");
}
#[test]
fn take_rejects_a_second_take() {
let mut slot = filling(Vec::new(), b"abc".to_vec());
take_payload(&mut slot, 3).unwrap();
assert!(take_payload(&mut slot, 0).is_err());
}
#[test]
fn take_rejects_commits_beyond_capacity_and_keeps_the_slot_usable() {
let mut slot = filling(Vec::new(), b"abc".to_vec());
assert!(take_payload(&mut slot, 4).is_err());
// The failed call must not consume the buffer.
let payload = take_payload(&mut slot, 2).unwrap();
assert_eq!(payload.to_vec(), b"ab");
}
#[test]
fn grow_seals_the_current_segment_and_keeps_earlier_pointers_stable() {
let mut slot = filling(Vec::new(), b"abc".to_vec());
let first_ptr = match &slot {
WriteBufferSlot::Filling(state) => state.current.as_ptr(),
WriteBufferSlot::Consumed { .. } => unreachable!(),
};
let (data, capacity) = grow(&mut slot, 2, 8).unwrap();
assert_eq!(capacity, 8);
assert!(!data.is_null());
match &slot {
WriteBufferSlot::Filling(state) => {
assert_eq!(state.sealed.len(), 1);
assert_eq!(state.sealed[0].1, 2);
// The sealed segment must not have moved when it changed hands.
assert_eq!(state.sealed[0].0.as_ptr(), first_ptr);
}
WriteBufferSlot::Consumed { .. } => panic!("slot must stay Filling"),
}
}
#[test]
fn create_hands_out_writable_memory_and_free_releases_it() {
let result = write_buffer_create(8);
assert_eq!(result.error.has_error, 0);
let buffer = result.buffer;
assert_eq!(buffer.capacity, 8);
unsafe {
*buffer.data = 7;
assert_eq!(*buffer.data, 7);
write_buffer_free(buffer.handle);
}
}
#[test]
fn create_rejects_zero_capacity() {
let mut result = write_buffer_create(0);
assert_eq!(result.error.has_error, 1);
assert!(result.buffer.handle.is_null());
crate::result::opendal_error_release(std::mem::replace(
&mut result.error,
OpenDALError::ok(),
));
}
}