0

In my browser application, two closures access data stored in a Rc<RefCell<T>>. One closure mutably borrows the data, while the other immutably borrows it. The two closures are invoked independently of one another, and this will occasionally result in a BorrowError or BorrowMutError.

Here is my attempt at an MWE, though it uses a future to artificially inflate the likelihood of the error occurring:

use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    pub fn log(s: &str);
    #[wasm_bindgen(js_name = setTimeout)]
    fn set_timeout(closure: &Closure<dyn FnMut()>, millis: u32) -> i32;
    #[wasm_bindgen(js_name = setInterval)]
    fn set_interval(closure: &Closure<dyn FnMut()>, millis: u32) -> i32;
}

pub struct Counter(u32);

#[wasm_bindgen(start)]
pub async fn main() -> Result<(), JsValue> {
    console_error_panic_hook::set_once();

    let counter = Rc::new(RefCell::new(Counter(0)));

    let counter_clone = counter.clone();
    let log_closure = Closure::wrap(Box::new(move || {
        let c = counter_clone.borrow();
        log(&c.0.to_string());
    }) as Box<dyn FnMut()>);
    set_interval(&log_closure, 1000);
    log_closure.forget();

    let counter_clone = counter.clone();
    let increment_closure = Closure::wrap(Box::new(move || {
        let counter_clone = counter_clone.clone();
        wasm_bindgen_futures::spawn_local(async move {
            let mut c = counter_clone.borrow_mut();
            // In reality this future would be replaced by some other
            // time-consuming operation manipulating the borrowed data
            SleepFuture::new(5000).await;
            c.0 += 1;
        });
    }) as Box<dyn FnMut()>);
    set_timeout(&increment_closure, 3000);
    increment_closure.forget();

    Ok(())
}

struct SleepSharedState {
    waker: Option<Waker>,
    completed: bool,
    closure: Option<Closure<dyn FnMut()>>,
}

struct SleepFuture {
    shared_state: Rc<RefCell<SleepSharedState>>,
}

impl Future for SleepFuture {
    type Output = ();
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut shared_state = self.shared_state.borrow_mut();
        if shared_state.completed {
            Poll::Ready(())
        } else {
            shared_state.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

impl SleepFuture {
    fn new(duration: u32) -> Self {
        let shared_state = Rc::new(RefCell::new(SleepSharedState {
            waker: None,
            completed: false,
            closure: None,
        }));

        let state_clone = shared_state.clone();
        let closure = Closure::wrap(Box::new(move || {
            let mut state = state_clone.borrow_mut();
            state.completed = true;
            if let Some(waker) = state.waker.take() {
                waker.wake();
            }
        }) as Box<dyn FnMut()>);

        set_timeout(&closure, duration);

        shared_state.borrow_mut().closure = Some(closure);

        SleepFuture { shared_state }
    }
}

panicked at 'already mutably borrowed: BorrowError'

The error makes sense, but how should I go about resolving it?

My current solution is to have the closures use try_borrow or try_borrow_mut, and if unsuccessful, use setTimeout for an arbitrary amount of time before attempting to borrow again.

1 Answers1

0

Think about this problem independently of Rust's borrow semantics. You have a long-running operation that's updating some shared state.

  • How would you do it if you were using threads? You would put the shared state behind a lock. RefCell is like a lock except that you can't block on unlocking it — but you can emulate blocking by using some kind of message-passing to wake up the reader.

  • How would you do it if you were using pure JavaScript? You don't automatically have anything like RefCell, so either:

    • The state can be safely read while the operation is still ongoing (in a concurrency-not-parallelism sense): in this case, emulate that by not holding a single RefMut (result of borrow_mut()) alive across an await boundary.
    • The state is not safe to be read: you'd either write something lock-like as described above, or perhaps arrange so that it's only written once when the operation is done, and until then, the long-running operation has its own private state not shared with the rest of the application (so there can be no BorrowError conflicts).

Think about what your application actually needs and pick a suitable solution. Implementing any of these solutions will most likely involve having additional interior-mutable objects used for communication.

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
  • Great answer, thank you. My application no longer holds a `RefMut` during `await`s. Regarding blocking emulation, do you know of any resources I can reference? I have been unsuccessful implementing this in Rust so far. –  Apr 04 '21 at 17:47
  • @user24182 It's fairly simple conceptually but has some fiddly details; you need a list of callbacks of some sort (operations "blocked" on the thing) and when the resource is "unlocked", you call one or all of them to let them know the "lock" is available again. It's not essentially different from any sort of "notifying on state change" mechanism. – Kevin Reid Apr 04 '21 at 19:24