diff --git a/book/src/01-limitations/03-volatile_destination.md b/book/src/01-limitations/03-volatile_destination.md index cbb14da..d635e88 100644 --- a/book/src/01-limitations/03-volatile_destination.md +++ b/book/src/01-limitations/03-volatile_destination.md @@ -65,17 +65,19 @@ work, and the Rust developers just aren't interested in doing all that for such a limited portion of their user population. We'll just have to deal with not having any syntax sugar. -But no syntax sugar doesn't mean we can't at least do a little work for -ourselves. Enter the `VolatilePtr` type, which is a newtype over a `*mut T`: +### VolatilePtr + +No syntax sugar doesn't mean we can't at least make things a little easier for +ourselves. Enter the `VolatilePtr` type, which is a newtype over a `*mut T`. +One of those "manual" newtypes I mentioned where we can't use our nice macro. ```rust #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] -pub struct VolatilePtr(*mut T); +pub struct VolatilePtr(pub *mut T); ``` -Obviously we'll need some methods go with it. The basic operations are reading -and writing of course: +Obviously we want to be able to read and write: ```rust impl VolatilePtr { @@ -91,20 +93,23 @@ impl VolatilePtr { ``` And we want a way to jump around when we do have volatile memory that's in -blocks. For this there's both +blocks. This is where we can get ourselves into some trouble if we're not +careful. We have to decide between [offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.offset) and [wrapping_offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.wrapping_offset). The difference is that `offset` optimizes better, but also it can be Undefined Behavior if the result is not "in bounds or one byte past the end of the same allocated object". I asked [ubsan](https://github.com/ubsan) (who is the expert -that you should always listen to on matters like this) what that means for us, -and the answer was that you _can_ use an `offset` in statically memory mapped +that you should always listen to on matters like this) what that means exactly +when memory mapped hardware is involved (since we never allocated anything), and +the answer was that you _can_ use an `offset` in statically memory mapped situations like this as long as you don't use it to jump to the address of -something that Rust itself allocated at some point. Unfortunately, the downside -to using `offset` instead of `wrapping_offset` is that with `offset`, it's -Undefined Behavior _simply to calculate the out of bounds result_, and with -`wrapping_offset` it's not Undefined Behavior until you _use_ the out of bounds -result. +something that Rust itself allocated at some point. Cool, we all like being able +to use the one that optimizes better. Unfortunately, the downside to using +`offset` instead of `wrapping_offset` is that with `offset`, it's Undefined +Behavior _simply to calculate the out of bounds result_ (with `wrapping_offset` +it's not Undefined Behavior until you _use_ the out of bounds result). We'll +have to be quite careful when we're using `offset`. ```rust /// Performs a normal `offset`. @@ -113,20 +118,25 @@ result. } ``` -Now, one thing of note is that doing the `offset` isn't `const`. If we wanted to have a `const` function for -finding the correct spot within a volatile block of memory we'd have to do all the math using `usize` values -and then cast that value into being a pointer once we were done. In the future Rust might be -able to do it without a goofy work around, but `const` is quite limited at the moment. -It'd look something like this: +Now, one thing of note is that doing the `offset` isn't `const`. The math for it +is something that's possible to do in a `const` way of course, but Rust +basically doesn't allow you to fiddle raw pointers much during `const` right +now. Maybe in the future that will improve. + +If we did want to have a `const` function for finding the correct address within +a volatile block of memory we'd have to do all the math using `usize` values, +and then cast that value into being a pointer once we were done. It'd look +something like this: ```rust const fn address_index(address: usize, index: usize) -> usize { - address + (index * std::mem::size_of::()) + address + (index * std::mem::size_of::()) } ``` -We will sometimes want to be able to cast a `VolatilePtr` between pointer types. Since we -won't be able to do that with `as`, we'll have to write a method for that: +But, back to methods for `VolatilePtr`, well we sometimes want to be able to +cast a `VolatilePtr` between pointer types. Since we won't be able to do that +with `as`, we'll have to write a method for it: ```rust /// Performs a cast into some new pointer type. @@ -135,10 +145,116 @@ won't be able to do that with `as`, we'll have to write a method for that: } ``` -TODO: iterator stuff +### Volatile Iterating -Also, just as a little bonus that we probably won't use, we could enable our new pointer type -to be formatted as a pointer value. +How about that `Iterator` stuff I said we'd be missing? We can actually make +_an_ Iterator available, it's just not the normal "iterate by shared reference +or unique reference" Iterator. Instead, it's more like a "throw out a series of +`VolatilePtr` values" style Iterator. Other than that small difference it's +totally normal, and we'll be able to use map and skip and take and all those +neat methods. + +So how do we make this thing we need? First we check out the [Implementing +Iterator](https://doc.rust-lang.org/core/iter/index.html#implementing-iterator) +section in the core documentation. It says we need a struct for holding the +iterator state. Right-o, probably something like this: + +```rust +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct VolatilePtrIter { + vol_ptr: VolatilePtr, + slots: usize, +} +``` + +And then we just implement +[core::iter::Iterator](https://doc.rust-lang.org/core/iter/trait.Iterator.html) +on that struct. Wow, that's quite the trait though! Don't worry, we only need to +implement two small things and then the rest of it comes free as a bunch of +default methods. + +So, the code that we _want_ to write looks like this: + +```rust +impl Iterator for VolatilePtrIter { + type Item = VolatilePtr; + + fn next(&mut self) -> Option> { + if self.slots > 0 { + let out = Some(self.vol_ptr); + self.slots -= 1; + self.vol_ptr = unsafe { self.vol_ptr.offset(1) }; + out + } else { + None + } + } +} +``` + +Except we _can't_ write that code. What? The problem is that we used +`derive(Clone, Copy` on `VolatilePtr`. Because of a quirk in how `derive` works, +this makes `VolatilePtr` will only be `Copy` if the `T` is `Copy`, _even +though the pointer itself is always `Copy` regardless of what it points to_. +Ugh, terrible. We've got three basic ways to handle this: + +* Make the `Iterator` implementation be for ``, and then hope that we + always have types that are `Clone`. +* Hand implement every trait we want `VolatilePtr` (and `VolatilePtrIter`) to + have so that we can override the fact that `derive` is basically broken in + this case. +* Make `VolatilePtr` store a `usize` value instead of a pointer, and then cast + it to `*mut T` when we actually need to read and write. This would require us + to also store a `PhantomData` so that the type of the address is tracked + properly, which would make it a lot more verbose to construct a `VolatilePtr` + value. + +None of those options are particularly appealing. I guess we'll do the first one +because it's the least amount of up front trouble, and I don't _think_ we'll +need to be iterating non-Clone values. All we do to pick that option is add the +bound to the very start of the `impl` block, where we introduce the `T`: + +```rust +impl Iterator for VolatilePtrIter { + type Item = VolatilePtr; + + fn next(&mut self) -> Option> { + if self.slots > 0 { + let out = Some(self.vol_ptr.clone()); + self.slots -= 1; + self.vol_ptr = unsafe { self.vol_ptr.clone().offset(1) }; + out + } else { + None + } + } +} +``` + +What's going on here? Okay so our iterator has a number of slots that it'll go +over, and then when it's out of slots it starts producing `None` forever. That's +actually pretty simple. We're also masking some unsafety too. In this case, +we'll rely on the person who made the `VolatilePtrIter` to have selected the +correct number of slots. This gives us a new method for `VolatilePtr`: + +```rust + pub unsafe fn iter_slots(self, slots: usize) -> VolatilePtrIter { + VolatilePtrIter { + vol_ptr: self, + slots, + } + } +``` + +With this design, making the `VolatilePtrIter` at the start is `unsafe` (we have +to trust the caller that the right number of slots exists), and then using it +after that is totally safe (if the right number of slots was given we'll never +screw up our end of it). + +### VolatilePtr Formatting + +Also, just as a little bonus that we probably won't use, we could enable our new +pointer type to be formatted as a pointer value. ```rust impl core::fmt::Pointer for VolatilePtr { @@ -149,4 +265,79 @@ impl core::fmt::Pointer for VolatilePtr { } ``` +Neat! + +### VolatilePtr Complete + +That was a lot of small code blocks, let's look at it all put together: + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct VolatilePtr(pub *mut T); +impl VolatilePtr { + pub unsafe fn read(self) -> T { + self.0.read_volatile() + } + pub unsafe fn write(self, data: T) { + self.0.write_volatile(data); + } + pub unsafe fn offset(self, count: isize) -> Self { + VolatilePtr(self.0.offset(count)) + } + pub fn cast(self) -> VolatilePtr { + VolatilePtr(self.0 as *mut Z) + } + pub unsafe fn iter_slots(self, slots: usize) -> VolatilePtrIter { + VolatilePtrIter { + vol_ptr: self, + slots, + } + } +} +impl core::fmt::Pointer for VolatilePtr { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{:p}", self.0) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct VolatilePtrIter { + vol_ptr: VolatilePtr, + slots: usize, +} +impl Iterator for VolatilePtrIter { + type Item = VolatilePtr; + fn next(&mut self) -> Option> { + if self.slots > 0 { + let out = Some(self.vol_ptr.clone()); + self.slots -= 1; + self.vol_ptr = unsafe { self.vol_ptr.clone().offset(1) }; + out + } else { + None + } + } +} +``` + ## Volatile ASM + +In addition to some memory locations being volatile, it's also possible for +inline assembly to be declared volatile. This is basically the same idea, "hey +just do what I'm telling you, don't get smart about it". + +Normally when you have some `asm!` it's basically treated like a function, +there's inputs and outputs and the compiler will try to optimize it so that if +you don't actually use the outputs it won't bother with doing those +instructions. However, `asm!` is basically a pure black box, so the compiler +doesn't know what's happening inside at all, and it can't see if there's any +important side effects going on. + +An example of an important side effect that doesn't have output values would be +putting the CPU into a low power state while we want for the next VBlank. This +lets us save quite a bit of battery power. It requires some setup to be done +safely (otherwise the GBA won't ever actually wake back up from the low power +state), but the `asm!` you use once you're ready is just a single instruction +with no return value. The compiler can't tell what's going on, so you just have +to say "do it anyway".