I'm the maintainer of the rtrb crate, which provides a wait-free SPSC (single producer, single consumer) ring buffer that's suitable for real-time use case like audio processing.
For the simplest case, it provides very straightforward
Consumer::pop() methods for writing and reading single items (of some type
In addition, it also provides methods for writing/reading chunks of multiple items (like, e.g., chunks of
f32 audio samples as provided by the sound card). Since the underlying data structure is a circular buffer, those chunks sometimes happen to "wrap around", which means that the beginning of the chunk is stored at the very end of the underlying buffer and the rest of the chunk is stored starting at the very beginning of the buffer. This is natural and expected, but if I know that I'm always using the same chunk size, I can choose the ring buffer length in a way that this "wrap around" never happens within a chunk.
So far so good, there is a RingBuffer::with_chunks(chunks, chunk_size) method which allows me to define a ring buffer with a size that's an integer multiple of my chunk size. However, there are two drawbacks that I don't like about this:
- Even though I as a user know that my chunks are always the same size, the compiler doesn't know that. The "chunk" methods still have to provide two slices of data, even though I know that the second one is always empty (because there is no "wrap around").
- The constructor has two unsigned integer arguments, which might be easily confounded.
As a solution to those two issues I've come up with a pull request and I'd like to hear your opinions on that:
I've split the
with_chunks() constructor into two parts to make clear what each of the two numbers mean:
RingBuffer::with_chunks(...).of_size(...). I guess this is somehow related to the "builder" pattern, but it isn't a typical case, because there are always exactly two arguments in exactly the same order.
So my first question: is this a bad idea?
I could just as well keep the more conventional (?) two-argument form
RingBuffer::with_chunks(..., ...). Are there any further suggestions?
In a future version of Rust there might be named arguments, which would help here, but this might never happen. And I would like to have something that works now.
The next question is about the return type of this constructor. Instead of returning a
RingBuffer, it returns a pair of producer and consumer
structs, which are typically passed on to different threads. What makes this a bit unconventional is the fact that not both sides have to use a fixed chunk size. It's perfectly reasonable to have one side use
.pop() with single elements, while the other side uses fixed chunks. Therefore, there are three possible combinations of return types:
In order to achieve this, I've created a
trait for the return value. This trait has to be public, but its name is not important for the user and therefore it is not documented.
So my second question: is this a bad idea?
As I have currently implemented it in my PR, the concrete return types are inferred from assignments to variables with known types or they can be specified with type annotations. I guess it would also be possible to provide multiple constructors with different names that each return a concrete pair of types. I guess this would be more conventional, but I couldn't come up with good names for those constructor functions and I somehow like the generic return type, but it may be a bit too fancy? Is there an alternative way to solve this?
Finally, to complete the API, there is one constructor for the "simple" case where no fixed chunks are used. This is implemented as
RingBuffer::new(capacity) -> (Producer, Consumer). This is definitely unconventional, because I have to appease Clippy with
So my third question: is this a bad idea?
Clippy would be silent if I chose a different name for the constructor function, any suggestions?
In the current API (before the aforementioned PR), I've worked around the issue by using the signature
RingBuffer::new(capacity) -> RingBuffer. The returned
RingBuffer then has a
.split() method which in turn returns a pair of
However, with my new PR there are two "types" of
RingBuffer: fixed size chunks or not. I don't really want to use two separate
structs for that, so the solution would be to never actually return a
In summary, I have two "constructor" methods on
RingBuffer that among them return 4 possible pairs of types, none of which actually is a
I'm not sure whether I should like this or if I should be disgusted, what is your opinion?
BTW, neither the number of chunks nor the chunk size is supposed to be
const. This would be a nice usage for the recently stabilized const generics, but for this project I'm interested in the non-