I'm having trouble finding an equivalent to this type of code, which makes me suspect it isn't idiomatic for Rust, but it's unclear what the canonical approach would be since I can't find instances of the problem discussed.
Consider this struct:
struct Looper {
shared_value: Arc<AtomicU64>,
handle: Option<JoinHandle<()>>
}
impl Looper {
pub fn new(task: Box<dyn Fn() + Send>) -> Self {
let mut looper = Self { shared_value: Arc::new(AtomicU64::new(10)), handle: None };
let shared_state = looper.shared_value.clone();
looper.handle = Some(thread::spawn(move || {
for _ in 0..1000 {
sleep(Duration::from_millis(shared_state.load(Relaxed)));
task();
}
}));
looper
}
pub fn set_value(&self, value: u64) {
self.shared_value.store(value, Relaxed)
}
}
impl Drop for Looper {
fn drop(&mut self) {
self.handle.take().unwrap().join().unwrap();
}
}
And consider a very contrived problem, where we want a wrapper object that can reuse the struct but it manages changes to the sleep period internally to the wrapper.
In C++, this is straightforward by capturing this in your lambda:
class Wrapper {
private:
std::unique_ptr<Looper> looper;
public:
Wrapper(Duration pollingRate, const std::function<uint64_t()>& task) {
looper = std::make_unique<Looper>( [this, task]() { looper->set_value(task()); });
}
}
In Rust, there are two issues. First, there is no constructor equivalent to capture this in. That type of issue is solvable by using options. I could create a None element first, then try to create a closure and capture the object currently set to None e.g.
struct WrappedLooper {
looper: Option<Looper>
}
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let mut wrapped = WrappedLooper { looper: None };
let this = &wrapped;
wrapped.looper = Some(Looper::new(Box::new(move || {
this.looper.unwrap().set_value(task());
})));
wrapped
}
}
This obviously doesn't work as you can't move out of this. I could switch the internal state to an Arc and Mutex it, but this seems extremely heavy handed and incorrect to me:
struct WrappedLooper {
looper: Arc<Mutex<Option<Looper>>>
}
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let mut wrapped = WrappedLooper { looper: Arc::new(Mutex::new(None)) };
let copy = wrapped.looper.clone();
wrapped.looper.lock().unwrap().replace(Looper::new(Box::new(move || {
copy.lock().unwrap().as_mut().as_ref().unwrap().set_value(task());
})));
wrapped
}
}
So what I am wondering is what is the right way to have this kind of self-referential initialization (the option, or something else) in scenarios where the access is always safe? (Here it is safe because the object outlives the closure + the shared mutable state is atomic)
From my understanding, there are two separate issues. One has to do with the lifetime of the spawned thread; the other is self-reference.
Let's disregard that first issue by ignoring the thread entirely. First, let's try only requiring that the closure live as long as
Looper::new:Then, everything else compiles just fine. But this isn't very useful: if we aren't guaranteed that the function lives past
Looper::new, we obviously can't use it in the thread. Instead, we could tell Rust thattaskmust live as long as the createdLooper:Then, the immediate issue is that we want
WrappedLooperto have an instance ofLooperthat depends on a reference to itself. We can't freely letLooperdo that: what happens to that self-reference ifLooperis moved? So, making this work would require some unsafe Rust usingPin, or at least a library that encapsulates that away.However, even if this were implemented, we hit the other problem: there's no way to really know how long the thread lives. Let's add the thread spawning back in:
(Playground link)
Telling Rust that our function lives as long as
Looperis not enough: it has to be'static. This makes sense: consider what happens ifWrappedLooperis dropped before the delay is over. Then,taskcould no longer hold a reference to the looper. The C++ snippet has exactly this problem: if theWrapperis destroyed, then thethiscaptured in thetaskfor theLooperis now dangling.Because Rust does not provide a way to guarantee the looper instance indeed outlives the thread, it's probably best to just use reference counting here.
Sidenote:
This first issue is also present in the C++ version, just hidden. In C++,
unique_ptrcan be null (and starts null). So, it acts just like anOption, wherelooper->set_valuein C++ is essentially equivalent tolooper.unwrap_unchecked().set_valuein Rust. The difference is that Rust forces us to explicitly recognize it.