blob: 3ff1817d60e777483a4f335c21b0129174e629af [file] [edit]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
#![cfg_attr(not(test), no_std)]
/// A simple single-threaded ring buffer of type `T` with a backing array of size `N`.
///
/// The effective capacity of this queue is `N - 1`.
///
/// If producer == consumer, the queue is empty.
/// If (producer + 1) % N == consumer, the queue is full.
///
/// Invariants:
/// The producer and consumer indicies are maintained to be less than `N`.
pub struct RingBuffer<T: Copy + Default, const N: usize> {
producer: usize,
consumer: usize,
data: [T; N],
}
/// Creates a default empty `RingBuffer`.
///
/// # Panics
///
/// Panics at compile time if `N <= 1`.
impl<T: Copy + Default, const N: usize> Default for RingBuffer<T, N> {
fn default() -> Self {
// The queue requires at least 2 slots to store 1 item (N-1 capacity).
// Using a const block to ensure this is checked at compile time.
const { assert!(N > 1, "RingBuffer size N must be greater than 1") };
Self {
producer: 0,
consumer: 0,
data: [T::default(); N],
}
}
}
impl<T: Copy + Default, const N: usize> RingBuffer<T, N> {
/// Pushes an item into the buffer.
///
/// Returns `Ok(())` if the item was successfully added, or `Err(item)` if the buffer is full.
pub fn push(&mut self, item: T) -> Result<(), T> {
let next = (self.producer + 1) % N;
if next != self.consumer {
// nosemgrep
unsafe {
// SAFETY: the producer is always less than N.
core::hint::assert_unchecked(self.producer < N);
}
self.data[self.producer] = item;
self.producer = next;
Ok(())
} else {
Err(item)
}
}
/// Removes and returns the first item from the buffer.
///
/// Returns `Some(item)` if the buffer is not empty, or `None` if it is.
pub fn pop(&mut self) -> Option<T> {
if self.consumer != self.producer {
// nosemgrep
unsafe {
// SAFETY: the consumer is always less than N.
core::hint::assert_unchecked(self.consumer < N)
};
let item = self.data[self.consumer];
self.consumer = (self.consumer + 1) % N;
Some(item)
} else {
None
}
}
/// Returns the number of items currently in the buffer.
pub fn len(&self) -> usize {
(self.producer + N - self.consumer) % N
}
/// Returns the number of slots available in the buffer.
pub fn available(&self) -> usize {
(self.consumer + N - self.producer - 1) % N
}
/// Returns `true` if the buffer is empty.
pub fn is_empty(&self) -> bool {
self.producer == self.consumer
}
/// Returns `true` if the buffer is full.
pub fn is_full(&self) -> bool {
(self.producer + 1) % N == self.consumer
}
/// Pushes as many items from the slice into the buffer as possible.
///
/// Returns `Ok(())` if all items were added, or `Err(&[T])` containing the
/// remaining unbuffered items if the buffer became full.
pub fn push_slice<'a>(&mut self, s: &'a [T]) -> Result<(), &'a [T]> {
let free = (self.consumer + N - self.producer - 1) % N;
let n = core::cmp::min(s.len(), free);
let mut remaining = n;
let mut src_idx = 0;
let chunk1 = core::cmp::min(remaining, N - self.producer);
if chunk1 > 0 {
// nosemgrep
unsafe {
// SAFETY: the producer is less than N, and chunk1 is calculated
// to not exceed the remaining space in the buffer.
core::hint::assert_unchecked(self.producer < N);
core::hint::assert_unchecked(self.producer + chunk1 <= N);
}
self.data[self.producer..self.producer + chunk1]
.copy_from_slice(&s[src_idx..src_idx + chunk1]);
self.producer = (self.producer + chunk1) % N;
remaining -= chunk1;
src_idx += chunk1;
}
if remaining > 0 {
// nosemgrep
unsafe {
// SAFETY: remaining is the wrapped portion, which is less than N.
// src_idx + remaining is exactly n, which is <= s.len().
core::hint::assert_unchecked(remaining <= N);
core::hint::assert_unchecked(src_idx + remaining <= s.len());
}
self.data[0..remaining].copy_from_slice(&s[src_idx..src_idx + remaining]);
self.producer = remaining;
}
if n < s.len() {
Err(&s[n..])
} else {
Ok(())
}
}
/// Returns a slice containing the contiguous part of the buffered data.
///
/// If the data wraps around the end of the backing array, this only returns
/// the first part. Callers should use `consume()` and `as_slice()` again to
/// retrieve the wrapped portion.
pub fn as_slice(&self) -> &[T] {
// nosemgrep
unsafe {
// SAFETY: producer and consumer indices are always less than or equal to N.
core::hint::assert_unchecked(self.consumer <= N);
core::hint::assert_unchecked(self.producer <= N);
}
if self.consumer <= self.producer {
// The available slice is contiguous in the array.
&self.data[self.consumer..self.producer]
} else {
// Slices can't wrap around the end of the array, so give just the chunk we can give.
&self.data[self.consumer..]
}
}
/// Advances the consumer pointer by `n` items, effectively removing them from the buffer.
///
/// If `n` is greater than the current length, only the available items are consumed.
pub fn consume(&mut self, n: usize) {
let n = core::cmp::min(n, self.len());
self.consumer = (self.consumer + n) % N;
}
}
#[cfg(test)]
#[allow(clippy::bool_assert_comparison)]
mod tests {
use super::*;
#[test]
fn test_push_pop() {
let mut q = RingBuffer::<u8, 4>::default();
assert_eq!(q.push(b'b'), Ok(()));
assert_eq!(q.push(b'y'), Ok(()));
assert_eq!(q.push(b'e'), Ok(()));
assert_eq!(q.push(b'x'), Err(b'x')); // Overflow
assert_eq!(q.len(), 3);
assert_eq!(q.is_empty(), false);
assert_eq!(q.is_full(), true);
assert_eq!(q.pop(), Some(b'b'));
assert_eq!(q.pop(), Some(b'y'));
assert_eq!(q.pop(), Some(b'e'));
assert_eq!(q.pop(), None); // No more items.
assert_eq!(q.len(), 0);
assert_eq!(q.is_empty(), true);
assert_eq!(q.is_full(), false);
}
#[test]
fn test_as_slice() {
let mut q = RingBuffer::<u8, 8>::default();
assert_eq!(q.push_slice(b"Hello"), Ok(()));
assert_eq!(q.as_slice(), b"Hello");
q.consume(5);
assert_eq!(q.is_empty(), true);
// This part will be wrapped around the end of the array,
// so we need to perform two `as_slice` ops to get the whole thing.
assert_eq!(q.push_slice(b"World"), Ok(()));
assert_eq!(q.as_slice(), b"Wor");
q.consume(3);
assert_eq!(q.as_slice(), b"ld");
q.consume(2);
assert_eq!(q.is_empty(), true);
}
}