从 `Fn` 调用 `FnOnce`

Call `FnOnce` from `Fn`

我正在使用两个不同的库(特别是 napi-rs and callback-future)并想在另一个库的 Fn 函数中调用一个库的 FnOnce 函数。具体来说,我试图将 Rust 函数公开给 JavaScript,它在调用时完成 Future

由于暴露的JS函数在技术上可以随时捕获和调用,Rust没有办法保证该函数只会被调用一次,所以它不得不假设该函数会被调用多次.然而,callback-future 要求 Future 只完成一次(通过调用 FnOnce)。我无法修改这两个签名,而且它们对于各自的用例都是准确的。我怎样才能让两者一起工作,以便我可以在 Fn 回调中解决 Future

我知道多次调用 FnOnce 是不行的,我可以使用 unsafe 代码或以其他方式在运行时强制该函数只被调用一次。在调用 FnOnce 之前可以检测并拒绝后续的调用尝试,但我不确定如何与 Rust 编译器沟通我正在执行此操作并且可以允许调用 FnOnce .目前我拥有的是:

// 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

这给了我错误:

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`.

虽然错误是抱怨在闭包之间移动,但我的理解是这是不允许的,因为 completeFnOnce 并且我试图从 Fn.如果有另一种方法可以解决关闭问题,那么我想这也是一个可行的解决方案。

此外,如果 N-API 中有一种方法可以接受 Promise 结果并 await 它而不是通过回调,那也是一个很好的选择。我相信你可以在 Rust 中 await a Promise,但是 AFAICT 没有办法从线程安全 N-API 函数接收同步结果,因为 napi-rs 似乎 ignore the return value.

到目前为止,我找到的唯一解决方案是分叉 callback-future API 以将 completeFnOnce 更改为 Fn ,这显然不是一个很好的解决方案。我还尝试了从 FnOnceFnstd::mem::transmute(),认为只要我只调用函数一次,就可以强制强制转换。但是,在调用时这样做会出现段错误,所以我认为它不会按照我希望的方式工作。非常感谢这里的任何想法!

由于您没有我们可以自己编译的示例并且缺少一些细节,我将解决您问题的核心:如何从 Fn 调用 FnOnce

您已经知道的第一个问题:如果您尝试直接调用 FnOnce,这是不允许的,因为它会消耗值,这会使调用闭包本身 FnOnce,但您需要一个 Fn.

第二个是,如果您尝试将 Option 之类的东西与其 take() 方法一起使用,您会发现 Fn 无法改变其捕获状态(它会必须 FnMut 才能做到这一点)。

解决方案是将 Option 包装在提供内部可变性的类型中。根据您是否需要 Fn 也是 Send + Sync,您可以使用 CellMutex

使用 Cell,即 而不是 Send + Sync,它看起来像这样:

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"),
        }
    });
}

当传递给 call_twice() 的闭包在 Cell 上调用 take() 时,提取内部值并替换为 None。如果再次调用该函数,内部值将是之前放在那里的 None。这也意味着您可以检测到这种情况,并可能将问题反馈给 JavaScript 方。

如果闭包需要 Send + Sync 那么你可以使用 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"),
        }
    });
}

您需要做的就是将此技术应用到您的具体情况。