Fighting with 'lifetime'

Hi,

I have the following code where I'm trying to express a relationship between 'dsp' and 'UI'. A specific FUI trait ('derived' from the UI one) keeps the &'a mut f32 type in a Vec. UI (and derived FUI) cannot live more that 'dsp' since FUI keeps references on part of 'dsp' state. This fails with lifetime issues I could not solve. Any idea ? Thanks.

#![allow(unused_parens)]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]

pub trait UI<'a> {
    fn addZone(&mut self, label: &str, zone: &'a mut f32) -> ();
}

pub struct FUI<'a>
{
    fButtons: Vec<&'a mut f32>
}

impl<'a> UI<'a> for FUI<'a> {

    fn addZone(&mut self, label: &str, zone: &'a mut f32) -> ()
    {
        self.fButtons.push(zone); 
    }

}

pub struct dsp {
	fHslider0: f32,
}

impl dsp {
		
	pub fn new() -> dsp {
		dsp {
			fHslider0: 0.0,
            }
	}

    pub fn buildUserInterface(&mut self, ui_interface: &mut UI)
    {
		ui_interface.addZone("foo", &mut self.fHslider0);   <== lifetime issue here
    }
}

fn main() {

    let mut dsp = Box::new(dsp::new());

    let mut fui = FUI {
        fButtons: Vec::new()
    };

    dsp.buildUserInterface(&mut fui);
}
2 Likes

Essentially what's happening is that you need to explicitly declare lifetimes used by a function.

pub fn buildUserInterface<'a>(&'a mut self, ui_interface: &mut UI<'a>) {
    ui_interface.addZone("foo", &mut self.fHslider0);
}

UI trait stores elements with 'a lifetime (the same as self), which needs to be declared in a function.

1 Like

I presume that's part of a more complex problem, since here you could copy the f32 instead of borrowing it.

So in general, trying to store &mut in a struct will lead to misery. Don't try to use borrows like you'd use pointers in C. Treat &mut as a strictly temporary, scope-limited permission to write to an object.

For passing things around:

  • Prefer owned/cloned/copied values when creating objects. Give an object instead of lending it.
  • If it has to be shared and writeable, use Rc<Mutex> or RefCell.
1 Like

Yes, this is indeed part of a more complex problem: basically "sharing" a f32 memory zone between 'UI' (and by extension FUI and other UI..) and 'dsp'. UIs are going to write the f32 zone and 'dsp' is going to read the f32 memory zone it from another (real-time) thread.

We also need to avoid mutex like access since this is going to be used in the real-time audio context. In C++ we were simply writing the zone in 'UIs' and reading in 'dsp', and since f32 read/write is atomic on the machine were are using, it was Ok.

So I see that rewriting the same pattern in Rust is more challenging: so to summarize "sharing" a f32 memory zone between two (or even more writer threads...), and a single reader 'dsp' real-time thread. 'UIs' lifetime typical has to match 'dsp' lifetime.

So is RefCell the correct model?

Rust is trying to prevent you from having unsynchronized shared mutable data. The whole type system is built to prevent accidentally doing what you're trying to do, so you'll have to explain to Rust that you know what you're doing :slight_smile:

RefCell still checks for race conditions, so it's not the right solution here.

In Rust there's Cell and UnsafeCell to "cheat" the type system to allow unrestricted read/write access.

There's also a wrapper for atomics: AtomicUsize in std::sync::atomic - Rust

1 Like

Thanks for the links. But why no atomic for f32 or f64 types ?

1 Like

Because hardware in practice (even x86) doesn't support it, so it wouldn't be all that useful. There are some atomic memory operations on x86, however they are for integers. C language does provide _Atomic double type, but actually doing operations like += 1 on x86 CPU involves a spinlock when multiple threads are trying to add a value.

There however are external crates providing spinlock implementation if that's what you want.

1 Like

You could convert the f32/64 to their raw bits (u32/64), and then use the integer atomics. The atomics would just be "raw storage" and you'd convert from/to the float as needed.

2 Likes

You have to be careful with this - compilers may do "funny" stuff when performing plain loads/stores. In C++, you'd probably want atomic loads/stores with memory_order_relaxed, rather than naked load/store. Similarly in Rust.

4 Likes

I'm pretty sure UnsafeCell is !Sync, so doesn't allow thread sharing.

2 Likes

Yes, you have to explicitly implement Sync, as a statement to the compiler that what you're doing is thread-safe. And if you have a data race in it, LLVM's optimizer will then feel free to break your code, as that is Undefined Behaviour.

1 Like