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