Note
I promised to point out where we close the impl Runtime block, and we do that after the spawn function. The upcoming functions are “free” functions that don’t belong to a struct.
While I think t_yield is the logically interesting function in this example, I think spawn is the most interesting one technically.
The first thing to note is that the function takes one argument: f: fn(). This is simply a function pointer to the function we take as an argument. This function is the task we want to run concurrently with other tasks. If this was a library, this is the function that users actually pass to us and want our runtime to handle concurrently.
In this example, we take a simple function as an argument, but if we modify the code slightly we can also accept a closure.
Tip
In example ch05/d-fibers-closure, you can see a slightly modified example that accepts a closure instead, making it more flexible than the one we walk through here. I would really encourage you to check that one out once you’ve finished this example.
The rest of the function is where we set up our stack as we discussed in the previous chapter and make sure our stack looks like the one specified in the System V ABI stack layout.
When we spawn a new fiber (or userland thread), we first check if there are any available userland threads (threads in Available state). If we run out of threads, we panic in this scenario, but there are several (better) ways to handle that. We’ll keep things simple for now.
When we find an available thread, we get the stack length and a pointer to our u8 byte array.
In the next segment, we have to use some unsafe functions. We’ll explain the functions we refer to here later, but this is where we set them up in our new stack so that they’re called in the right order for our runtime to work.
First, we make sure that the memory segment we’ll use is 16-byte-aligned. Then, we write the address to our guard function that will be called when the task we provide finishes and the function returns.
Second, we’ll write the address to a skip function, which is there just to handle the gap when we return from f, so that guard will get called on a 16-byte boundary. The next value we write to the stack is the address to f.
Why do we need the skip function?
Remember how we explained how the stack works? We want the f function to be the first to run, so we set the base pointer to f and make sure it’s 16-byte aligned. We then push the address to the skip function and lastly the guard function. Since, skip is simply one instruction, ret, doing this makes sure that our call to guard is 16-byte aligned so that we adhere to the ABI requirements.
After we’ve written our function pointers to the stack, we set the value of rsp, which is the stack pointer to the address of our provided function, so we start executing that first when we are scheduled to run.
Lastly, we set the state to Ready, which means we have work to do and that we are ready to do it. Remember, it’s up to our scheduler to actually start up this thread.
We’re now finished implementing our Runtime, if you got all this, you basically understand how fibers/green threads work. However, there are still a few details needed to make it all work.