Dealing with blocking input in WASM


I'm trying to port the little language interpreter I've been working on to run within a browser. So far I've got most of it working: I can cross-compile the code to WASM, I've added an xterm.js widget to a webpage, and I'm handling onKey events on the xterm.js widget to buffer enough data to feed a line of input to the interpreter via its REPL. All of this logic is in Rust, not JS.

But things get tricky when the interpreter itself wants to stop and ask for further user input: e.g. "read a line from stdin". In my mind, this "stop and wait for input" means blocking... but it seems that Rust's WASM doesn't like that. I'm blocking with a Condvar to synchronize with the fetching of a line from the onKey events... and it complains with can't block with web assembly.

I've been trying a few things and it seems that there is experimental support for a Condvar based on WASM atomics... but that seems to require the nightly toolchain, rebuilding the world with an atomics feature, seemingly enabling shared memory in Firefox... and I haven't gotten it to work yet.

So. I cannot believe this has to be that complicated, and therefore I'm assuming my mental model on this whole thing is wrong given that I have done little JS before.

How would you go about this? In particular, how would you implement the line entering part based on asynchronous events from JS in a way that can be consumed whenever the program needs a line, blocking until such line is ready?

Maybe the answer is... forget about WASM to workaround the threading/blocking limitations, and implement this part of the logic on the JS side, using await in the Rust code to fetch the line from a JS Promise?

I fear my explanation above is too confusing... so please let me know if you'd need any extra details to better understand what I'm doing...

Thanks a lot!

In browser environment you can't block (apart from very hacky hacks and/or deprecated APIs that I won't mention).

Nothing in WASM could change that. WASM by itself is incapable of performing any I/O, and it has to ask JavaScript for input every time, and JS can't respond synchronously.

You have to use async for this. No amount of condvars, mutexes or atomics will save you.

So what does that mean? Having an async read_line function in JS that blocks on a Promise fulfilled by the onKey event loop and then returns the string, and then calling that from Rust whenever I need to get some text?

Been sleeping on this... and I fear the last thing I said isn't going to work either because that'd mean blocking inside WASM to wait for the result of a promise, which sounds like a no-no.

So... does that mean I'd need to rewrite the interpreter so that its run entry point is async and have it return some form of continuation whenever it needs input?

Can you require entire input at once? As Vec<u8> or String? If so, you could read it synchronously in WASM.

If you want to stream input from the network, or wait for next line to be provided from user interface, then the whole thing must be async fn (or some other form of a state machine that can be suspended).

1 Like

Alright. With a ton of pain, I have finally made this work :slight_smile: Had to do as you said and turn the whole interpreter's entry point into async so that the "request user input" function could be async as well. Hooking that up to the web interface proved to be tricky too. So... thanks a lot! The code is garbage right now and it will take some time to clean it up, but I think I'm on the right track.

However, along the way, I found about this "web workers" thing -- which sounded like the solution. After all, if I'm feeding a chunk of code to my interpreter, which could "block" due to an infinite loop, I wouldn't want the UI to block, right? I was hoping that this background worker would allow blocking until it received a message from the UI, but it sounds like that's impossible as well...

Anyway, this is becoming off-topic at this point, but if you could shed some more light into it, I'd appreciate it!

I'm not sure I fully understand what you want to do, but sometimes async channels can be useful. So your repl state machine might look like

struct Repl {
    sender: Sender<String>,
    receiver: Receiver<String>,
    // ...

then somewhere run

wasm_bindgen_futures::spawn_local(async {
    loop {
        let incoming = repl.receiver.recv();

I've had good success attaching WASM libraries to web workers for long-running jobs. The general idea is to write an entry point in the library for the worker's onmessage input and attach a callback which could be called from the library and convert to the worker's postMessage() calls for sending output to the page. But I suspect you'd need a way to retain state in your library between function calls from the worker. Perhaps the lazy_static crate might be of some use.

Though a bit outdated at this point, I found the old Hello, Rust page demos as easy starting points.