If you remember when we talked about the operating system ABI and calling conventions earlier, you probably remember that each architecture and OS have different requirements. This is especially important when creating new stack frames, which is what happens when you call a function. So, the compiler knows about what each architecture/OS requires and adjusts layout, and parameter placement on the stack and saves/restores certain registers to make sure we satisfy the ABI on the platform we’re on. This happens both when we enter and exit a function and is often called a function prologue and epilogue.
In Rust, we can enable this feature and mark a function as #[naked]. A naked function tells the compiler that we don’t want it to create a function prologue and epilogue and that we want to take care of this ourselves. Since we do the trick where we return over to a new stack and want to resume the old one at a later point we don’t want the compiler to think it manages the stack layout at these points. It worked in our first example since we never switched back to the original stack, but it won’t work going forward.
Our DEFAULT_STACK_SIZE is set to 2 MB, which is more than enough for our use. We also set MAX_THREADS to 4 since we don’t need more for our example.
The last static constant, RUNTIME, is a pointer to our runtime (yeah, I know, it’s not pretty with a mutable global variable, but it’s making it easier for us to focus on the important parts of the example later on).
The next thing we do is set up some data structures to represent the data we’ll be working with:
pub struct Runtime {
threads: Vec<Thread>,
current: usize,
}
#[derive(PartialEq, Eq, Debug)]
enum State {
Available,
Running,
Ready,
}
struct Thread {
stack: Vec<u8>,
ctx: ThreadContext,
state: State,
}
#[derive(Debug, Default)]
#[repr(C)]
struct ThreadContext {
rsp: u64,
r15: u64,
r14: u64,
r13: u64,
r12: u64,
rbx: u64,
rbp: u64,
}
Runtime is going to be our main entry point. We are basically going to create a very small runtime with a very simple scheduler and switch between our threads. The runtime holds an array of Thread structs and a current field to indicate which thread we are currently running.
Thread holds data for a thread. The stack is similar to what we saw in our first example in earlier chapters. The ctx field is a context representing the data our CPU needs to resume where it left off on a stack and a state field that holds our thread state.
State is an enum representing the states our threads can be in:
- Available means the thread is available and ready to be assigned a task if needed
- Running means the thread is running
- Ready means the thread is ready to move forward and resume execution
ThreadContext holds data for the registers that the CPU needs to resume execution on a stack.