Accurately representing a per-processor, fixed-address MMIO device

Hello,

I am writing a bare-metal operating system, currently working on the x86_64 implementation. I am dealing with the local APIC, which is a per-processor interrupt delivery device that is wired to the same MMIO address on each processor. To me, this is most easily represented with a static structure that contains a pointer to the memory, e.g.:

pub struct LocalApic(NonNull<u32>);

pub static LOCAL_APIC: Lazy<LocalApic> = Lazy::new(...);

impl LocalApic {
    pub fn set_enable(&mut self, enable: bool) { ... }

My concern with this is the mutability semantics. Given the structure is static, taking self by exclusive borrow is not possible. I would, however, like to be able to do this, as it more accurately represents some of the operations I may perform on the local APIC. I see two ways to overcome this:

  1. A mutex or other sync mechanism (which would be semantically pointless, given the memory is processor-local).
  2. Lying to the compiler by taking self by shared borrow, and then using NonNull::as_mut.

I lean towards option 2, but I fear that taking self by shared borrow will allow some kind of unsafe behavior in-place. For instance, if I return a type that references the lifetime of self, e.g.:

pub struct LvtTimer<'a>(&'a mut LocalApic);

impl LocalApic {
    fn get_lvt_timer<'a>(&'a self) -> LvtTimer<'a> { ... }

… then I suspect (but I do not know for sure) that I could somehow violate a safety rule somewhere without noticing.

Any input on how to neatly solve this is greatly appreciated.

Thanks.

In Rust, &mut represents exclusivity, mutability is merely a byproduct of that. You example would, most likely, be UB: if you generate two &mut to the same bytes, it is instant UB at the moment you've created the alias.

In general, for low lever things it is better to only use pointers and add lifetimes with phantom data, so that API is safe.

In this case I personally would have done things differently. It is very difficult to say without knowing your architecture, but depending on details I would have either initialized a number of structs per processor and passed them around - that way you'll be able to control exclusivity in compile time. Another, more probable route is either to design the whole structure to allow aliasing (I bet Intel allows it) or added runtime checks.

Idea of having a static for this AND using borrow checker are fundamenly incompatible.

I'm not too versed in this topic, but remembered reading this blog post. I think the part about taking references of MMIO addresses in the section "Memory-Mapped I/O" could be relevant for your situation. Accessing Hardware in Rust - Ferrous Systems