And I guess that any non-async function therein makes the content opaque to the algorithm that slices the code into a state machine? If that's true, that was the missing part for understanding the problem.
Yes, âthe contentâ of the non-async function is opaque to the state machine construction. If it werenât, then Rust would have very different properties â for example, std::thread::scope()
and std::thread::LocalKey::with()
couldnât exist. There are lots of things that depend on the fact that they cannot be interrupted and saved into a state machine, but must run to completion (or unwind or abort) on the same thread.
You also wouldnât be able to call any functions not written in Rust except with some special mechanism, since those functions are necessarily opaque.
Thank you all for your time and patience. I now have a better understanding of why this is impossible.
The algorithm that converts an async fn
into a state machine never inserts additional yields. It only supports those where a .await
appear, which means the async fn
is evaluating another Future
which might in turn yield. If that happens the async fn
yields as well. So if your async
function calls a sync function the algorithm will never introduce a yield because there's no .await
there.
Even if this was changed and the compiler tried to see through sync functions (which is likely not gonna happen, since there's unsafe
code relying on this not happening for soundness), there would still be some functions where thi would not be possible. For example there's no async equivalent of function pointers or trait objects that's would be always valid. Likewise, blocking OS syscalls need to be manually translated into the corresponding async syscalls, and that's not something the compiler can do (not to mention it requires a lot of machinery).
I have no experience with WASM, but my understanding is that itâs a pretty full-featured single-threaded environment as far as compute is concerned (though I/O probably has quirks).
As such, I would expect something like thisš to be possible, though extremely inefficientâ Instead of jumping up the call stack to the executor, run another copy of it within the callback and ensure that the two concurrent iterations donât step on each otherâs toes too badly.
I wouldnât be surprised at all, though, if browser I/O events are unable to trigger a waker in this setup until control returns to the top level naturally. But thatâs not necessarily the only source of wakes in the system; thereâs also cross-task communication at least.
š Content warning: Extremely quick-and dirty, untested proof of concept. Probably contains deadlocks and other concurrency bugs.
WASM fundamentally requires you to return out of WASM to yield.
Such a method could perhaps spin a custom executor's loop (obviously not the JS event loop). But you'd be paying stack frames for that, eventually leading to stack overflows if futures within the executors then in turn also call that kind of yield. So it'd be a pretty bad idea to use such a method even if it existed.
also it wouldn't solve my problem of giving the JavaScript runtime some time to execute other tasks as the surrounding WASM-call would still be blocking
Iâm still trying to wrap my head around the what the restriction is. When you say that yielding requires returning out of WASM, do you mean that the lowering of async
blocks to state machines is somehow fundamentally different on the WASM platform than native?
Or that the mechanism used to deliver I/O events relies on an empty Rust stack? Or something else?
Or that the mechanism used to deliver I/O events relies on an empty Rust stack?
This. There is a built-in event loop (in browsers, at least) which executes only when the JS stack and the Rust stack are empty. Actual input from the outside world (mostly) cannot arrive except by events delivered by that event loop.
When using WASM directly in the browser, your async runtime is the JavaScript event loop. You have to yield all the way back into JS. There's no way to recursively invoke the JS event loop from within WASM. The code you shared may be able to run other Rust tasks, but the browser event loop has a bunch of other non-WASM work such as painting the screen or responding to mouse clicks or triggering pure JS timers. Those things can only run when you yield out of WASM.
Just completeness: I was able to work around this problem using wasm-rs-shared-channel
.
The WebWorker still needs to cooperatively poll the channel, but at least the messages are getting through.