A proc macro that turns a tuple struct into a fixed-size ring buffer. Supports single-threaded, lock-free SPSC, and lock-free MPSC modes.
[dependencies]
ring-buffer-macro = "0.2.0"You write a tuple struct with one field indicating the element type:
#[ring_buffer(5)]
struct IntBuffer(i32);The macro replaces this with a named struct (data, head, tail, etc.) and generates a full impl block with ring buffer methods. The tuple struct is just a way to specify the name and element type — the (i32) field gets thrown away.
For concurrent modes (SPSC/MPSC), the generated struct uses UnsafeCell<Vec<MaybeUninit<T>>> with atomic indices instead of plain Vec<T>, so items are moved rather than cloned. This drops the trait bound from Clone to Send.
use ring_buffer_macro::ring_buffer;
#[ring_buffer(5)]
struct IntBuffer(i32);
fn main() {
let mut buf = IntBuffer::new();
buf.enqueue(1).unwrap();
buf.enqueue(2).unwrap();
buf.enqueue(3).unwrap();
assert_eq!(buf.peek(), Some(&1));
assert_eq!(buf.peek_back(), Some(&3));
for item in buf.iter() {
println!("{}", item);
}
assert_eq!(buf.dequeue(), Some(1));
// drain() removes items as it iterates
let rest: Vec<_> = buf.drain().collect();
assert!(buf.is_empty());
}Type parameters are preserved in the generated code:
#[ring_buffer(10)]
struct GenericBuffer<T: Clone>(T);
let mut buf: GenericBuffer<String> = GenericBuffer::new();
buf.enqueue("hello".to_string()).unwrap();Uses AtomicUsize head/tail with acquire/release ordering. No locks.
use ring_buffer_macro::ring_buffer;
use std::sync::Arc;
use std::thread;
#[ring_buffer(capacity = 1024, mode = "spsc")]
struct MessageQueue(String);
fn main() {
let queue = Arc::new(MessageQueue::new());
let q1 = Arc::clone(&queue);
let producer = thread::spawn(move || {
let (p, _) = q1.split();
for i in 0..100 {
while p.try_enqueue(format!("msg {}", i)).is_err() {}
}
});
let q2 = Arc::clone(&queue);
let consumer = thread::spawn(move || {
let (_, c) = q2.split();
for _ in 0..100 {
while c.try_dequeue().is_none() {}
}
});
producer.join().unwrap();
consumer.join().unwrap();
}Producers coordinate via compare_exchange_weak on the tail index. Each slot has an AtomicBool flag so the consumer knows when a write is complete.
The producer handle is Clone; the consumer is not. Only one consumer should exist — this is a protocol constraint, not enforced at runtime.
use ring_buffer_macro::ring_buffer;
use std::sync::Arc;
use std::thread;
#[ring_buffer(capacity = 1024, mode = "mpsc")]
struct WorkQueue(i32);
fn main() {
let queue = Arc::new(WorkQueue::new());
let mut handles = vec![];
for i in 0..4 {
let q = Arc::clone(&queue);
handles.push(thread::spawn(move || {
let producer = q.producer();
for j in 0..100 {
while producer.try_enqueue(i * 100 + j).is_err() {}
}
}));
}
let q = Arc::clone(&queue);
handles.push(thread::spawn(move || {
let consumer = q.consumer();
let mut count = 0;
while count < 400 {
if consumer.try_dequeue().is_some() {
count += 1;
}
}
}));
for h in handles {
h.join().unwrap();
}
}Available for both SPSC and MPSC. Uses a Mutex<()> + Condvar pair — the mutex doesn't protect data, it just satisfies the condvar API. The actual data path is still lock-free atomics.
use ring_buffer_macro::ring_buffer;
#[ring_buffer(capacity = 64, mode = "mpsc", blocking = true)]
struct BlockingQueue(String);
// These wait instead of returning Err/None
// producer.enqueue_blocking("message".to_string());
// let msg = consumer.dequeue_blocking();If your capacity is a power of two, this swaps modulo for bitwise AND on index wraparound. The macro enforces the constraint at compile time.
#[ring_buffer(capacity = 1024, power_of_two = true)]
struct FastBuffer(u8);Aligns head and tail to 64-byte boundaries to prevent false sharing when producer and consumer run on different cores. Mostly relevant for SPSC mode.
#[ring_buffer(capacity = 1024, mode = "spsc", cache_padded = true)]
struct PaddedQueue(u8);| Option | Values | Default | Description |
|---|---|---|---|
capacity |
positive integer | required | Maximum number of elements |
mode |
"standard", "spsc", "mpsc" |
"standard" |
Buffer mode |
power_of_two |
true, false |
false |
Bitwise indexing (capacity must be 2^n) |
cache_padded |
true, false |
false |
64-byte align head/tail to avoid false sharing |
blocking |
true, false |
false |
Blocking enqueue/dequeue (concurrent modes only) |
// simple
#[ring_buffer(10)]
// named
#[ring_buffer(capacity = 1024, mode = "spsc", power_of_two = true, cache_padded = true)]new()/enqueue(item)/dequeue()/clear()peek()/peek_mut()/peek_back()iter()/drain()is_full()/is_empty()/len()/capacity()
dequeue() and drain() require T: Clone (bound is on the method, not the struct, so you can create a buffer of non-Clone types — you just can't dequeue from it).
Buffer: new(), split() -> (Producer, Consumer), is_full(), is_empty(), len(), capacity()
Producer: try_enqueue(item), enqueue_blocking(item) (if blocking)
Consumer: try_dequeue(), dequeue_blocking() (if blocking), peek()
Buffer: new(), producer(), consumer(), is_empty(), len(), capacity()
Producer (clonable): try_enqueue(item), enqueue_blocking(item) (if blocking), is_full()
Consumer: try_dequeue(), dequeue_blocking() (if blocking), peek(), is_empty(), len()
- Input must be a tuple struct with one field:
struct Buffer(i32) - Standard mode requires
T: Clone(for dequeue/drain only) - SPSC/MPSC modes require
T: Send - Capacity must be a positive integer
MIT