| // Copyright 2026 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #![no_std] |
| |
| use core::sync::atomic::{AtomicU64, Ordering}; |
| use core::{cmp, ptr, slice}; |
| pub use kalloc::NoOpAllocator; |
| use kalloc::{Allocator, Box, DefaultAllocator}; |
| use zx_status::Status; |
| |
| /// A simple convenience type used to hold the read and write pointers as separate values. |
| #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| struct RingPointers { |
| read: u32, |
| write: u32, |
| } |
| |
| impl RingPointers { |
| /// Constructs a new `RingPointers` with the given read and write offsets. |
| const fn new(read: u32, write: u32) -> Self { |
| Self { read, write } |
| } |
| |
| /// Splits combined 64-bit pointers into individual read and write pointers. |
| const fn from_combined(combined: u64) -> Self { |
| Self::new((combined >> 32) as u32, combined as u32) |
| } |
| |
| /// Combines read and write pointers into a single 64-bit value. |
| const fn as_combined(&self) -> u64 { |
| ((self.read as u64) << 32) | (self.write as u64) |
| } |
| |
| /// Returns the amount of data available to read in the buffer. |
| const fn available_data(&self) -> u32 { |
| self.write.wrapping_sub(self.read) |
| } |
| } |
| |
| /// A wrapper around a slot of memory in the ring buffer. |
| /// |
| /// A Reservation has a predetermined size that is determined by the size passed into `reserve`. Any |
| /// attempt to write more than this amount of data into the slot is a programming error and will |
| /// cause an assertion failure. This class provides a formal way for writers to serialize data in |
| /// place in the ring buffer, thus eliminating the need for a temporary serialization buffer. |
| #[derive(Debug)] |
| pub struct Reservation<'a> { |
| combined_pointers: &'a AtomicU64, |
| storage_len: u32, |
| initial_ring_pointers: RingPointers, |
| region1: &'a mut [u8], |
| region2: &'a mut [u8], |
| write_offset: u32, |
| committed: bool, |
| } |
| |
| impl<'a> Drop for Reservation<'a> { |
| fn drop(&mut self) { |
| debug_assert!(self.committed, "Reservation dropped without being committed"); |
| } |
| } |
| |
| impl<'a> Reservation<'a> { |
| /// Writes the given data into this reservation. |
| pub fn write(&mut self, data: &[u8]) -> Result<(), Status> { |
| if self.committed { |
| return Err(Status::BAD_STATE); |
| } |
| |
| let mut bytes_to_copy = data.len(); |
| let mut region1_copy_amount = 0; |
| let mut write_offset = self.write_offset as usize; |
| |
| let region1_len = self.region1.len(); |
| if write_offset < region1_len { |
| let space_left_in_region1 = region1_len - write_offset; |
| region1_copy_amount = cmp::min(bytes_to_copy, space_left_in_region1); |
| |
| self.region1[write_offset..write_offset + region1_copy_amount] |
| .copy_from_slice(&data[..region1_copy_amount]); |
| |
| write_offset += region1_copy_amount; |
| bytes_to_copy -= region1_copy_amount; |
| } |
| |
| if bytes_to_copy > 0 { |
| if write_offset < region1_len { |
| return Err(Status::BAD_STATE); |
| } |
| let region2_len = self.region2.len(); |
| let region2_offset = write_offset - region1_len; |
| if region2_len < region2_offset { |
| return Err(Status::BAD_STATE); |
| } |
| if region2_len - region2_offset < bytes_to_copy { |
| return Err(Status::BUFFER_TOO_SMALL); |
| } |
| |
| self.region2[region2_offset..region2_offset + bytes_to_copy] |
| .copy_from_slice(&data[region1_copy_amount..region1_copy_amount + bytes_to_copy]); |
| |
| write_offset += bytes_to_copy; |
| } |
| |
| self.write_offset = write_offset as u32; |
| Ok(()) |
| } |
| |
| /// Advances the write pointer of the associated spsc buffer. |
| /// |
| /// This makes the written data visible to the reader, and thus can only be called once all |
| /// writes have been completed and the reservation is fully written. |
| pub fn commit(mut self) -> Result<(), Status> { |
| if self.committed { |
| return Err(Status::BAD_STATE); |
| } |
| self.committed = true; |
| |
| let total_len = |
| self.region1.len().checked_add(self.region2.len()).ok_or(Status::BAD_STATE)? as u32; |
| if self.write_offset != total_len { |
| return Err(Status::BAD_STATE); |
| } |
| |
| advance_write_pointer( |
| self.combined_pointers, |
| self.storage_len, |
| self.initial_ring_pointers, |
| total_len, |
| ) |
| } |
| } |
| |
| /// A transactional, single-producer, single-consumer ring buffer. |
| /// |
| /// The caller is responsible for ensuring that there is only one reader and one writer; no internal |
| /// synchronization is provided to enforce this constraint. |
| /// |
| /// Backing storage is allocated dynamically during the `init` method. The requested size must be a |
| /// power of two for correct functionality. |
| // TODO(https://fxbug.dev/517301686): Use bindgen or another systematic way to avoid duplicating |
| // this structure and causing drift. |
| #[repr(C, align(8))] |
| pub struct Buffer<A: Allocator + Default = DefaultAllocator> { |
| // The read and write pointers are stored as the upper and lower halves, respectively, of a |
| // single 64-bit atomic. |
| combined_pointers: AtomicU64, |
| // The types used for `storage` and `size` must match those of ktl::span. |
| storage: *mut u8, |
| // The size of the backing storage in bytes. This is enforced to be at most |
| // MAX_STORAGE_SIZE (2 GiB), meaning this value will never exceed u32::MAX. |
| size: usize, |
| _phantom: core::marker::PhantomData<A>, |
| } |
| |
| impl<A: Allocator + Default> Drop for Buffer<A> { |
| fn drop(&mut self) { |
| if !self.storage.is_null() { |
| let slice_ptr = ptr::slice_from_raw_parts_mut(self.storage, self.size); |
| unsafe { |
| let _ = Box::from_raw_in(slice_ptr, A::default()); |
| } |
| } |
| } |
| } |
| |
| impl<A: Allocator + Default> Buffer<A> { |
| /// Maximum size of backing storage buffer (2 GiB). |
| const MAX_STORAGE_SIZE: u32 = 1 << 31; |
| |
| /// Constructs a new `Buffer` with a dynamically allocated backing storage of the given size, |
| /// using the given allocator. |
| pub fn try_new_in(size: u32, allocator: A) -> Result<Self, Status> { |
| if size > Self::MAX_STORAGE_SIZE { |
| return Err(Status::INVALID_ARGS); |
| } |
| if !size.is_power_of_two() { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| let storage_box = Box::<[u8], A>::try_new_zeroed_slice_in(size as usize, allocator) |
| .map_err(|_| Status::NO_MEMORY)?; |
| let (storage_ptr, _) = Box::into_raw_with_allocator(storage_box); |
| |
| Ok(Self { |
| combined_pointers: AtomicU64::new(0), |
| storage: storage_ptr as *mut u8, |
| size: size as usize, |
| _phantom: core::marker::PhantomData, |
| }) |
| } |
| |
| /// Returns the size of the backing storage. |
| pub fn size(&self) -> u32 { |
| self.size as u32 |
| } |
| |
| /// Reserves a block of the given size in the buffer. |
| /// |
| /// Any data written into this block will not be visible to readers until `commit` is called on |
| /// the returned `Reservation`. |
| pub fn reserve(&mut self, size: u32) -> Result<Reservation<'_>, Status> { |
| if size == 0 || size > Self::MAX_STORAGE_SIZE { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| let storage_len = self.size as u32; |
| if size > storage_len { |
| return Err(Status::NO_SPACE); |
| } |
| |
| let initial_state = self.load_pointers(); |
| let available_space = self.available_space(initial_state); |
| if available_space < size { |
| return Err(Status::NO_SPACE); |
| } |
| |
| let write_offset = self.pointer_to_offset(initial_state.write); |
| let ring_break_distance = storage_len - write_offset; |
| let bytes_before_break = cmp::min(size, ring_break_distance); |
| |
| // SAFETY: The creator of Buffer must ensure that `storage` points to a valid memory region |
| // of `size` bytes. |
| let storage_slice = unsafe { slice::from_raw_parts_mut(self.storage, self.size) }; |
| let (left, right) = storage_slice.split_at_mut(write_offset as usize); |
| let region1 = &mut right[..bytes_before_break as usize]; |
| |
| let region2 = if bytes_before_break < size { |
| let region2_len = size - bytes_before_break; |
| &mut left[..region2_len as usize] |
| } else { |
| &mut [] |
| }; |
| |
| Ok(Reservation { |
| combined_pointers: &self.combined_pointers, |
| storage_len, |
| initial_ring_pointers: initial_state, |
| region1, |
| region2, |
| write_offset: 0, |
| committed: false, |
| }) |
| } |
| |
| /// Copies `len` bytes out of the buffer using the provided `copy_fn`. |
| /// |
| /// The copy function has the signature `copy_fn(offset: u32, src: &[u8]) -> Result<(), Status>` |
| /// and may be invoked multiple times. |
| /// |
| /// Returns the number of bytes read on success. If `copy_fn` returns an error, that error is |
| /// propagated. |
| /// |
| /// Importantly, even if an error is returned, `copy_fn` might have already processed a partial |
| /// amount of data (between 0 and `len` bytes). However, these partially processed bytes are |
| /// considered *not read*. Consequently, the internal read pointer of the ring buffer will *not* |
| /// be advanced for these unread bytes, meaning that these same bytes will remain available for |
| /// reading in subsequent calls to `read`. |
| pub fn read<F>(&self, mut copy_fn: F, len: u32) -> Result<u32, Status> |
| where |
| F: FnMut(u32, &[u8]) -> Result<(), Status>, |
| { |
| let initial_state = self.load_pointers(); |
| let available_data = initial_state.available_data(); |
| if available_data == 0 { |
| return Ok(0); |
| } |
| |
| let amount_to_copy = cmp::min(available_data, len) as usize; |
| let read_offset = self.pointer_to_offset(initial_state.read) as usize; |
| let ring_break_distance = self.size - read_offset; |
| let bytes_before_break = cmp::min(amount_to_copy, ring_break_distance); |
| |
| // SAFETY: The creator of Buffer must ensure that `storage` points to a valid memory region |
| // of `size` bytes. |
| let storage_slice = unsafe { slice::from_raw_parts(self.storage, self.size) }; |
| let slice1 = &storage_slice[read_offset..read_offset + bytes_before_break]; |
| copy_fn(0, slice1)?; |
| |
| if bytes_before_break < amount_to_copy { |
| let bytes_after_break = amount_to_copy - bytes_before_break; |
| let slice2 = &storage_slice[..bytes_after_break]; |
| copy_fn(bytes_before_break as u32, slice2)?; |
| } |
| |
| self.advance_read_pointer(initial_state, amount_to_copy as u32)?; |
| Ok(amount_to_copy as u32) |
| } |
| |
| /// Empties the contents of the buffer. |
| /// |
| /// This is logically a read operation, so a `read` and `drain` cannot be called concurrently. |
| /// Additionally, the behavior of this method is non-deterministic if a write is in-progress. |
| pub fn drain(&self) -> Result<(), Status> { |
| let initial_state = self.load_pointers(); |
| let available_data = initial_state.available_data(); |
| if available_data == 0 { |
| return Ok(()); |
| } |
| self.advance_read_pointer(initial_state, available_data) |
| } |
| |
| // Helper function that converts read and write pointers into ring buffer offsets. |
| // |
| // This is logically performing pointer % storage.len(), but because storage.len() is guaranteed |
| // to be a power of two it is equivalent to this logical AND. |
| fn pointer_to_offset(&self, pointer: u32) -> u32 { |
| let storage_len = self.size as u32; |
| pointer & (storage_len - 1) |
| } |
| |
| /// Returns the remaining available space in the buffer. |
| fn available_space(&self, pointers: RingPointers) -> u32 { |
| let storage_len = self.size as u32; |
| storage_len.wrapping_sub(pointers.available_data()) |
| } |
| |
| /// Loads the current values of the read and write pointers. |
| fn load_pointers(&self) -> RingPointers { |
| let combined = self.combined_pointers.load(Ordering::Acquire); |
| RingPointers::from_combined(combined) |
| } |
| |
| // Adds the given delta to the read half of the combined_pointers. |
| // |
| // Because we store the pointers in a single combined atomic variable, we must update the entire |
| // combined pointer. We perform this update using a compare and exchange to ensure that |
| // concurrent operations to the write half of the combined pointers are preserved. We also check |
| // to ensure reads do not encounter concurrent reads. |
| // |
| // This is a store-release operation that synchronizes with the load-acquire in load_pointers. |
| // By using release semantics, we ensure that if the updated value is seen in load_pointers, all |
| // memory operations that occurred prior to this update are observable. |
| fn advance_read_pointer(&self, initial: RingPointers, delta: u32) -> Result<(), Status> { |
| if delta > initial.available_data() { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| let target_read = initial.read.wrapping_add(delta); |
| let mut starting_pointers = initial.as_combined(); |
| let mut target_pointers = RingPointers::new(target_read, initial.write).as_combined(); |
| |
| loop { |
| match self.combined_pointers.compare_exchange_weak( |
| starting_pointers, |
| target_pointers, |
| Ordering::Release, |
| Ordering::Relaxed, |
| ) { |
| Ok(_) => break, |
| Err(observed_combined) => { |
| starting_pointers = observed_combined; |
| let observed = RingPointers::from_combined(observed_combined); |
| debug_assert_eq!( |
| observed.read, initial.read, |
| "potential concurrent read detected; expected read pointer {}, got {}", |
| initial.read, observed.read |
| ); |
| target_pointers = RingPointers::new(target_read, observed.write).as_combined(); |
| } |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| // Adds the given delta to the write half of the combined_pointers. |
| // |
| // Because we store the pointers in a single combined atomic variable, we must update the entire |
| // combined pointer. We perform this update using a compare and exchange to ensure that concurrent |
| // operations to the read half of the combined pointers are preserved. We also check to ensure |
| // writes do not encounter concurrent writes. |
| // |
| // This is a store-release operation that synchronizes with the load-acquire in load_pointers. By |
| // using release semantics, we ensure that if the updated value is seen in load_pointers, all memory |
| // operations that occurred prior to this update are observable. |
| fn advance_write_pointer( |
| combined_pointers: &AtomicU64, |
| storage_len: u32, |
| initial: RingPointers, |
| delta: u32, |
| ) -> Result<(), Status> { |
| let available_data = initial.available_data(); |
| if delta > storage_len.checked_sub(available_data).ok_or(Status::INVALID_ARGS)? { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| let target_write = initial.write.wrapping_add(delta); |
| let mut starting_pointers = initial.as_combined(); |
| let mut target_pointers = RingPointers::new(initial.read, target_write).as_combined(); |
| |
| loop { |
| match combined_pointers.compare_exchange_weak( |
| starting_pointers, |
| target_pointers, |
| Ordering::Release, |
| Ordering::Relaxed, |
| ) { |
| Ok(_) => break, |
| Err(observed_combined) => { |
| starting_pointers = observed_combined; |
| let observed = RingPointers::from_combined(observed_combined); |
| debug_assert_eq!( |
| observed.write, initial.write, |
| "potential concurrent write detected; expected write pointer {}, got {}", |
| initial.write, observed.write |
| ); |
| target_pointers = RingPointers::new(observed.read, target_write).as_combined(); |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| impl Buffer<DefaultAllocator> { |
| /// Constructs a new `Buffer` with a dynamically allocated backing storage of the given size, |
| /// using the default allocator. |
| pub fn try_new(size: u32) -> Result<Self, Status> { |
| Self::try_new_in(size, DefaultAllocator) |
| } |
| } |
| |
| impl Buffer<NoOpAllocator> { |
| /// Constructs a `Buffer` from raw pointers using a no-op allocator. |
| /// |
| /// The returned buffer does not own the memory and will not deallocate it when dropped. |
| /// |
| /// # Safety |
| /// |
| /// - `storage` must point to a valid, initialized slice of bytes whose length is a power of two |
| /// and does not exceed `MAX_STORAGE_SIZE`. |
| pub unsafe fn from_raw_parts(storage: *mut u8, size: usize) -> Self { |
| Self { |
| combined_pointers: AtomicU64::new(0), |
| storage, |
| size, |
| _phantom: core::marker::PhantomData, |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_ring_pointers_from_combined() { |
| struct TestCase { |
| combined_pointers: u64, |
| expected: RingPointers, |
| } |
| let test_cases = [ |
| TestCase { |
| combined_pointers: 0x01320087_01005382, |
| expected: RingPointers::new(0x1320087, 0x1005382), |
| }, |
| TestCase { combined_pointers: 0, expected: RingPointers::new(0, 0) }, |
| TestCase { |
| combined_pointers: 0xFFFFFFFF_00004587, |
| expected: RingPointers::new(0xFFFFFFFF, 0x4587), |
| }, |
| TestCase { |
| combined_pointers: 0x00004587_FFFFFFFF, |
| expected: RingPointers::new(0x4587, 0xFFFFFFFF), |
| }, |
| ]; |
| |
| for tc in &test_cases { |
| let actual = RingPointers::from_combined(tc.combined_pointers); |
| assert_eq!(tc.expected, actual); |
| } |
| } |
| |
| #[test] |
| fn test_ring_pointers_as_combined() { |
| struct TestCase { |
| pointers: RingPointers, |
| combined: u64, |
| } |
| let test_cases = [ |
| TestCase { |
| pointers: RingPointers::new(0x1320087, 0x1005382), |
| combined: 0x01320087_01005382, |
| }, |
| TestCase { pointers: RingPointers::new(0, 0), combined: 0 }, |
| TestCase { |
| pointers: RingPointers::new(0xFFFFFFFF, 0x123), |
| combined: 0xFFFFFFFF_00000123, |
| }, |
| TestCase { |
| pointers: RingPointers::new(0x123, 0xFFFFFFFF), |
| combined: 0x00000123_FFFFFFFF, |
| }, |
| ]; |
| |
| for tc in &test_cases { |
| let actual = tc.pointers.as_combined(); |
| assert_eq!(tc.combined, actual); |
| } |
| } |
| |
| #[test] |
| fn test_available_space() { |
| struct TestCase { |
| pointers: RingPointers, |
| buffer_size: u32, |
| expected: u32, |
| } |
| let test_cases = [ |
| TestCase { pointers: RingPointers::new(0, 0), buffer_size: 16, expected: 16 }, |
| TestCase { pointers: RingPointers::new(3, 3), buffer_size: 16, expected: 16 }, |
| TestCase { pointers: RingPointers::new(3, 7), buffer_size: 16, expected: 12 }, |
| TestCase { pointers: RingPointers::new(0xFFFFFFFC, 3), buffer_size: 16, expected: 9 }, |
| TestCase { pointers: RingPointers::new(0, 16), buffer_size: 16, expected: 0 }, |
| ]; |
| |
| for tc in &test_cases { |
| let buffer = Buffer::try_new(tc.buffer_size).unwrap(); |
| let actual = buffer.available_space(tc.pointers); |
| assert_eq!(tc.expected, actual); |
| } |
| } |
| |
| #[test] |
| fn test_ring_pointers_available_data() { |
| struct TestCase { |
| pointers: RingPointers, |
| expected: u32, |
| } |
| let test_cases = [ |
| TestCase { pointers: RingPointers::new(0, 0), expected: 0 }, |
| TestCase { pointers: RingPointers::new(3, 3), expected: 0 }, |
| TestCase { pointers: RingPointers::new(3, 7), expected: 4 }, |
| TestCase { pointers: RingPointers::new(0, 16), expected: 16 }, |
| TestCase { pointers: RingPointers::new(0xFFFFFFFC, 3), expected: 7 }, |
| ]; |
| |
| for tc in &test_cases { |
| let actual = tc.pointers.available_data(); |
| assert_eq!(tc.expected, actual); |
| } |
| } |
| |
| #[test] |
| fn test_advance_read_pointer() { |
| struct TestCase { |
| initial_pointers: RingPointers, |
| buffer_size: u32, |
| delta: u32, |
| expected: u64, |
| } |
| let test_cases = [ |
| TestCase { |
| initial_pointers: RingPointers::new(1, 6), |
| buffer_size: 16, |
| delta: 4, |
| expected: 0x5_00000006, |
| }, |
| TestCase { |
| initial_pointers: RingPointers::new(0, 16), |
| buffer_size: 16, |
| delta: 16, |
| expected: 0x10_00000010, |
| }, |
| TestCase { |
| initial_pointers: RingPointers::new(0xFFFFFFFC, 12), |
| buffer_size: 16, |
| delta: 5, |
| expected: 0x1_0000000C, |
| }, |
| ]; |
| |
| for tc in &test_cases { |
| let buffer = Buffer::try_new(tc.buffer_size).unwrap(); |
| |
| let initial = tc.initial_pointers.as_combined(); |
| buffer.combined_pointers.store(initial, Ordering::Release); |
| |
| buffer.advance_read_pointer(tc.initial_pointers, tc.delta).unwrap(); |
| let actual = buffer.combined_pointers.load(Ordering::Acquire); |
| assert_eq!(tc.expected, actual); |
| } |
| } |
| |
| #[test] |
| fn test_advance_write_pointer() { |
| struct TestCase { |
| initial_pointers: RingPointers, |
| buffer_size: u32, |
| delta: u32, |
| expected: u64, |
| } |
| let test_cases = [ |
| TestCase { |
| initial_pointers: RingPointers::new(7, 9), |
| buffer_size: 16, |
| delta: 4, |
| expected: 0x7_0000000D, |
| }, |
| TestCase { |
| initial_pointers: RingPointers::new(0, 0), |
| buffer_size: 16, |
| delta: 16, |
| expected: 0x10, |
| }, |
| TestCase { |
| initial_pointers: RingPointers::new(0xFFFFFFF1, 0xFFFFFFFC), |
| buffer_size: 16, |
| delta: 5, |
| expected: 0xFFFFFFF100000001, |
| }, |
| ]; |
| |
| for tc in &test_cases { |
| let buffer = Buffer::try_new(tc.buffer_size).unwrap(); |
| |
| let initial = tc.initial_pointers.as_combined(); |
| buffer.combined_pointers.store(initial, Ordering::Release); |
| |
| advance_write_pointer( |
| &buffer.combined_pointers, |
| buffer.size as u32, |
| tc.initial_pointers, |
| tc.delta, |
| ) |
| .unwrap(); |
| let actual = buffer.combined_pointers.load(Ordering::Acquire); |
| assert_eq!(tc.expected, actual); |
| } |
| } |
| |
| #[test] |
| fn test_try_new() { |
| // Happy case |
| { |
| assert!(Buffer::try_new(256).is_ok()); |
| } |
| |
| // Calling try_new with too big of a size should fail |
| { |
| assert_eq!(Buffer::try_new(u32::MAX).err().unwrap(), Status::INVALID_ARGS); |
| } |
| |
| // Calling try_new with a size that is not a power of two should fail |
| { |
| assert_eq!(Buffer::try_new(100).err().unwrap(), Status::INVALID_ARGS); |
| } |
| |
| // try_new should propagate allocation failures |
| { |
| assert_eq!(Buffer::try_new_in(256, NoOpAllocator).err().unwrap(), Status::NO_MEMORY); |
| } |
| } |
| |
| #[test] |
| fn test_read_write_single_threaded() { |
| const STORAGE_SIZE: usize = 256; |
| let mut src = [0u8; STORAGE_SIZE]; |
| for i in 0..STORAGE_SIZE { |
| src[i] = (i * 17 + 5) as u8; |
| } |
| |
| struct TestCase { |
| write_size: u32, |
| read_size: u32, |
| expected_read_size: u32, |
| expected_reserve_status: Result<(), Status>, |
| expected_read_status: Result<(), Status>, |
| initial_pointers: RingPointers, |
| use_copy_out_err_fn: bool, |
| } |
| |
| let test_cases = [ |
| TestCase { |
| write_size: (STORAGE_SIZE / 2) as u32, |
| read_size: (STORAGE_SIZE / 2) as u32, |
| expected_read_size: (STORAGE_SIZE / 2) as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0, 0), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: (STORAGE_SIZE / 2) as u32, |
| read_size: (STORAGE_SIZE / 4) as u32, |
| expected_read_size: (STORAGE_SIZE / 4) as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0, 0), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: STORAGE_SIZE as u32, |
| read_size: STORAGE_SIZE as u32, |
| expected_read_size: STORAGE_SIZE as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0, 0), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: (STORAGE_SIZE / 4) as u32, |
| read_size: (STORAGE_SIZE / 2) as u32, |
| expected_read_size: (STORAGE_SIZE / 4) as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0, 0), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: STORAGE_SIZE as u32, |
| read_size: STORAGE_SIZE as u32, |
| expected_read_size: STORAGE_SIZE as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new( |
| (STORAGE_SIZE / 2) as u32, |
| (STORAGE_SIZE / 2) as u32, |
| ), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: STORAGE_SIZE as u32, |
| read_size: STORAGE_SIZE as u32, |
| expected_read_size: STORAGE_SIZE as u32, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0xFFFFFFFA, 0xFFFFFFFA), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: 64, |
| read_size: 0, |
| expected_read_size: 0, |
| expected_reserve_status: Err(Status::NO_SPACE), |
| expected_read_status: Ok(()), |
| initial_pointers: RingPointers::new(0, (STORAGE_SIZE - 48) as u32), |
| use_copy_out_err_fn: false, |
| }, |
| TestCase { |
| write_size: STORAGE_SIZE as u32, |
| read_size: (STORAGE_SIZE / 2) as u32, |
| expected_read_size: 0, |
| expected_reserve_status: Ok(()), |
| expected_read_status: Err(Status::BAD_STATE), |
| initial_pointers: RingPointers::new(0, 0), |
| use_copy_out_err_fn: true, |
| }, |
| ]; |
| |
| for tc in &test_cases { |
| let mut dst = [0u8; STORAGE_SIZE]; |
| |
| let mut spsc = Buffer::try_new(STORAGE_SIZE as u32).unwrap(); |
| |
| let starting_pointers = tc.initial_pointers.as_combined(); |
| spsc.combined_pointers.store(starting_pointers, Ordering::Release); |
| |
| let reservation = spsc.reserve(tc.write_size); |
| if let Err(e) = &tc.expected_reserve_status { |
| match reservation { |
| Err(actual_err) => assert_eq!(actual_err, *e), |
| Ok(_) => panic!("expected reserve to fail with {:?}, but it succeeded", e), |
| } |
| continue; |
| } |
| let mut reservation = reservation.unwrap(); |
| |
| reservation.write(&src[..tc.write_size as usize]).unwrap(); |
| reservation.commit().unwrap(); |
| |
| let copy_out_fn = |offset: u32, src_slice: &[u8]| -> Result<(), Status> { |
| let offset = offset as usize; |
| assert!(offset + src_slice.len() <= dst.len()); |
| dst[offset..offset + src_slice.len()].copy_from_slice(src_slice); |
| Ok(()) |
| }; |
| |
| let copy_out_err_fn = |
| |_offset: u32, _src_slice: &[u8]| -> Result<(), Status> { Err(Status::BAD_STATE) }; |
| |
| let read_result = if tc.use_copy_out_err_fn { |
| spsc.read(copy_out_err_fn, tc.read_size) |
| } else { |
| spsc.read(copy_out_fn, tc.read_size) |
| }; |
| |
| if let Err(e) = &tc.expected_read_status { |
| assert_eq!(read_result.unwrap_err(), *e); |
| assert_eq!(spsc.load_pointers().available_data(), tc.write_size); |
| continue; |
| } |
| |
| let read_bytes = read_result.unwrap(); |
| assert_eq!(read_bytes, tc.expected_read_size); |
| assert_eq!( |
| &dst[..tc.expected_read_size as usize], |
| &src[..tc.expected_read_size as usize] |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_drain() { |
| const STORAGE_SIZE: u32 = 256; |
| let mut spsc = Buffer::try_new(STORAGE_SIZE).unwrap(); |
| |
| let mut reservation = spsc.reserve(STORAGE_SIZE / 2).unwrap(); |
| let write_data = [b'f'; (STORAGE_SIZE / 2) as usize]; |
| reservation.write(&write_data).unwrap(); |
| reservation.commit().unwrap(); |
| |
| assert_eq!(spsc.load_pointers().available_data(), STORAGE_SIZE / 2); |
| |
| spsc.drain().unwrap(); |
| assert_eq!(spsc.load_pointers().available_data(), 0); |
| } |
| |
| #[test] |
| fn test_commit_error() { |
| const STORAGE_SIZE: u32 = 256; |
| let mut spsc = Buffer::try_new(STORAGE_SIZE).unwrap(); |
| |
| let mut reservation = spsc.reserve(STORAGE_SIZE / 2).unwrap(); |
| // Write fewer bytes than reserved. |
| let write_data = [b'f'; (STORAGE_SIZE / 2) as usize - 1]; |
| reservation.write(&write_data).unwrap(); |
| assert_eq!(reservation.commit(), Err(Status::BAD_STATE)); |
| } |
| |
| #[test] |
| fn test_write_error() { |
| const STORAGE_SIZE: u32 = 256; |
| let mut spsc = Buffer::try_new(STORAGE_SIZE).unwrap(); |
| |
| let mut reservation = spsc.reserve(STORAGE_SIZE / 2).unwrap(); |
| // Write more bytes than reserved. |
| let write_data = [b'f'; (STORAGE_SIZE / 2) as usize + 1]; |
| assert_eq!(reservation.write(&write_data), Err(Status::BUFFER_TOO_SMALL)); |
| reservation.committed = true; |
| } |
| |
| #[test] |
| fn test_from_raw_parts() { |
| let mut mock_storage = [0u8; 256]; |
| |
| // Safety: Pointers are valid. |
| let mut spsc = |
| unsafe { Buffer::from_raw_parts(mock_storage.as_mut_ptr(), mock_storage.len()) }; |
| |
| // Verify reserve, write, commit works |
| let mut reservation = spsc.reserve(100).unwrap(); |
| let write_data = [b'x'; 100]; |
| reservation.write(&write_data).unwrap(); |
| reservation.commit().unwrap(); |
| |
| assert_eq!(spsc.load_pointers().available_data(), 100); |
| assert_eq!(spsc.combined_pointers.load(Ordering::Relaxed) & 0xffffffff, 100); // write pointer is 100 |
| assert_eq!(&mock_storage[..100], &write_data[..]); |
| } |
| |
| #[test] |
| fn test_reserve_zero() { |
| const STORAGE_SIZE: u32 = 256; |
| let mut spsc = Buffer::try_new(STORAGE_SIZE).unwrap(); |
| match spsc.reserve(0) { |
| Err(e) => assert_eq!(e, Status::INVALID_ARGS), |
| Ok(_) => panic!("reserve(0) should fail with INVALID_ARGS"), |
| } |
| } |
| |
| #[test] |
| fn test_reserve_too_large() { |
| const STORAGE_SIZE: u32 = 256; |
| let mut spsc = Buffer::try_new(STORAGE_SIZE).unwrap(); |
| match spsc.reserve(u32::MAX) { |
| Err(e) => assert_eq!(e, Status::INVALID_ARGS), |
| Ok(_) => panic!("reserve(u32::MAX) should fail with INVALID_ARGS"), |
| } |
| } |
| |
| #[test] |
| fn test_reserve_at_break() { |
| const STORAGE_SIZE: u32 = 16; |
| // read raw = 3, write raw = 15 |
| // write_offset = 15 & 15 = 15 (distance to end is exactly 1 byte) |
| // available data = 15 - 3 = 12 bytes |
| // available space = 16 - 12 = 4 bytes |
| let mut mock_storage = [0u8; STORAGE_SIZE as usize]; |
| |
| // Safety: Pointers are valid. |
| let mut spsc = |
| unsafe { Buffer::from_raw_parts(mock_storage.as_mut_ptr(), mock_storage.len()) }; |
| spsc.combined_pointers.store((3u64 << 32) | 15u64, Ordering::Release); |
| |
| let mut reservation = spsc.reserve(4).unwrap(); |
| assert_eq!(reservation.region1.len(), 1); |
| assert_eq!(reservation.region2.len(), 3); |
| |
| let data = [1, 2, 3, 4]; |
| reservation.write(&data).unwrap(); |
| reservation.commit().unwrap(); |
| |
| assert_eq!(mock_storage[15], 1); |
| assert_eq!(mock_storage[0], 2); |
| assert_eq!(mock_storage[1], 3); |
| assert_eq!(mock_storage[2], 4); |
| } |
| |
| #[test] |
| fn test_cpp_rust_integration() { |
| #[link(name = "c++")] |
| unsafe extern "C" { |
| fn cpp_spsc_allocate(size: u32) -> *mut Buffer<NoOpAllocator>; |
| fn cpp_spsc_free(spsc: *mut Buffer<NoOpAllocator>); |
| fn cpp_spsc_write(spsc: *mut Buffer<NoOpAllocator>, data: *const u8, len: u32) -> i32; |
| fn cpp_spsc_read(spsc: *mut Buffer<NoOpAllocator>, dst: *mut u8, len: u32) -> i32; |
| } |
| |
| // 1. Allocate on the C++ side. |
| let spsc_ptr = unsafe { cpp_spsc_allocate(256) }; |
| assert!(!spsc_ptr.is_null()); |
| |
| // Convert the raw pointer to a Rust reference to interact with it in-place. |
| let spsc = unsafe { &mut *spsc_ptr }; |
| |
| // 2. C++ writes, Rust reads. |
| let write_data = b"Hello from C++!"; |
| let cpp_write_status = |
| unsafe { cpp_spsc_write(spsc_ptr, write_data.as_ptr(), write_data.len() as u32) }; |
| assert_eq!(cpp_write_status, 0); // ZX_OK |
| |
| // Rust reads and verifies. |
| let mut read_buf = [0u8; 100]; |
| let bytes_read = spsc |
| .read( |
| |_, src| { |
| read_buf[..src.len()].copy_from_slice(src); |
| Ok(()) |
| }, |
| write_data.len() as u32, |
| ) |
| .unwrap(); |
| assert_eq!(bytes_read, write_data.len() as u32); |
| assert_eq!(&read_buf[..bytes_read as usize], write_data); |
| |
| // 3. Rust writes, C++ reads. |
| let rust_write_data = b"Hello from Rust!"; |
| let mut reservation = spsc.reserve(rust_write_data.len() as u32).unwrap(); |
| reservation.write(rust_write_data).unwrap(); |
| reservation.commit().unwrap(); |
| |
| // C++ reads and verifies. |
| let mut cpp_read_buf = [0u8; 100]; |
| let cpp_read_bytes = unsafe { |
| cpp_spsc_read(spsc_ptr, cpp_read_buf.as_mut_ptr(), rust_write_data.len() as u32) |
| }; |
| assert_eq!(cpp_read_bytes, rust_write_data.len() as i32); |
| assert_eq!(&cpp_read_buf[..cpp_read_bytes as usize], rust_write_data); |
| |
| // 4. Free on the C++ side. |
| unsafe { |
| cpp_spsc_free(spsc_ptr); |
| } |
| } |
| } |