1 unstable release

0.1.0 May 29, 2023

#680 in Concurrency

41 downloads per month

Custom license

40KB
718 lines

Introduction

This is a heapless single producer single consumer (SPSC) ring buffer that depends only on the Rust Core library. The implementation consists of an inner ring buffer and a lightweight wrapper for providing a pair of mutable access handlers that can be moved to their corresponding producer and consumer entities. The inner ring buffer is designed to be easily instantiable as global static encapsulating any need of using unsafe to access the structure in user code.

Basic ring buffer

The underlying ring buffer utilizes the "Array + two unmasked indices" method illustrated here. Essentially this allows usage of all elements allocated in the buffer without resorting to storing length along with read and write indices. If the Const Generics parameter capacity N is power of two, the overhead of masking read and write indices (within [0, N-1]) is only required upon buffer access. If N is not a power of two, the implementation wraps the indices between 0 and 2*N-1 as they are incremented.

The maximum supported size of buffer is 2^32 - 1 items. This limit comes from the size of the read and write indices (u32). This can be relaxed if needed by changing the index to usize type.

The ring buffer item is a generic structure with no trait requirements.

Usage model

The producer calls write_front() to beginning writing to the location under buffer[mask(wr_idx)]. This returns a mutable reference to the underlying item. After the location is populated, the producer calls commit() to advance the write index. The consumer can use read_front() to check (and read) new items available. The consumer uses pop() to consume the item and advance the read index.

Working around limitations of static global

In order to achieve the goal of allocating these queues statically while avoid requiring all code that uses the structure to be wrapped in unsafe, the implementation employs the following methods:

  1. Cell wrapped read and write indices for providing interior mutability of indices
  2. limited internal unsafe code to return mutable references of the inner buffer. This is considered safe due to the single producer and single consumer premise. i.e. write index is only modified by the producer and read index only modified by the consumer.
  3. Use of MaybeUninit in inner buffer to avoid static initialization (not required in queues)
  4. Cell structures are not inheritently thread safe and globals containing Cells cannot be instantiated for the lack of the Sync trait marker. Again due to the SPSC premise, the core::marker::Sync is added to the RingbufRef structure. Straightly speaking though, the marker should be only added to wrapper Ringbuf structure which enforces SPSC use with the split operation. But this is not done to allow instantianting global RingbufRef without incurring the handler overhead should the user decides the usage is safe enouhg.

Top level wrapper

The top level RingBuf structure provides the final protection of unintended direct usage of the inner ring buffer. This wrapper provides a one-time call returning a pair of (mutable) handles that can be moved to producer and consumer entities. Since the writ_front, commit and pop functions all require mutable self, Rust's single mutable reference check should guarantee that only a single producer or consumer is possible.

Shared Singleton

This crate also provides a separate cheaper implementation for the special case of single item ring buffer. In this version a tri-state owner flag replaces all read and write indices manipulation overhead. This structure can also be used in conjunction with the ring buffer to serve as ringbuffer data extension. In a heapless environment, for example, one can imagine with dynamic allocation it would be difficult to create a command queue with a deeper depth than available command "payloads" since not all commands require a payload.

In the included SharedPool example, payload can be allocated from a pool of such shared singletons with pool index passed by command in the queue. The owner flag provides separate ownership tracking of each item in the payload pool. On initialization, the return ring buffer is filled to indicate that every pool item belongs to the allocation producer. Producer allocates an item in the pool and pass the index along with any other information through the alloc_prod ring buffer. As the consumer is done with the allocated item, it is returned (in the form of the pool index) through the return ring buffer.

                        Pool of SharedSingleton<T>
                        ┌─┬─┬─┬─┐   ┌───┐
                        │0123..│N-1│
                        └─┴─┴─┴┬┘   └───┘
┌────────────────┐             │PoolIndex<N>       ┌────────────────┐
│Producer        │         ┌───┘                   │Consumer        │
│ ┌───────────┐  │         │                       │ ┌───────────┐  │
│ │alloc_prod ├──┼───┐  ┌─┬┴┬─┬─┬─┐   ┌───┐     ┌──► │alloc_cons │  │
│ └───────────┘  │   └─►│01234.. │M-1├─────┘  │ └───────────┘  │
│  RingbufProducer      └─┴─┴─┴─┴─┘   └───┘        │                │
│ ┌───────────┐  │   Prod. -> Cons Ringbuf<Q,M>    │ ┌───────────┐  │
│ │return_cons│◄─┼──┐                            ┌─┼─┤return_prod│  │
│ └───────────┘  │  │   ┌─┬─┬─┬─┬─┐   ┌───┐      │ │ └───────────┘  │
│ RingbufConsumer│  └───┤01234.. │M-1│◄─────┘ │                │
└────────────────┘      └─┴─┴─┴─┴─┘   └───┘        └────────────────┘
                    Cons. -> Prod. Ringbuf<Q,M>


No runtime deps