javascriptrustclosuresfuturen-api

Call `FnOnce` from `Fn`


I'm working with two different libraries (specifically napi-rs and callback-future) and want to invoke a FnOnce function from one library in a Fn function from another. Specifically, I'm trying to expose a Rust function to JavaScript which completes a Future when invoked.

Since the exposed JS function can technically be captured and invoked at any time, there is no way for Rust to guarantee that the function will only be called once, so it has to assume that the function will be called many times. However, callback-future requires that a Future is only completed once (via invoking an FnOnce). I can't modify these two signatures, and they are both accurate for their respective use cases. How can I get the two to work together so I can resolve a Future in a Fn callback?

I understand that it's not ok to invoke the FnOnce multiple times, and I'm ok with using unsafe code or otherwise enforcing at runtime that the function is only called once. Subsequent invocation attempts can be detected and rejected before calling the FnOnce, but I'm not sure how to communicate to the Rust compiler that I'm doing this and that it's ok to allow the call to FnOnce. Currently what I have is:

// Create a `CallbackFuture` to wait for JS to respond.
// `complete` is an `FnOnce` to mark the `Future` as "Ready".
CallbackFuture::<Result<String, Error>>::new(move |complete| {
  thread_safe_js_function.call(Ok(Args {
    // Other arguments...

    // Callback for JS to invoke when done.
    // `Fn` because JS could theoretically call this multiple times,
    // though that shouldn't be allowed anyways.
    callback: Box::new(move |ctx| {
      let result = ctx.get::<JsString>(0)?.into_utf8()?.as_str()?.to_owned();

      // Complete the `Future` with the result.
      complete(Ok(result));

      ctx.env.get_undefined() // Return `undefined` to JS.
    }),
  }), ThreadsafeFunctionCallMode::Blocking);
}).await

This gives me the error:

error[E0507]: cannot move out of `complete`, a captured variable in an `Fn` closure
   --> node/src/lib.rs:368:15
    |
352 |           CallbackFuture::<Result<PathBuf, Error<BundleErrorKind>>>::new(move |complete| {
    |                                                                                -------- captured outer variable
...
358 |               callback: Box::new(move |ctx| {
    |  ________________________________-
...   |
368 | |               complete(Ok(result));
    | |               ^^^^^^^^ move occurs because `complete` has type `std::boxed::Box<dyn FnOnce(Result<PathBuf, Error>) + Send>`, which does not implement the `Copy` trait
369 | |     
370 | |               ctx.env.get_undefined()
371 | |             }),
    | |_____________- captured by this `Fn` closure

For more information about this error, try `rustc --explain E0507`.

While the error is complaining about moving between closures, my understanding is that this isn't allowed because complete is FnOnce and I'm trying to call it from an Fn. If there's another approach which solves the closure issue, then I guess that could be a viable solution too.

Also if there's a way in N-API to accept a Promise result and await it instead of going through a callback, that could be a great alternative as well. I believe you can await a Promise in Rust, but AFAICT there's no way to receive a synchronous result from a threadsafe N-API function as napi-rs seems to ignore the return value.

So far, the only solution I've found is to fork the callback-future API to change complete from an FnOnce to an Fn, which obviously isn't a great solution. I also tried std::mem::transmute() from FnOnce to Fn thinking that forcing a cast that way would work as long as I only called the function once. However doing so segfaulted when invoked, so I don't think it works the way I want it to here. Any ideas here are greatly appreciated!


Solution

  • Since you don't have an example we can compile ourselves and there is some missing detail, I will address the heart of your question: how do you call an FnOnce from an Fn?

    The first problem you already know: if you try to call the FnOnce directly, this is disallowed because it consumes the value, which would make the calling closure itself FnOnce, but you need an Fn.

    The second is that if you try to use something like Option with its take() method, you'll find that Fn can't mutate its captured state (it would have to be FnMut to do that).

    The solution is to wrap an Option in a type providing interior mutability. Depending on whether you need your Fn to also be Send + Sync, you could use either Cell or Mutex.

    With Cell, which is not Send + Sync, it would look something like this:

    use std::cell::Cell;
    
    fn print_owned(x: String) {
        println!("print_owned called with {:?}", x);
    }
    
    fn call_twice(f: impl Fn()) {
        f();
        f();
    }
    
    fn main() {
        let foo = "foo".to_string();
        let fnonce = move || print_owned(foo);
        
        let maybe_fnonce = Cell::new(Some(fnonce));
        
        call_twice(move || {
            match maybe_fnonce.take() {
                Some(inner) => inner(),
                None => println!("maybe_fnonce is None"),
            }
        });
    }
    

    When the closure passed to call_twice() invokes take() on the Cell, the inner value is extracted and replaced with None. If the function is called again, the inner value will be the None previously put there. This also means you can detect this situation and possibly signal the problem back to the JavaScript side.

    If the closure needs to be Send + Sync then you can instead use Mutex<Option<_>>:

    use std::sync::Mutex;
    
    fn print_owned(x: String) {
        println!("print_owned called with {:?}", x);
    }
    
    fn call_twice(f: impl Fn()) {
        f();
        f();
    }
    
    fn main() {
        let foo = "foo".to_string();
        let fnonce = move || print_owned(foo);
        
        let maybe_fnonce = Mutex::new(Some(fnonce));
        
        call_twice(move || {
            match maybe_fnonce.lock().unwrap().take() {
                Some(inner) => inner(),
                None => println!("maybe_fnonce is None"),
            }
        });
    }
    

    All you need to do is apply this technique to your specific situation.