Demilade Sonuga's blog
All postsRefactoring VIII
There are a lot of improvements that could be done to the code to make it generally better, easier to read, and more organized, but we won't be doing that at the moment. In this post, we'll be focused on figuring out how to go about removing the many instances of unsafety in our code.
What Needs To Be Done
Take a look at these lines in main.rs
:
static mut SCREEN: Option<&mut Screen> = None;
static mut GDT: Option<GDT> = None;
static mut TSS: Option<TSS> = None;
static mut IDT: Option<IDT> = None;
static mut PICS: Option<PICs> = None;
static mut KEYBOARD: Option<Keyboard> = None;
Mutable statics are inherently unsafe because they can be modified and read by any code anywhere without restrictions. When there are so many of them in the code, unsafety is unavoidable. So, to reduce unsafety, we need to reduce the instances of mutable statics.
Once and Interior Mutability
To do that, we first need to understand what these structures are used for and why exactly we modeled them as mutable statics.
First, we have the SCREEN
mutable static. The purpose of this static is to hold a reference to the memory
segment that controls what is displayed on the screen.
#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others
let init_graphics_result = init_graphics(boot_services);
// Halt with error if graphics initialization failed
if let Err(msg) = init_graphics_result {
let simple_text_output = sys_table.simple_text_output();
write!(simple_text_output, "{}", msg);
loop {}
}
let screen = init_graphics_result.unwrap();
init_screen(screen);
// ...Others
}
If you can remember from past posts, init_graphics
changes the mode of the screen into a graphics mode
that allows for drawing pixels. Upon success, the result returned by init_graphics
will hold the reference
to the screen memory.
Now, this is the reason we used a mutable static here: the address of the screen memory is not known at compile time, only at runtime. And that was a problem because regular safe statics are immutable and only useful when you know the value of what needs to be stored at compile time.
Another reason, mentioned in the Panicking I post, is that the panic handler needs to be able to print panic messages to the screen.
If the screen reference stayed on the stack, the panic handler won't have been able to access it. When we were writing the panic handler, there was no heap, but even now that there is a heap, I still don't think it will be a good idea to keep the reference on the heap. I mean, what if a panic occurs while memory is being allocated for the screen reference? How will the panic message be printed?
Making the static mutable allowed us to set it to the correct value at runtime.
The problem of only being able to set the value at runtime is the reason for making GDT
, TSS
,
and IDT
mutable statics. We just can't give them values until runtime, and if you take a
close look at the code, after we give them values, we never change them again.
Out of all the statics PICS
and KEYBOARD
are the only ones that aren't only initialized once. That is, after
initializing PICS
or KEYBOARD
, we still call functions on it that require mutable references, like
PICs::end_of_interrupt
or Keyboard::interpret_byte
.
But GDT
, TSS
, and IDT
, after initialization, are never modified again.
So, the power given to us by mutable statics is way much more than we need, and that can be a recipe for disaster.
What we need to resolve this is some kind of structure that will allow us to mutate a value only once, just to initialize it, and never allow mutation again.
In other words, we need a structure that can allow for one-time initialization. Normally, statics are initialized at compile time with a value and are never changed in a program. One-time initialization allows for initializing statics at runtime with a function to be run once, and after the run of that function, the static is never allowed to be modified again.
For example:
static GDT: Once<GDT> = Once::new();
fn init() {
GDT.call_once(|| { /* initialize the GDT */ });
// GDT is now accessed like a regular static
}
This Once
indicated above is a common synchronization primitive in std
. But we aren't using std
here,
so we'll have to implement one ourselves.
If you take a good look at the idea just presented, you'll notice that there's a problem. Regular statics are
always marked immutable, but a Once
mutates it after the run of the initialization function.
This mutation of apparently immutable values is also another rather common pattern in Rust called interior immutability. It's interior because the mutation happens within the value itself. This is a superpower that's used only when needed.
When such primitives with interior mutability, like Once
, are created, they typically use unsafe operations
which are manually verified to be safe and then present a safe outwards interface for use. In other words, when
interior mutability is used, it is the job of the programmer to verify that its use doesn't result in the
violation of any borrow checking rules or Rust safety guarantees.
Interior mutability is needed here because of the goals of the Once
: to allow a value to be initialized once
at runtime, and never be mutated again.
The value is not fully mutable. You can't mistakenly modify GDT
. The mutation is strategic and controlled
safely through the safe interface presented by Once
.
Mutex
As for KEYBOARD
and PICS
, these statics are mutated continuously. When PICs::end_of_interrupt
is called,
the ports controlled by the structure are written to. When Keyboard::interpret_byte
is called,
the Keyboard::code_is_extended
field of the keyboard is (sometimes) mutated.
Another problem we've been encountering is the occasional panic due to random and unpredictable double faults. Common causes of bugs of this kind are races (using shared data without any form of synchronization).
All this talk about synchronization brings us to another mutable static in the project, in allocator.rs
:
static mut ALLOCATOR: Option<LinkedListAllocator> = None;
And its use:
In boxed.rs
impl<T> Box<T> {
pub fn new(val: T, allocator: *mut dyn Allocator) -> Box<T> {
// ...Others
}
}
In vec.rs
impl<T: Clone> Vec<T> {
// Creates a vector with the stated capacity
pub fn with_capacity(capacity: usize, allocator: *mut dyn Allocator) -> Vec<T> {
// ...Others
}
}
In boxed_fn.rs
impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
// ...Others
}
}
The Allocator
trait objects are not safe references or boxes. They're all raw pointers. And data behind
raw pointers can be easily accessed without any guarantees of synchronization. This lack of
synchronization is the sort of thing that can lead to races.
Even though this code is not multithreaded, there is still a form of concurrency, through interrupts.
One primitive that we can use to handle this synchronization issue is the Mutex
, which stands for mutual
exclusion. The Mutex
is a structure that allows only one line of execution to access a piece of data
at a time. In other words, when one line of execution has access to the data in a Mutex
, no other line
of execution can access that data until the first one lets go.
You can think of a Mutex
as a coordinator that gives mutable references to a piece of data to one thread
at a time.
It can also allow for interior mutability in a multithreaded scenario.
With a Mutex
, we could refactor our KEYBOARD
and PICS
from mutable statics to:
static KEYBOARD: Mutex<Keyboard> = Mutex::new();
static PICS: Mutex<Pics> = Mutex::new()
And safely access and modify it, like so:
KEYBOARD.lock().interpret_byte(byte);
PICS.lock().end_of_interrupt(val);
Effectively getting rid of all the unsafety.
The allocator no longer has to be a mutable static, but instead, it should be in a Mutex
that will control
accesses to it. Mutation will no longer be done anyhow and anywhere. It will be done in a safe synchronized
manner, using interior mutability.
No, we need a Mutex
too. There is a Mutex
in std
, but we aren't using std
here. We need to create one
ourselves.
What We Need To Do Now
Before we can get refactoring with a Mutex
and a Once
, we first need to create them by ourselves.
This presents to us a new todo list:
- Create a
Once
. - Create a
Mutex
.
We'll start tackling the Once
in the next post
Take Away
- Interior mutability is a pattern in Rust where supposedly immutable variables provide a safe and contained interface to mutate values within them.
- A
Once
is used for one-time initialization. - A
Once
uses interior mutability by providing a safe interface to mutate the contained data only once for initialization purposes. - A
Mutex
is used to prevent multiple threads from accessing shared data at the same time. - A
Mutex
uses interior mutability by providing a safe interface to allow code to obtain exclusive mutable access to some data
In The Next Post
We'll start implementing a Once
.
References
- Interior Mutability: https://ricardomartins.cc/2016/06/08/interior-mutability https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
std
'sOnce
: https://doc.rust-lang.org/std/sync/struct.Once.htmlstd
'sMutex
: https://doc.rust-lang.org/std/sync/struct.Mutex.html