bytearray-ringbuffer 0.4.0

A no_std, no-alloc ring buffer for variable-length byte slices in Rust
Documentation

crates.io crates.io

Bytearray-Ringbuffer

An embedded-friendly VecDeque<Vec<u8>>.

BytearrayRingbuffer stores byte slices in a fixed-size array, similar to heapless::Vec. However, the elements in BytearrayRingbuffer are not required to have a fixed size. Instead, information about each slice (ie. length) is stored alongside the payload. Each slice data uses up data.len() + 8 bytes in the buffer.

This is useful for efficiently storing elements of very different lengths, as short elements do not have to be padded.

One downside is that elements may wrap around at the end of the buffer. For pop_front, iter, iter_backwards, nth, and nth_reverse, each element is returned as two slices (a, b). If the element does not wrap around, a will contain all data and b will be empty. Otherwise, a and b have to be concatenated to yield the full result. nth_contiguous is the exception: it returns a single &[u8] and may rotate the backing array so that slice is contiguous. It returns None if the queue is empty or if n >= count().

MSRV

Rust 1.85 or later (edition = "2024").

Usage

use bytearray_ringbuffer::BytearrayRingbuffer;
// Create a buffer with a capacity of 64 bytes.
let mut buffer = BytearrayRingbuffer::<64>::new();
// store some packets
buffer.push(b"hello world").unwrap();
buffer.push(b"").unwrap();
buffer.push(b"testing").unwrap();
// number of packets
assert_eq!(buffer.count(), 3);
// iterate (oldest first; does not remove entries)
for (a, b) in buffer.iter() {
    // a and b are &[u8]
    let mut output = Vec::new();
    output.extend_from_slice(a);
    output.extend_from_slice(b);
    // now output contains the original elements
}

Multipart push

When the full payload is not available up-front, use push_multipart (or push_multipart_force) to build a packet incrementally:

use bytearray_ringbuffer::BytearrayRingbuffer;
let mut buffer = BytearrayRingbuffer::<64>::new();
// begin an in-progress packet
let mut mp = buffer.push_multipart().unwrap();
mp.push(b"hello").unwrap();
mp.push(b" world").unwrap();
drop(mp); // commits the packet "hello world"
assert_eq!(buffer.count(), 1);

// or discard the in-progress write:
let mut mp = buffer.push_multipart().unwrap();
mp.push(b"discarded").unwrap();
mp.cancel(); // head is rewound; count is unchanged
assert_eq!(buffer.count(), 1);

Performance

The tradeoff here is that random access by index (nth, nth_reverse, nth_contiguous) walks packets from an end of the queue, so it is linear in the number of packets skipped.

operation time complexity
push() O(1)
push_force() O(1) amortized per push; worst case O(m) if m oldest packets are dropped to make room
push_multipart() O(1) to create the guard; each push call is O(1); drop/cancel is O(1)
push_multipart_force() O(1) to create the guard; each push call is O(1) amortized; drop/cancel is O(1)
pop_front() O(1)
iter() / iter_backwards() O(1) to create the iterator; O(1) per next(); O(k) to visit all k packets
count() O(1)
nth(n) / nth_reverse(n) O(n) in the index n (packets walked)
nth_contiguous(n) O(n) to locate the packet, plus O(N) if the buffer is rotated

Changelog

See CHANGELOG.md in the repository.