Code review: Barebone triple buffer
⚓ Rust 📅 2025-09-24 👤 surdeus 👁️ 8Partly because I need to publish a state from one thread to another and partly because I want to learn how to do this, I wrote a triple buffer. I decided to constrain it to big objects, i.e. the ones that implement the clone trait. For convenience I require also the default trait.
The triple buffer itself holds two naked pointers to such objects, an atomic pointer and an atomic bool. The "constructor" of the buffer gives a producer and a consumer that cannot be cloned. They both contain an Arc to the triple buffer for cleanup and a pointer to the triple buffer.
There is no way to "push" a state to the buffer. Instead you have to ask the producer of a mutable reference to the state that the buffer holds and then write on it. After that you have to drop the reference and publish the state. When publishing the buffer swaps the back pointer and the atomic pointer and sets the atomic bool to true. Then when you ask the consumer for the state it asks the buffer for it and if the atomic bool is true it swaps the front pointer and the atomic pointer and it returns a mutable reference to the front object.
I opted not to use an array of length 3 to keep the objects and have an atomic u8 for bookeeping only because this would be more complicated and error-prone. Is there a significant overhead in having both an atomic pointer and an atomic bool instead of just an atomic u8?
The design is pretty straightforward but it uses quite a few of dodgy unsafe operations. In construction I create 3 boxes with the default objects and turn them into pointers. Then every time the front buffer or the back buffer needs to be borrowed, I turn the appropriate pointer to a mutable reference.
I have tried it out in tests and it seems to be working as expected. However, I'm afraid that I missed something that can break it or that I have written something very inefficient.
I have pasted the important parts of my implementation. Any feedback is welcome.
pub(crate) struct TripleBuffer<T>
where
T: Send + Clone + Default,
{
back: *mut T,
idle: AtomicPtr<T>,
front: *mut T,
new_data: Arc<AtomicBool>,
}
impl<T> TripleBuffer<T>
where
T: Send + Clone + Default,
{
pub(crate) fn init() -> (TripleBufferProducer<T>, TripleBufferConsumer<T>) {
let back = Box::into_raw(Box::new(T::default()));
let idle = Box::into_raw(Box::new(T::default()));
let front = Box::into_raw(Box::new(T::default()));
let new_data = Arc::new(AtomicBool::new(false));
let buffer = Arc::new(TripleBuffer {
back,
idle: AtomicPtr::new(idle),
front,
new_data,
});
return (TripleBufferProducer::new(buffer.clone()), TripleBufferConsumer::new(buffer));
}
#[inline(always)]
pub(super) fn get_back(&mut self) -> &mut T {
unsafe {
return &mut *self.back;
}
}
#[inline(always)]
pub(super) fn publish_back(&mut self) {
let swapped = self.idle.swap(self.back, Ordering::Acquire);
self.back = swapped;
self.new_data.store(true, Ordering::Release);
}
#[inline(always)]
pub(super) fn get_new_front(&mut self) -> Option<&mut T> {
if self.new_data.load(Ordering::Acquire) {
let swapped = self.idle.swap(self.front, Ordering::Acquire);
self.front = swapped;
self.new_data.store(false, Ordering::Release);
unsafe {
return Some(&mut *self.front);
}
}
return None;
}
#[inline(always)]
pub(super) fn get_front(&mut self) -> &mut T {
if self.new_data.load(Ordering::Acquire) {
let swapped = self.idle.swap(self.front, Ordering::Acquire);
self.front = swapped;
self.new_data.store(false, Ordering::Release);
}
unsafe {
return &mut *self.front;
}
}
}
impl<T> Drop for TripleBuffer<T>
where
T: Send + Clone + Default,
{
fn drop(&mut self) {
unsafe {
let _back = Box::from_raw(self.back);
let _front = Box::from_raw(self.front);
let idle_ptr = self.idle.swap(ptr::null_mut(), Ordering::Relaxed);
if !idle_ptr.is_null() {
let _idle = Box::from_raw(idle_ptr);
}
}
}
}
// producer
pub(crate) struct TripleBufferProducer<T>
where
T: Send + Clone + Default,
{
pub(super) buffer: Arc<TripleBuffer<T>>,
pub(super) ptr: *mut TripleBuffer<T>,
}
impl<T> TripleBufferProducer<T>
where
T: Send + Clone + Default,
{
pub fn new(buffer: Arc<TripleBuffer<T>>) -> Self {
let ptr = Arc::as_ptr(&buffer) as *mut TripleBuffer<T>;
Self {
buffer,
ptr,
}
}
#[inline(always)]
fn get_state(&mut self) -> &mut T {
let mutable_ref = unsafe { &mut *self.ptr };
return mutable_ref.get_back();
}
#[inline(always)]
fn publish(&mut self) {
let mutable_ref = unsafe { &mut *self.ptr };
mutable_ref.publish_back();
}
}
// consumer
pub(crate) struct TripleBufferConsumer<T>
where
T: Send + Clone + Default,
{
pub(super) buffer: Arc<TripleBuffer<T>>,
pub(super) ptr: *mut TripleBuffer<T>,
}
impl<T> TripleBufferConsumer<T>
where
T: Send + Clone + Default,
{
pub fn new(buffer: Arc<TripleBuffer<T>>) -> Self {
let ptr = Arc::as_ptr(&buffer) as *mut TripleBuffer<T>;
Self {
buffer,
ptr,
}
}
#[inline(always)]
fn get_new_state(&mut self) -> Option<&mut T> {
let mutable_ref = unsafe { &mut *self.ptr };
return mutable_ref.get_new_front();
}
#[inline(always)]
fn get_state(&mut self) -> &mut T {
let mutable_ref = unsafe { &mut *self.ptr };
return mutable_ref.get_front();
}
}
}
1 post - 1 participant
🏷️ Rust_feed