This closure implements `FnOnce`, not `FnMut` in Wasm context

Hello!

I'm trying to implement a web socket client in wasm. I use this example. I'm adding a channel to use received messages in other application module and have an error when use the Sender in a Closure. There is minimal reproduction code:

Cargo.toml

[package]
name = "demo"
version = "0.1.0"
edition = "2021"

[dependencies]
js-sys = "0.3.77"
wasm-bindgen = "0.2.100"
crossbeam = "0.8.4"

[dependencies.web-sys]
version = "0.3.77"
features = [
  "BinaryType",
  "Blob",
  "ErrorEvent",
  "FileReader",
  "MessageEvent",
  "ProgressEvent",
  "WebSocket",
]

src/main.rs

use crossbeam::channel::{unbounded, Receiver, Sender};
use wasm_bindgen::prelude::*;
use web_sys::MessageEvent;

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}


#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

fn main() {
    let (sender, _receiver): (
        Sender<String>,
        Receiver<String>,
    ) = unbounded();

    let _onmessage_callback = Closure::<dyn FnMut(_)>::new(move |e: MessageEvent| {
        if let Ok(blob) = e.data().dyn_into::<web_sys::Blob>() {
            console_log!("message event, received blob: {:?}", blob);
            // [...]
            let onloadend_cb = Closure::<dyn FnMut(_)>::new(move |_e: web_sys::ProgressEvent| {
                // [...]
                sender.send("ReceivedMessage".to_string()).unwrap();
            });
            // [...]
        } else {
            console_log!("message event, received Unknown: {:?}", e.data());
        }
    });
}

This produce the error (when add the line sender.send("ReceivedMessage".to_string()).unwrap();):

error[E0525]: expected a closure that implements the `FnMut` trait, but this closure only implements `FnOnce`
   --> src/main.rs:22:60
    |
22  |       let _onmessage_callback = Closure::<dyn FnMut(_)>::new(move |e: MessageEvent| {
    |                                 ---------------------------- -^^^^^^^^^^^^^^^^^^^^^
    |                                 |                            |
    |  _______________________________|____________________________this closure implements `FnOnce`, not `FnMut`
    | |                               |
    | |                               required by a bound introduced by this call
23  | |         if let Ok(blob) = e.data().dyn_into::<web_sys::Blob>() {
24  | |             console_log!("message event, received blob: {:?}", blob);
25  | |             // [...]
...   |
28  | |                 sender.send("ReceivedMessage".to_string()).unwrap();
    | |                 ------ closure is `FnOnce` because it moves the variable `sender` out of its environment
...   |
33  | |         }
34  | |     });
    | |_____- the requirement to implement `FnMut` derives from here
    |
    = note: required for `{closure@src/main.rs:22:60: 22:82}` to implement `IntoWasmClosure<dyn FnMut(MessageEvent)>`
note: required by a bound in `wasm_bindgen::prelude::Closure::<T>::new`
   --> /home/bastiensevajol/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wasm-bindgen-0.2.100/src/closure.rs:274:12
    |
272 |     pub fn new<F>(t: F) -> Closure<T>
    |            --- required by a bound in this associated function
273 |     where
274 |         F: IntoWasmClosure<T> + 'static,
    |            ^^^^^^^^^^^^^^^^^^ required by this bound in `Closure::<T>::new`

I'm not familiar with FnOnce and FnMut and I have no idea about what happen. Any idea ? Thanks !

Oh, I just noticed I have to clone the sender just before onloadend_cb closure!

1 Like

Seems you found a solution in the meantime, but I already wrote all of this explaining what happens and how to address it. So here it is:

move closures capture their environment with move semantics. This means that ownership of all captured variables is moved into the closure, rather than borrowing. Closures furthermore impl one of the Fn* traits based on how its captures are used. In this case, the sender is moved first into move |e: MessageEvent| {} and then moved again into move |_e: web_sys::ProgressEvent| {} (I hope you will forgive the closure references; they are unnamed, and I can only refer to them by the arguments they take).

When move |_e: web_sys::ProgressEvent| {} returns, it drops ownership of its captures. Because sender can only be dropped once, these closures therefore only impl FnOnce.

You can usually get around this issue by moving a loan into the closure(s). E.g.:

let _onmessage_callback = Closure::<dyn FnMut(_)>::new({
    // This scope and shadowed binding will ensure that
    // only the loan is moved into the closure.
    let sender = &sender;

    move |e: MessageEvent| {
        // `sender` is `&Sender` here, it can be moved (by `Copy`)
        // and dropped many times.
    }
});

If the sender needs to outlive the caller's stack frame (which is certainly the case for wasm) then moving ownership of a clone is the right solution.

3 Likes

Okay, thanks a lot for this explanation !