如何使用 wasm-bindgen 在闭包之间惯用地共享数据?

How to idiomatically share data between closures with wasm-bindgen?

在我的浏览器应用程序中,两个闭包访问存储在 Rc<RefCell<T>> 中的数据。一个闭包可变地借用数据,而另一个闭包不变地借用它。这两个闭包是相互独立调用的,这偶尔会导致 BorrowErrorBorrowMutError.

这是我对 MWE 的尝试,尽管它使用未来来人为地增加错误发生的可能性:

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'

错误是有道理的,但我应该如何解决它?

我目前的解决方案是让闭包使用 try_borrowtry_borrow_mut,如果不成功,在尝试再次借用之前使用 setTimeout 任意时间。

独立于 Rust 的借用语义来思考这个问题。您有一个长 运行 操作正在更新一些共享状态。

  • 如果你使用线程,你会怎么做?您会将共享状态放在锁后面。 RefCell 就像一把锁,只是你不能在解锁时阻塞——但你可以通过使用某种消息传递来模拟阻塞来唤醒 reader。

  • 如果你使用纯粹的JavaScript,你会怎么做?你不会自动拥有 RefCell 之类的东西,所以 要么:

    • 可以在操作仍在进行时安全地读取状态(在并发而非并行的意义上):在这种情况下,通过不持有单个 RefMut([=12 的结果)来模拟=]) 跨越 await 边界活着。
    • 读取状态安全:你要么像上面描述的那样写一些类似锁的东西,要么安排它只在操作时写入一次完成,直到那时,long-运行 操作有自己的私有状态 not 与应用程序的其余部分共享(因此不会有 BorrowError 冲突).

考虑您的应用程序实际需要什么,然后选择合适的解决方案。实施这些解决方案中的任何一个很可能涉及使用额外的内部可变对象进行通信。