So if I were to implement a signaling mechanism in a WASM runtime, what would that look like?
So if I were to implement a signaling mechanism in a WASM runtime, what would that look like?
From a similar handler, you could check if a signal flag has been set, then call a user-defined signal handler exported from the module.
If you don't control the runtime, you could do this instrumentation to the bytecode rather than the generated machine code, effectively simulating preemption via injected yield points.
Wasmer's implementation of metering[0] just traps when it runs out of fuel. WasmEdge's implementation of interruptibility[1] checks a flag and stops execution if it's set.
While neither of these support resuming execution after the deadline, replacing the halt with a call to a signal dispatcher should work.
Wasmtime has two different implementations of interrupting execution that both support resuming[2]. The fuel mechanism[3] is deterministic but the epoch mechanism[4] is more performant. If you're free to pick your runtime, I'm sure you could configure Wasmtime into doing what you want.
[0]: https://github.com/wasmerio/wasmer/blob/master/lib/middlewar...
[1]: https://github.com/WasmEdge/WasmEdge/pull/910
[2]: https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#...
6.4 The Cons Counter and Errorset
The cons counter is a useful device for breaking out of program loops. it automatically causes a trap when a certain number of conses have been performed. The counter is turned on by executing count [n], where n is an integer. If n conses are performed before the counter is turned off, a trap will occur and an error diagnostic will be given.
Sythesizing the other comments:
The "natural" way to do it exactly resembles a microcontroller interrupt handler. At an instruction boundary, push the current execution state onto the stack; jump to an alternate execution point; provide a special instruction to return to the previous execution state. This also copies all the _problems_ of POSIX signals (may happen in the middle of any execution state, what to do if you're in the middle of a syscall, etc).
If you just want a bidirectional bytestream: build a bidirectional bytestream. The classic select() or poll() style interface.
If you need higher performance but don't want the hassle of interruption at any stack state: build co-operative multitasking into the runtime. Provide a means of very quickly checking a single bit (in practice, a word of atomic granularity) to see if something needs to be done, then write the software so it can check this at various points and co-operate. Similar to uses of CancellationToken in C#.
>At an instruction boundary, push the current execution state onto the stack; jump to an alternate execution point; provide a special instruction to return to the previous execution state.
In the VM, for each instruction:
1. Check if some bit has been set, which would indicate "we gotta interrupt" 2. Push current state onto the stack 3. Jump to pre-defined location & execute whatever's there
At some point, the code from step 3 (be it code I wrote or some guest-supplied interrupt handler) will push an instruction onto the stack that says "resume".
Is that roughly correct?
If so, this sounds like something that (unsurprisingly) requires support at the VM level. WASM has no such feature, though... are you aware of any clever hacks or workarounds?
At issue is the fact that select/poll requires integration into the language runtime of whatever has been compiled to WASM. For example, imagine that you've compiled an application written in Go to WASM and you want to create a syscall that is analogous to select while still being a distinct thing. The problem you run into is this: what if there's a timer that's going to fire in 500µs? You have no way of determining that and setting your `select` timeout accordingly. To do so would require hacking the Go runtime itself, along with the compiler.
>There is also a design variant where the whole read/write operation is scheduled in advance and the blocking syscall just notifies you which previously scheduled operation has completed, similar to Windows IOCP or Linux io_uring.
I'm not sure I'm fully grasping the distinction, but it seems appealing... can you elaborate? Or is there something I could read/watch?
look into a.out and ELF formats for executable files; your executable files, said files hereby called "your code", will be in this format on linux.
There will be an entry point for your code, so if your program gets run, the kernel (or whatever userspace launcher it's given this task to... now that I think of it, from the command line, the shell would fork itself, and one fork would exec your program, so we're talking about exec here) will load your code into memory, and transfer control to--transfer control to means set the program counter to--your code at a particular address (the address of C main(), so to speak). Before this happens, the launcher will have allocated memory for you, set up the stack pointer, opened stdin/out/err, put the command line arguments into place (argc, argv), etc. Then your code can run till it's done, making system calls if it needs to, and potentially being preemptively multitasked, but that's all invisible to your code, black box in terms of how it's implemented.
now for signals...
Signals from the operating system need their own entry point(s) into your code. So, while your code is running, the OS can interrupt it (stuffing everything of yours onto your stack so it can be popped off later when control flow is returned to the same point later). In this meantime, the OS will now transfer control (set the program counter just like it did when it first launched you) and you're off and running with code to handle the signal, the interrupt.
If you have not prepared your code for this event, the default (set up by the launcher for programs) program flow at this point will be sent to a generic handler that says "unhandled signal", and your program will be killed. They're actually lying, the signal is being handled, but it's being handled by a handler that cleans up and exits.
But, you can write code to handle the signal yourself, and when your program launches you (generally in main() or nearby) register the address of the handler you want to use so that when the interrupt comes the OS will know where to send control. At that point it's as if you just called a function/subroutine at wherever your code running at that moment, and when that function returns, things continue as they were before, modulo whatever you did in your handler.
There's a bunch of specifics I don't know, is there one entry point for all signals, and a signal argument token decides where to transfer control, or are there 10 separate handlers so you don't waste time? etc.
if you're doing no-wait I/O with a single thread, one way is to make a blocking call, call into something that doesn't return till there's new data, and that new data may then arrive, handled by your interrupt handler; in that case returning from your input-getting handler could return from the blocking call and your code is off and running as if it just returned in the first place. There are a myriad variations on this, but that's the general idea.
happy to clarify anything, others can probably answer better, etc.
>Then your code can run till it's done, making system calls if it needs to, and potentially being preemptively multitasked, but that's all invisible to your code, black box in terms of how it's implemented.
I'm realizing that I don't actually understand how pre-emptive multi-tasking is implemented at the VM level, and I think this is perhaps what I actually want. Can you elaborate and/or suggest any articles/video-lectures on the subject?
Maybe what I need is an operating-systems course...