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 anFn
?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 itselfFnOnce
, but you need anFn
.The second is that if you try to use something like
Option
with itstake()
method, you'll find thatFn
can't mutate its captured state (it would have to beFnMut
to do that).The solution is to wrap an
Option
in a type providing interior mutability. Depending on whether you need yourFn
to also beSend + Sync
, you could use eitherCell
orMutex
.With
Cell
, which is notSend + Sync
, it would look something like this:When the closure passed to
call_twice()
invokestake()
on theCell
, the inner value is extracted and replaced withNone
. If the function is called again, the inner value will be theNone
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 useMutex<Option<_>>
:All you need to do is apply this technique to your specific situation.