I thought I’d share the approach I used for I/O on the online Quill Adventure player tool I developed recently for playing ZX Spectrum Quill games online (take a look at my website link through my user details if you’re interested).
The backend of this was developed in the language Rust, targetting WebAssembly. Although I initially handled I/O just as character streams, it quickly became apparent that I needed something a bit more. Even on the fairly simplistic games that the Quill generates, there were still features, such as sound, that couldn’t be as easily handled through a character stream.
The approach I took was for the back-end to generate a JSON array as a string. Each element of the array could contain a different action.
I could then generate a range of actions, including things such as:
FreeText - can just rendered to the front-end output device directly
Debug - any debug information that I did not want to render to the front-end
Pause - a command instructing the front-end to pause for a number of milliseconds
Save - an array of bytes containing the data to write to a save file
Beep - the pictch and duration of a sound
Admittedly, my Quill Player is an interpreter, not a compiler but you may be able to apply similar concepts to the I/O runtime supporting the compiled code.
I’m not so familiar with generating WebAssembly from C. I’d recommend Rust to anyone that hasn’t tried it yet, although I appreciate that there is quite a learning curve and it’s not really as mainstream as C yet. But it’s got great support for JSON; just include the ‘serde’ library, annotate output data structures with the [Serialize] attribute and it will automatically generate JSON from them.
I’ve thought a lot about it, which is paradoxically why it’s taken so long to reply. There’s a lot to summarize.
In the long term I’d like it to be pluggable, using the WebAssembly dynamic linking mechanism if possible. For the short term, I’ll just support WASI, which is an immature libc-like standard interface that is supported by the major standalone WebAssembly engines but not yet by any browsers.
The mid term is the most challenging. It’s not currently possible for WebAssembly running on the main browser thread to block or yield. There are several proposals in the pipeline to address this, but it’s impossible to say how long that will take or what it will look like.
Emscripten, the C/C++ compiler to WebAssembly, uses a few different techniques to work around this limitation. They’re all based on the fact that the WebAssembly module’s memory stays the same after the code is finished running and code can be called again, reminiscent of TSRs back in the DOS days. We can emulate what Emscripten does with some challenge, and may have to in order to implement saving anyway.
Similarly, it should be possible for the Inform-produced module to import an interface source compatible with the one used by Glulx Inform.
Overall, I think there is a lot of opportunity to be flexible both on the author and on the users’ sides.
I’d like to chip in a couple of points that might be helpful;
I’m currently building interpreters for webassembly via emscripten. I’m not generating the WASM directly, only via transpiled C/C++.
I have had the same problems with how to block the interpreter and other I/O issues:
My architecture separates the GUI from the interpreter, usually by running the “back-end” in another thread. I could not get threads to work under emscripten. They are meant to work(?), but i had all sorts of problems and they didn’t work for me.
I reworked my front/back architecture to optionally support co-op multi-tasking. ie co-routine yielding.
Turns out emscripten supports co-routines as “fibers”. This worked well for me with WASM.
emscripten comes with simple file IO that appears to live in the download “package”, not sure where writes end up. Browser storage??
Initially, i had everything in the same package. Obviously this was bad for the initial download size. Especially if you have a selection of fonts and worse when you have sounds and pictures. Basically you need async fetching!
This was fixed when i switched to “sokol” as my renderer interface where now i use the sokol “fetch” interface, which i think calls out to JS to provide async fetching IO. Now i can demand load fonts, pictures etc.
sokol also fixed text input from mobile. I had a problem where it would not pop up the virtual keyboard, making it basically broken on mobile. I mention this as this is a common problem!
I’ve found Asyncify works quite well for blocking the interpreter. In Emglken there’s only one actual async function, getc, but dozens or hundreds of other functions which call it also need to be handled. Emscripten takes care of that for you.
And it might be possible to run the Asyncify algorithm on wasm code generated by Rust?
Emglken also has a custom file system which then access GlkOte’s Dialog library. Writing custom file systems would be possible for other storage models. But if you’re writing your own IO with Rust then you could just do whatever you want, it would be even simpler than this.
Theoretically you can run binaryen’s standalone wasm-opt tool in asyncify mode on it. As a practical matter, how well that will work depends on whether binaryen produces the WebAssembly constructs used by the Rust compiler. I’ve never seen it have a problem with any WebAssembly 1.0 instructions, but newer ones can come out of wasm-opt severely suboptimal or wrong.
(The code in question is almost verbatim from an Inform 6 for loop.)
Of course, there’s nothing stopping the Rust compiler from implementing Asyncify itself. The algorithm is pretty straightforward.