For context, I am writing a keyboard based cross platform calorie tracker in zig using sokol (for transparency goals to sell it commercially to make money). I want the project to compile to WASM, (I am using zig 15.2). I tried to make sure I am using the correct terms here, but WASM and threading are very much not my area of expertise (english too, for that matter), I hope my description is clear enough. (feel free to tell me to rename this something better)
I want thread A and B to be able to yield to each other like a stackful coroutine. So the pattern is thread A yields to thread B, thread B runs, then yields to thread A, and they essentially ping pong off each other. (Sokol runs on the main thread, and my logic runs on a second thread.) For desktop it was very easy to do this by running 2 threads and having semaphores tell each other when to switch turns, but that didnât work for WASM (itâs not exactly clear to me what the rules are for this thing in WASM, as well as what the language currently can/canât do with the IO changes).
I failed to find how to do this, a worse case scenario would be me having to refactor all that logic into a finite state machine, but I would rather avoid that if thereâs an alternative.
WASM by itself has no concept of threading. When you use threads on zig and compile to wasm, you are either using wasi threads or emscripten threads (pthreads). The actual threading is done by the runtime that runs your program. Emscripten (and prob wasmer wasi?) uses WebWorkers for this, instancing the same wasm program on a web worker and running the entry point there.
Wasmtime w/ wasi uses OS threads.
Iâm not sure what the limitations of WebWorkers are, but I feel like you canât ever block the main thread on JS. You probably could alternatively implement couroutines using either JSPI or emscriptenâs asyncify (which also uses JSPI if available) Asynchronous Code â Emscripten 5.0.4-git (dev) documentation without having to run any webworkers at all.
WASM can work with this model, depending on how your code does the yielding.
Without WASI, you wonât have threading in the traditional sense. You will have a module that exposes some functions that you can call from the host. The module can only run when the host calls into one of those functions.
The easiest way would be to expose 2 functions, one for sokol/rendering, one for business logic. The host would run the functions one after the other in a loop. This assumes that the functons terminate after they have done their work. Since you already have semaphore stop conditions, you could use those as your breaking point.
Another option would be to have 1 exposed function (i.e. tick, poll) and the host just calls that one function. Whoâs turn it is can be maintained inside the module and on each call to the function, you check which turn it is.
These two options keep it simple (only one module, only one instance), but rely on the host to do all the driving.
I suppose my problem that I am trying to solve is that the thread for my logic needs to âstoreâ a lot of information on the stack between calls, where this seems like it starts from a âblank slateâ.
Contrary to popular belief, WASM supports pthreads-style shared-memory-threading and Emscripten provides a POSIX-threads compatible C-API (I donât know though how thatâs handled in the Zig stdlib).
It comes with one massive caveat though: the web server must be configured to add âCOOP/COEPâ headers to HTTP responses for security reasons (e.g. âcross-origin-isolationâ - basically telling the browser that the page needs to run isolated in its own process).
I never tinkered with WASM pthreads though because the COOP/COEP requirement locks out most hosting providers (like Github pages or AFAIK also itchio).
Tbh, unless you absolutely need multithreading for performance, I would recommend sticking to the main thread and if things like loading assets needs to be performed in the âbackgroundâ while rendering continues, jump out into Javascript functions and use Promise.then().catch() where the then() and catch() code calls âcompletion callbacksâ on the WASM side - traditional completion callback donât require dealing with the ASYNCIFY magic code transform, and since you are rendering frames anyway, polling each frame for completion of background tasks isnât a big deal.
If you just need to load assets in the background you can also check out sokol_fetch.h, this basically shields you from having to write Javascript Thereâs now also a Zig-binding for it (sokol-zig/src/sokol/fetch.zig at master ¡ floooh/sokol-zig ¡ GitHub) but I havenât actually tested it on Zig (e.g. thereâs no Zig example for sokol-fetch).
The sokol-samples repository has a couple of C examples for loading things in the background via sokol_fetch.h, e.g.:
(click the âsrcâ link at the bottom to see the C source code)
There is also a âlocal workaroundâ for the COOP/COEP issue via âservice workersâ, but that isnât reliable unfortunately:
If browser supports JSPI (javascript promise integration), ASYNCIFY is not that bad. In the web frontend for the platform Iâm working on I actually require JSPI for functions like preadv and so on. https://cloudef.pw/sorvi/#supertux.sorvi (example here, requires javascript.options.wasm_js_promise_integration on firefox, this does not use emscripten at all and it can load any compatible wasm binary)
Iâm just slightly annoyed that such basic feature like JSPI (suspend and resume wasm execution) is such a recent feature on browsers.
First of all thanks for writing Sokol, itâs the only library I tried that just immediately worked for all platforms and that got me sold. Itâs also somewhat surreal to be in a community where essentially the authors of my entire tech stack are present in the same forum.
The problem isnât performance at all actually, everything in the codebase runs pretty much instantly, itâs the way that the logic needs to be written. Thatâs what made me use threads.
From what I read you can use multiple threads in WASM as long as the main thread doesnât block, but in my initial setup the problem is that while the âbusinessâ thread is running the sokol thread is waiting.
The app is similar to a terminal in the sense that there is a text buffer, the sokol thread writes on that buffer and the âbusiness threadâ just reads it as if reading text from stdin is the illusion I was trying to create with the API. This is essentially how a function from the âbusiness threadâ looks like.
fn foo() void {
const s = UI.readNext();
//... do stuff1
const s2 = UI.readNext();
//... do stuff2
const s3 = UI.readNext();
//... do stuff3 etc.
}
Where as if I was calling this step by step from the main thread I would have to store the variables for âdo stuffâ somewhere as well as what code should be executed (immediately thinking for a finite state machine).
Yeah essentially Sokol hijacks the main thread, and from what I read the main thread is not allowed to block. (correct me if I am wrong here). So when the âbusinessâ thread actually gets the string, the sokol thread is blocked with the semaphore setup. The Sokol thread reads from memory the âbusinessâ thread modifies so I am forced to block it.
I also failed to compile multithreaded code in the first place in WASM, but I think thatâs a completely separate issue. (I didnât try all these tricks / compiler flags people have suggested so far).
I feel like you can do this without threads all together. Considering your app probably renders every frame anyways, so you can do your own polling / scheduling based on that. Of course, if you donât want to wastefully render at fixed rate, then unfortunately you need to interact with the browser somehow (write platform specific code).
Iâm not sure if sokol has abstractions for this but Iâm sure @floooh knows
Yeah thereâs 100% not a need for concurrency here, if I could have thread A say âI am done, switch stack frame to thread Bâ and thread B do the same the problem would be solved. Thing is I donât know how to do it (or whether zig 15.2 supports it, I think it doesnât with the IO stuff coming).
The Reason I used 2 threads is because itâs easy to do what I just described with them.
Of course, if this is hard to do I could just eat my medicine and write the business logic as a finite state machine (tedious to write, but I am confident itâs going to work), this will depend on how easy the solutions I learn here are. (my business logic is due to a re-write because of some breaking changes I made, so I made this post to decide what I will do).
This is something I am trying to get opinions on actually, whether people think that will be easier.
Zig does not support stackless coroutines (yet). Wasm also does not have standard way to do stackful coroutines. Itâs possible to do coroutines on browser through JSPI or emscripten asyncify (as emscripten fibers.h seems to do). That lets you suspend and resume a stack frame.
If you want to cheat, you could also write the scheduler in rust as it has stackless coroutines, which then call your zig functions
I need stackful co-routines, since The challenge is that In the âdo stuffâ steps I store a lot of information.
The way I see it, I can either try to figure out a cheat to make threads work in WASM and make the sokol thread not block, or I can look into the asyncify stuff, or I can write my logic as a finite state machine.
I donât have the same requirements for quality on the web version, It will be like a demo you can run on the browser, so if itâs not perfect itâs not the end of the world, the actual product will only run on the users machine.
Stackless really does not mean you donât have a stack. It just means compiler generates the state machine for you that you would otherwise write manually (using the caller stack).
Okay, I didnât know that. If thatâs the case why is there a distinction between stackless and stackful co-routines if stackless essentially have a stack?
So would it be possible to use co-routines (preferably written in C, I didnât like rust) to make this API work without having to go through the emscripten asyncify stuff?
Iâm not aware of any C compiler with stackless coroutines, so no. You still would have to use the emscripten fibers.h api (you can use this from zig too), or the asyncify transformation which is more lower level.
Stackless is bit silly name, but it just means there is no dedicated stack for each coroutine, but rather shared. Stackless is nice because it is purely compiler transformation so it will work anywhere. This is why rust on embedded is so good.