5

js_sys exposes JavaScript Promise via a function pub fn new(cb: &mut dyn FnMut(Function, Function)) -> Promise;. Per my reading of the MDN documentation, there's nothing suggesting that the executor function will be called more than once, yet js_sys exposes it as an FnMut instead of an FnOnce.

Because of this, I can't wrap my head around a simple task, such as sending a message to myself in the future via a channel. Ideally, the sender half of the channel would be moved into the closure and Promise::new would accept an FnOnce.

async func send_to_future_self() {
  use std::sync::mpsc::{Sender, Receiver, channel};
  let (sender, receiver) = channel().split();

  // Schedule sending a message in the future

  // Option A: Move `sender` --> The closure becomes `FnOnce` because it consumes `sender`
  // Option B: Reference `sender` --> Then the borrow outlives `sender`'s lifetime

  let mut sleep_100 = move?? |accept: Function, _reject: Function| {
    // This Rust closure is exported to Javascript...
    let callback = Closure::__??__(move?? || {
      // Send a message via the channel
      sender.send(()).unwrap();
      let result = js_sys::Array::new();
      // Doesn't matter what value is passed here, it's just for synchronization
      result.push(&JsValue::from(true)); 
      accept.apply(&JsValue::undefined(), &result).unwrap();
    });

    web_sys::window().unwrap()
      .set_timeout_with_callback_and_timeout_and_arguments_0(
         callback.as_ref().unchecked_ref(), 
        100
      ).unwrap();
    // ... so intentionally forget it, otherwise it will be dropped at the end of this block 
    // despite JS still referring to it.
    callback.forget();
  };

  // Convert JS Promise into a Rust future
  // This wants an FnMut(Function, Function)
  let future: JsFuture = js_sys::Promise::new(&mut sleep_100).into();
  let _ = future.await;
  receiver.try_recv().unwrap();
}
Huckle
  • 1,810
  • 3
  • 26
  • 40

1 Answers1

4

I suspect that it gets wrapped as a JavaScript function object internally, which can be called multiple times (even though it won't be).

The simplest way you can get around this is by having your function be callable multiple times. An easy way to do this is to wrap consumable values in Option and use Option::take() to get the value -- if you get back None then the function was called multiple times.

Here is some sample code illustrating this technique:

struct Consumable;

impl Consumable {
    fn consume(self) {}
}

fn requires_fnmut(_: impl FnMut()) {}

fn main() {
    let mut x = Some(Consumable);
    
    requires_fnmut(move || {
        let x = match x.take() {
            Some(v) => v,
            
            // If None we were called twice, so just bail.
            None => return,
        };
        
        x.consume();
    });
}

Here, Consumable is similar to Sender in your case: it exposes a method that consumes the receiver. Calling this on a captured variable in a closure would make the closure implement FnOnce and not FnMut, as you have found yourself.

However, notice in main() that we wrap the Consumable in an Option and mark it mut:

let mut x = Some(Consumable);

Now we can "unwrap" this using match x.take() in the closure, returning from the closure if take() returns None, which would indicate that the function was called a second time.

After taking the value from the Option you are free to consume it.


It looks like in your question you have two nested closures, both of which need to be FnMut, and the inner one is what needs access to the Sender. You can still use this approach, but you have to do the same thing in both closures to transfer the consumable value from the outer closure to the inner closure. Here is an example:

fn main() {
    let mut x = Some(Consumable);
    //      ^ The outer closure takes ownership of this x.
    
    requires_fnmut(move || {
        let mut x = x.take();
        //      ^ The inner closure takes ownership of this x.
        
        if x.is_none() {
            // We were already called once.
            return;
        }
        
        requires_fnmut(move || {
            let x = match x.take() {
                Some(v) => v,
                
                // We were already called once.
                None => return,
            };
            
            x.consume();
        });
    });
}
cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • In your example, the lifetime of the mutable borrow of `x` can't outlive `x`, but with `js_sys::Promise::new`, the closure is going to be run asynchronously and so the borrow of `x` would outlive `x`. – Huckle Aug 08 '23 at 02:42
  • @Huckle There is no borrowing if you move into the closure. In nested closures you can repeat the pattern -- `take()` into the outer closure and move the result into the inner closure. Or with nested `move` the compiler will do it for you. – cdhowie Aug 08 '23 at 03:14
  • (Nested `move` may not work as that will likely make the outer closure `FnOnce`. So `take()` at each level is probably what you want.) – cdhowie Aug 08 '23 at 03:19
  • @Huckle I've updated my answer with an example of using this technique to transfer a consumable value through two nested closures. – cdhowie Aug 08 '23 at 04:10
  • I'm looking closer at the function signatures and `Window::set_timeout_*` only require a JavaScript `Function`, rather than a Rust `Fn*`, so that actually simplifies things a bit. The `Closure` struct is a utility to help move between the two and it has an `FnOnce` and an `FnMut` version. – Huckle Aug 08 '23 at 04:11
  • @Huckle Ah, nice. You can probably make it work using the `take()` trick only in the outer closure then. – cdhowie Aug 08 '23 at 04:14
  • [Rust Playground of working example w/ stubbed signatures](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cad807629d15a905f6cfdd7bbe875162). – Huckle Aug 08 '23 at 05:22
  • @Huckle Nice. Note that your `if is_none()` followed by `take()` could be simplified to `take().expect("called twice")`. – cdhowie Aug 08 '23 at 06:14
  • Instead of wrapping the captured values in `Option` you could wrap the whole `FnMut` and keep the inner code the same, as in this [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b36043aea5cc31c000ed9b0049c01c61). – rodrigo Aug 08 '23 at 13:17
  • @rodrigo I had that thought as well, not a bad approach. – cdhowie Aug 08 '23 at 21:32