ring-buffer-macro 0.2.0

A procedural macro for creating ring buffer (circular buffer) data structures at compile time
Documentation

ring-buffer-macro

Crates.io docs.rs License Website

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"

How it works

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.

Usage

Standard mode

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());
}

Generics

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();

SPSC (lock-free, single-producer/single-consumer)

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();
}

MPSC (multi-producer, single-consumer)

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();
    }
}

Blocking mode

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();

Power-of-two optimization

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);

Cache-line padding

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);

Configuration

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)]

Generated API

Standard mode

  • 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).

SPSC mode

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()

MPSC mode

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()

Requirements

  • 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

License

MIT