planck_noalloc/ringbuf.rs
1//! Circular/ring buffer implementation with fixed capacity.
2//!
3//! This module provides [`RingBuf`], a circular buffer (also known as a ring buffer)
4//! that operates in a First-In-First-Out (FIFO) manner. It's particularly useful for
5//! producer-consumer scenarios, buffering streams of data, or implementing queues
6//! without heap allocation.
7//!
8//! # Overview
9//!
10//! A ring buffer is a fixed-size buffer that wraps around when it reaches the end,
11//! forming a conceptual circle. Elements are added at the head and removed from the
12//! tail, making it ideal for streaming data and FIFO queues.
13//!
14//! # Capacity
15//!
16//! The buffer has a compile-time fixed size `SIZE`, but the actual usable capacity
17//! is `SIZE - 1`. This is a common implementation technique that simplifies the
18//! empty/full detection logic.
19//!
20//! # Use Cases
21//!
22//! Ring buffers are excellent for:
23//! - Buffering serial/UART data in embedded systems
24//! - Implementing producer-consumer queues
25//! - Audio/video frame buffers
26//! - Network packet buffers
27//! - Log message buffers
28//! - Any scenario requiring a fixed-size FIFO queue
29//!
30//! # Performance
31//!
32//! - Push: O(1)
33//! - Pop: O(1)
34//! - Space efficient: uses exactly `SIZE * sizeof(T)` bytes
35//! - Lock-free for single producer/single consumer scenarios
36//!
37//! # Examples
38//!
39//! ```
40//! use planck_noalloc::ringbuf::RingBuf;
41//!
42//! // Create a ring buffer with size 8 (capacity 7)
43//! let mut buf = RingBuf::<u8, 8>::new();
44//!
45//! // Push some data
46//! buf.push(1);
47//! buf.push(2);
48//! buf.push(3);
49//!
50//! // Pop in FIFO order
51//! assert_eq!(buf.pop(), Some(1));
52//! assert_eq!(buf.pop(), Some(2));
53//! assert_eq!(buf.len(), 1);
54//!
55//! // Can push more after popping
56//! buf.push(4);
57//! buf.push(5);
58//!
59//! assert_eq!(buf.len(), 3);
60//! ```
61//!
62//! ## Error Handling
63//!
64//! ```
65//! use planck_noalloc::ringbuf::RingBuf;
66//!
67//! let mut buf = RingBuf::<u8, 4>::new();
68//!
69//! // Fill to capacity (3 elements for size 4)
70//! buf.push(1);
71//! buf.push(2);
72//! buf.push(3);
73//!
74//! // Trying to push beyond capacity returns an error
75//! assert!(buf.try_push(4).is_err());
76//! assert!(buf.is_full());
77//!
78//! // After popping, we can push again
79//! buf.pop();
80//! assert!(buf.try_push(4).is_ok());
81//! ```
82
83use core::mem::MaybeUninit;
84
85/// A Circular / Ring buffer.
86///
87/// A fixed-capacity FIFO (First-In-First-Out) data structure that wraps around
88/// when it reaches the end of its backing array. Elements are added at the head
89/// and removed from the tail.
90///
91/// # Capacity
92///
93/// The buffer is considered 'full' when it contains `SIZE - 1` elements.
94/// This design choice simplifies the empty/full detection logic.
95///
96/// # Type Parameters
97///
98/// - `T`: The type of elements stored (must be `Copy`)
99/// - `SIZE`: The size of the backing array (usable capacity is `SIZE - 1`)
100///
101/// # Examples
102///
103/// ```
104/// use planck_noalloc::ringbuf::RingBuf;
105///
106/// let mut buf = RingBuf::<i32, 16>::new();
107///
108/// buf.push(10);
109/// buf.push(20);
110/// buf.push(30);
111///
112/// assert_eq!(buf.pop(), Some(10));
113/// assert_eq!(buf.pop(), Some(20));
114/// assert_eq!(buf.len(), 1);
115/// ```
116#[derive(Clone, Copy)]
117pub struct RingBuf<T: Copy, const SIZE: usize> {
118 buf: [MaybeUninit<T>; SIZE],
119 head: usize,
120 tail: usize,
121}
122
123impl<T, const N: usize> Default for RingBuf<T, N>
124where
125 T: Copy,
126{
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl<T, const N: usize> RingBuf<T, N>
133where
134 T: core::marker::Copy,
135{
136 /// The total size of the backing array. The usable capacity is `SIZE - 1`.
137 pub const SIZE: usize = N;
138
139 /// Create a new ringbuf with no data inside
140 ///
141 /// This method does not allocate memory.
142 ///
143 /// # Example
144 /// ```
145 /// use planck_noalloc::ringbuf::RingBuf;
146 ///
147 /// // Create an empty ringbuf
148 /// let ringbuf = RingBuf::<u8, 8>::new();
149 /// assert!(ringbuf.is_empty());
150 /// ```
151 #[must_use]
152 pub const fn new() -> Self {
153 Self {
154 buf: [const { MaybeUninit::uninit() }; N],
155 head: 0,
156 tail: 0,
157 }
158 }
159
160 /// Returns true if the ring buffer is empty
161 ///
162 /// # Example
163 /// ```
164 /// use planck_noalloc::ringbuf::RingBuf;
165 ///
166 /// // Create an empty ringbuf
167 /// let mut ringbuf = RingBuf::<u8, 8>::new();
168 /// assert!(ringbuf.is_empty());
169 /// ringbuf.push(42);
170 /// assert!(!ringbuf.is_empty());
171 /// ````
172 #[must_use]
173 pub const fn is_empty(&self) -> bool {
174 // Because we only supported SIZE-1 as maximum capacity,
175 // buf is empty when head == tail
176 self.head == self.tail
177 }
178
179 /// Returns the length of the used buffer
180 ///
181 /// # Example
182 /// ```
183 /// use planck_noalloc::ringbuf::RingBuf;
184 ///
185 /// // Create an empty ringbuf
186 /// let mut ringbuf = RingBuf::<u8, 8>::new();
187 /// assert_eq!(ringbuf.len(), 0);
188 /// ringbuf.push(1);
189 /// ringbuf.push(2);
190 /// ringbuf.push(3);
191 /// assert_eq!(ringbuf.len(), 3);
192 /// # let popped =
193 /// ringbuf.pop();
194 /// # assert_eq!(popped, Some(1));
195 /// assert_eq!(ringbuf.len(), 2);
196 /// ```
197 #[must_use]
198 pub const fn len(&self) -> usize {
199 (self.head + Self::SIZE - self.tail) % Self::SIZE
200 }
201
202 /// Returns true if the buffer is full
203 ///
204 /// # Example
205 /// ```
206 /// use planck_noalloc::ringbuf::RingBuf;
207 ///
208 /// // Create an empty ringbuf
209 /// let mut ringbuf = RingBuf::<u8, 8>::new();
210 /// for i in 0..6 {
211 /// ringbuf.push(i);
212 /// }
213 /// assert!(!ringbuf.is_full());
214 /// ringbuf.push(6);
215 /// assert!(ringbuf.is_full());
216 /// ````
217 #[must_use]
218 pub const fn is_full(&self) -> bool {
219 (self.head + 1) % N == self.tail
220 }
221
222 /// Returns the maximum number of elements that can be stored in the ringbuf
223 /// This is calculated as the size subtracted 1
224 #[must_use]
225 pub const fn max_capacity(&self) -> usize {
226 Self::SIZE - 1
227 }
228
229 /// Pushes (or enqueues) an element on the ring buffer
230 ///
231 /// # Errors
232 /// Returns an error with the pushed value if the ringbuf is full
233 pub fn try_push(&mut self, x: T) -> Result<(), T> {
234 if self.is_full() {
235 return Err(x);
236 }
237
238 // SAFETY: We checked that it isn't full
239 unsafe { self.push_unchecked(x) };
240 Ok(())
241 }
242
243 /// Pushes (or enqueues) an element on the ring buffer
244 ///
245 /// # Panics
246 /// Panics if the ringbuf is full. If this is not what you want, see [`RingBuf::try_push`] or
247 /// [`RingBuf::push_unchecked`].
248 pub fn push(&mut self, x: T) {
249 assert!(self.try_push(x).is_ok(), "ringbuf is full");
250 }
251
252 /// Pushes (or enqueues) an element on the ring buffer
253 ///
254 /// # Safety
255 /// This does not check if it is out of bounds, which may cause data to be overwritten
256 pub unsafe fn push_unchecked(&mut self, x: T) {
257 self.buf[self.head].write(x);
258 self.head = (self.head + 1) % N;
259 }
260
261 /// Pops (or dequeues) an element off the ring buffer
262 ///
263 /// Returns none if the ringbuf is empty
264 #[must_use]
265 pub fn pop(&mut self) -> Option<T> {
266 if self.is_empty() {
267 return None;
268 }
269
270 // SAFETY: The element at `self.tail` is initialized because the buffer
271 // is not empty (head != tail), and all elements between tail and head
272 // were written by previous push operations.
273 let x = unsafe { self.buf[self.tail].assume_init_read() };
274 self.buf[self.tail] = MaybeUninit::uninit();
275 self.tail = (self.tail + 1) % N;
276 Some(x)
277 }
278}
279
280#[cfg(all(test, feature = "std"))]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_is_empty() {
286 let buf = RingBuf::<u8, 1024>::new();
287 assert!(buf.is_empty());
288 }
289
290 #[test]
291 fn test_push() {
292 let mut buf = RingBuf::<u8, 1024>::new();
293 buf.try_push(15).unwrap();
294 buf.try_push(42).unwrap();
295 assert_eq!(unsafe { buf.buf[0].assume_init_read() }, 15);
296 assert_eq!(unsafe { buf.buf[1].assume_init_read() }, 42);
297 }
298
299 #[test]
300 fn test_push_rollover() {
301 let mut buf = RingBuf::<u8, 1024>::new();
302 assert_eq!(buf.max_capacity(), 1023);
303 for i in 0..1023 {
304 let res = buf.try_push((i % 255) as u8);
305 assert!(res.is_ok());
306 }
307 assert!(buf.try_push(1).is_err());
308 assert_eq!(buf.len(), 1023);
309
310 // Now we pop one
311 let res = buf.pop();
312 assert!(res.is_some());
313 assert_eq!(res.unwrap(), 0);
314 assert_eq!(buf.len(), 1022);
315 let res = buf.try_push(1);
316 assert!(res.is_ok());
317 assert_eq!(buf.len(), 1023);
318 assert!(buf.is_full());
319 }
320
321 #[test]
322 fn pop_empty() {
323 let mut buf = RingBuf::<u8, 8>::new();
324 assert_eq!(buf.pop(), None);
325 }
326
327 #[test]
328 fn push_pop_fifo_order() {
329 let mut buf = RingBuf::<u8, 8>::new();
330 buf.push(1);
331 buf.push(2);
332 buf.push(3);
333 assert_eq!(buf.pop(), Some(1));
334 assert_eq!(buf.pop(), Some(2));
335 assert_eq!(buf.pop(), Some(3));
336 assert_eq!(buf.pop(), None);
337 }
338
339 #[test]
340 fn len_tracking() {
341 let mut buf = RingBuf::<u8, 8>::new();
342 assert_eq!(buf.len(), 0);
343 buf.push(1);
344 assert_eq!(buf.len(), 1);
345 buf.push(2);
346 assert_eq!(buf.len(), 2);
347 let _ = buf.pop();
348 assert_eq!(buf.len(), 1);
349 }
350
351 #[test]
352 fn is_full_check() {
353 let mut buf = RingBuf::<u8, 4>::new();
354 assert!(!buf.is_full());
355 buf.push(1);
356 buf.push(2);
357 buf.push(3);
358 assert!(buf.is_full());
359 }
360
361 #[test]
362 fn max_capacity_value() {
363 let buf = RingBuf::<u8, 8>::new();
364 assert_eq!(buf.max_capacity(), 7);
365 }
366
367 #[test]
368 fn wrap_around_multiple_times() {
369 let mut buf = RingBuf::<u8, 4>::new();
370 // Capacity is 3. Do multiple wrap-arounds.
371 for round in 0u8..5 {
372 buf.push(round * 3);
373 buf.push(round * 3 + 1);
374 buf.push(round * 3 + 2);
375 assert_eq!(buf.pop(), Some(round * 3));
376 assert_eq!(buf.pop(), Some(round * 3 + 1));
377 assert_eq!(buf.pop(), Some(round * 3 + 2));
378 assert!(buf.is_empty());
379 }
380 }
381
382 #[test]
383 #[should_panic(expected = "ringbuf is full")]
384 fn push_panics_when_full() {
385 let mut buf = RingBuf::<u8, 4>::new();
386 buf.push(1);
387 buf.push(2);
388 buf.push(3);
389 buf.push(4); // should panic
390 }
391}