Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do the current semantics for referencing registers interfere with composability? #151

Closed
kjetilkjeka opened this issue Oct 6, 2017 · 19 comments

Comments

@kjetilkjeka
Copy link
Contributor

Memory mapped IO is a completely different animal than the normal memory. It may mask bits when reading/writing, it oftens changes values of registers "asynchronously", may be inherently stateful and may even trigger hard faults (or similar) when a read is attempted. It's not given how rusts notion of safety should be extended or extrapolated into something meaningful when hardware access is thrown into the mix.

Code generated with svd2rust currently exposes two interfaces to the hardware. One of them is the static Peripheral structs. These give a strong feeling of interior mutability and lets you safely (no unsafe) borrow one in exchange for a CriticalSection token. This is great! Except for when it isn't. Which unfortunately is a lot of the time. What I'm referring to is the composability problem of

  1. Being allowed to safely access IO in a critical section
  2. Only being allowed to safely access IO in a critical section.

Let's look at number two first. Imagine a token representing that a clock source has been activated for a peripheral. To use the peripheral you would have to present such token with a longer lifetime than the peripheral. The first problem is that, with safe rust, the peripheral would have lifetime of the CriticalSection and so would the ClockSource token and the actual peripheral as well. This would force you to either reinitialize everything the next time you're going to use the unit (which makes the library *useless in practice) or always do everything in a critical section all the time (which makes the library *useless in practice).

The other way to access IO is by generating a struct with references to all io. With this method, you don't have a present a CriticalSection token. This is obviously unsafe for the following reasons:

  1. You can generate a gazillion structs which all reference the same data
  2. You can change the data by using non-mutable references without "feeling like" interior mutability
  3. Even if you only give out one struct and doesn't alias the reference, the only thing an external module has to do is presenting a CriticalSection badge and they will be able to mess up the hw module without calling unsafe code.

My first question is. Why do we generate these Peripherals that will safely trade a CriticalSection for register access? It seems like they are of limited use in practice and by allowing this as "safe behavior" destroys composability by making it safe for a library to change the state of io registers as long as they have a critical section.

Is this not a threat to composability? What is the drawback of removing these constants? What is the drawback of making them unsafe to use?


The next problem I see is that there is no way to get a mutable reference to the registers. Let's look at the clock gating example again. If several peripherals share a common clock source it would make sense to require a read reference so they can't be changed while the peripheral is in use. This only holds as long as you need to use a mutable reference to mutate the registers. Due to never being able to get a mutable reference It's currently impossible to write the drivers in such way (without using transmute).

What would be the drawback of the Peripheral struct containing mutable references instead of non-mutable references?

I see that "state altering" calls in std often doesn't require mutable references, this in turns requires more error handling capabilities (no functions are assumed "not fail" and thus have to return a Result). An example is std::net::TcpStream. I guess it could be achievable by checking all relevant configurations before doing anything, but i don't think it will be worth it.

(I'm aware that you can currently create several Peripheral structs. It's not ideal, but due to requiring unsafe code it's ok. And in the future, mechanisms for ensuring that only one mutable reference exists to each register can be implemented, the app! macro from rtfm seems like a step in the right direction)


*(Exaggeration for artistic purposes. The library is not useless, it's actually great! The safe constant definitions is a bit weird though.)

@japaric
Copy link
Member

japaric commented Oct 7, 2017

@kjetilkjeka Thanks for bringing the topic up. I've never too happy about the way we handle
peripherals today (or ever). This is good opportunity to write my thoughts on the topic.

Preamble: on safety

I think it's important to remember that safety in Rust means memory safety and that race
conditions are considered safe in Rust. What's not considered safe are data races. The
observable effects of data races are undefined behavior (i.e. the compiler changes the behavior of
your program through (mis)"optimization") and torn reads and writes (these corrupt data at runtime).
The observable effect of race conditions is non-determinism, which mainly translates to logic bugs.

Thus an API like this is totally (memory) safe:

// generated by svd2rust. Usart1 has interior mutability.
static USART1: &'static Usart1 = /* .. */;

fn isr1() {
    USART1.dr.write(0xAA);
}

fn isr2() {
    USART1.dr.write(0x55);
}

It is racy but it's memory safe (there's no data race) per Rust definition.

The example above is no different than this one:

static FOO: AtomicUsize = ATOMIC_USIZE_INIT;

fn isr1() {
    FOO.store(FOO.load(Ordering::Relaxed) + 1, Ordering::Relaxed)
}

fn isr2() {
    FOO.store(FOO.load(Ordering::Relaxed) * 2, Ordering::Relaxed)
}

Which is also memory safe (no unsafe) and racy (intermediate values of FOO may be lost).

Both examples have no data races because the read / write operations, in isolation, are performed in
a single instruction (no torn reads / writes). The first example has no undefined behavior (UB)
because the use of volatile operations under the hood; volatile prevents both misoptimizations and
optimizations. The second one has no UB because the compiler knows about atomic operations.

What I'm trying to say with this example is that unsafe is not the right way to prevent race
conditions -- the composability problem you are describing effectively boils down to race conditions
in practice. Operations on registers are already free of data race problems; unsafe would be
signaling the wrong problem as there's no memory safety problem here.

An old idea

OK. So what should we do here? The problem of race conditions arises from peripherals having
global scope. Race conditions are very easy to introduce if you use the peripheral abstraction
exposed by svd2rust. RTFM helps with the scoped nature of resources but nothing stops you from
directly using svd2rust peripherals and introducing race conditions (cf. japaric/cortex-m-rtfm#13).

I think it may be worthwhile to retry an idea that I discarded some time ago because it's not zero
cost: non-global singletons. With code it looks like this:

// # svd2rust generated code
// proxy
pub struct USART1 { _0: () }

impl ops::Deref for USART1 {
    type Target = usart1::RegisterBlock;

    fn deref(&self) -> &usart1::RegisterBlock {
        unsafe { &*(0x4000_0000 as *const _) }
    }
}

// omitted: DerefMut implementation

pub mod usart1 {
    pub struct RegisterBlock { sr: Sr, dr: Dr, /* more registers */ }
}

// here's the singleton "constructor"
pub fn usart1() -> Option<USART1> {
    static USED: Mutex<Cell<bool>> = Mutex::new(Cell::new(false));

    let free = interrupt::free(|cs| {
        let used = USED.borrow(cs);

        if used.get() {
            false
        } else {
            used.set(true);
            true
        }
    });

    if free {
        Some(USART1 { _0: () })
    } else {
        None
    }
}

// # application code
fn main() {
    // get peripheral singleton instance
    let usart1 = stm32f103xx::usart1().unwrap();
    // let usart1_alias = stm32f103xx::usart1().unwrap(); // would `panic!`

    // construct driver
    let serial: Serial<USART1> = Serial::new(usart1);

    // do your thing
}

(Alternatively we could make the singleton "constructor" return Option<&'static mut usart1::RegisterBlock> but I think the proxy will optimize better)

Basically we move from a global singleton with synchronized access (interrupt::free) to a
non-global singleton. This non-global singleton can only be acquired once -- there's a boolean
protecting it. That check is also where the non zero cost come from: the compiler is unable to
optimize away the boolean even when it's obvious to us that the peripheral is acquired once.

Leaving the non zero cost problem aside. Recent improvements in RTFM (japaric/cortex-m-rtfm#43) open
new possibilities when paired with this non-global singleton approach. One of them is being able to
construct drivers in init (see below); today you kind of have to reconstruct drivers at the start
of each task.

app! {
    resources: {
        static SERIAL: Serial<Usart1>;
    },

    tasks: {
        USART1: {
            path: usart1,
            resources: [SERIAL],
        },
    }
}

fn init() -> init::LateResources {
    let usart1 = stm32f103xx::usart1().unwrap();
    let serial = Serial::new(usart1);

    init::LateResources {
        SERIAL: serial,
    }
}

fn usart1(_t: &mut Threshold, r: USART1::Resources) {
    r.SERIAL.write(r.SERIAL.read().unwrap()).unwrap();
}

This also means that your driver would now be able to hold state and preserve it across task runs.
Yay!

Making it zero cost

The singleton constructor I showed above is certainly non zero cost but I think that with the
guarantees of the init function (known to run once) we can make all the singletons available
in init at zero cost. See below:

// svd2rust generated code
static mut USED: bool = false;

pub fn usart1() -> Option<USART1> {
    let free = interrupt::free(|cs| {
        if unsafe { USED } {
            false
        } else {
            unsafe { USED = true; }
            true
        }
    });

    if free {
        Some(USART1 { _0: () })
    } else {
        None
    }
}

// NOTE(unsafe) aliases the singleton. Must be called once.
pub unsafe fn _usart1() -> USART1 {
    // NOTE this will be mutated within a critical section (see below)
    USED = true;

    USART1 { _0: () }
}

// rtfm generated code
fn main() {
    interrupt::free(|_| {
        let usart1 = unsafe { stm32f103xx::_usart1() };
        let p = stm32f103xx::Peripherals { USART1: usart1, /* more peripherals */ };

        init(p);
    });
}

// user code
fn init(p: stm32f103xx::Peripherals) {
    let serial = Serial::new(p.USART1);
    // let usart1 = stm32f103xx::usart1(); // this would `panic!`
}

I think LLVM is smart enough to optimize away static variables that are only written to as it's
the case of USED here but this is untested.

Shared peripherals?

This non-global singleton approach lets you "hide the existence of peripherals" (for the most part
-- you'll still have to deal with them in init) because you can put them inside drivers and have
task resources be drivers instead of peripherals. This should work well with drivers that own
peripherals but some drivers will need to share peripherals like the DMA. If you need the DMA then
I think you will still have peripherals as resources. See below:

fn exti0(t: &mut Threshold, r: EXTI0::Resources) {
    r.DMA1.claim(t, |dma1| r.SERIAL1.write_all(dma1, some_buffer));
}

fn exti1(t: &mut Threshold, r: EXTI0::Resources) {
    r.DMA1.claim(t, |dma1| r.SERIAL2.write_all(dma1, some_buffer));
}

Can't think of a way to hide the DMA1 resource into the Serial abstraction right now. There's no
access to resources (Resource<T>) in init and being able to construct one there would bypass
static analysis, I think.

Thoughts on the above proposal? cc @hannobraun @thejpster


Interior mutability

Interior mutability was required for RTFMv1 because it was not safe / sound to get a mutable
resource to the resource data; without interior mutability it would have been impossible to safely
modify peripherals in RTFMv1. Interior mutability is not required in RTFMv2 and removing it has
already been brought up in japaric/cortex-m-rtfm#36.

At this point I'm not sure what the "immutable" (&-) vs "mutable" (&mut-) buys us for
registers since even if you only have an "immutable" reference to a register (a) the peripheral
hardware (the actual electronics in the microcontroller chip) can mutate the register right under
your nose (the peripheral hardware effectively has a "mutable" reference to the register) and (b) a
read operation on the register performed on the processor can toggle the bits of the register,
thus mutating it, in some cases (I think I have seen this behavior before; I could be
misremembering).

re (a) you mentioned this

If several peripherals share a common clock source it would make sense to require a read reference
so they can't be changed while the peripheral is in use. This only holds as long as you need to use
a mutable reference to mutate the registers.

This makes sense in this particular case but in general the hardware may change the contents of a
register at any time so in general is not true that holding a &- reference a register means that it
can't change.

re (b) if you know that a read operation on a register modifies its contents should &mut self be
required on the read method? Would keeping it as &self be an issue in practice? Can we detect if
this will be the case for a particular register from the information in the SVD file? I think these
are important question to answer before implementing japaric/cortex-m-rtfm#36 and removing
interior mutability from registers (if we do both).


You had some questions as well:

Why do we generate these Peripherals that will safely trade a CriticalSection for register access?

Because it's memory safe to do so.

Is this not a threat to composability?

They are certainly racy but so are statically allocated atomics as shown in the second example
starting from the top.

What is the drawback of removing these constants?

Then, I think, you don't have any way to use peripherals unless you are using RTFM. This would not
be a problem for me but not everyone wants to use RTFM.

What is the drawback of making them unsafe to use?

It would signal the wrong message. They are as memory safe to use as atomics, even if racy.

What would be the drawback of the Peripheral struct containing mutable references instead of
non-mutable references?

You mean having a get_mut method on it that returns a "mutable" reference for the span of a
critical section? That would be unsound because it breaks Rust aliasing rules ("there can only exist
a single 'mutable' reference at any point in time."). See below:

// `free` needs to be tweaked a little for `get_mut` to work at all
interrupt::free(|cs: CriticalSection| {
    let usart1: &mut Usart1 = USART1.get_mut(&mut cs);

    interrupt::free(|cs: CriticalSection| {
        let aliased_usart1: &mut Usart1 = USART1.get_mut(&mut cs);

        // two mutable references to the same memory region; this is undefined behavior
    });
});

@kjetilkjeka
Copy link
Contributor Author

Let me try to refine my argument of why the const references to peripherals are unsafe.

I think it's important to remember that safety in Rust means memory safety and that race
conditions are considered safe in Rust. What's not considered safe are data races. The
observable effects of data races are undefined behavior (i.e. the compiler changes the behavior of
your program through (mis)"optimization") and torn reads and writes (these corrupt data at runtime).
The observable effect of race conditions is non-determinism, which mainly translates to logic bugs.

The nomicon lists 8 bullet points for what should be considered undefined behaviour. One of them is "Causing a data race" and another one is "Breaking the pointer aliasing rules".

As you point out the consts should not be made unsafe based on them beeing racy. But I think they do break the pointer aliasing rule. Let's look at an analog example based on "normal memory" rather than memory mapped IO. (Same example on playground)

extern crate rand;

use rand::Rng;

struct U32Cell<'a>(&'a mut u32);

fn main() {
    let a = junk_reference_generator();
    let b = a.0;

    *b = 0xAA;
}

fn junk_reference_generator() -> U32Cell<'static> {
    let mut rng = rand::thread_rng();
    let address = rng.gen::<usize>();
    U32Cell(unsafe{std::mem::transmute::<usize, &'static mut u32>(address)})
}

Creating this U32Cell would be sound if we knew that there could never exist mutable references to this location in memory. When it comes to memory, it's obvious that all bets are off since the compiler might generate references. When it comes to memory mapped IO we know that the compiler won't generate references, but we don't know what other crates will do.

This means that by making these consts safe we either:

  • Make assumptions about the libraries being used by the consumer of the peripheral library.
  • Potentially breaking aliasing rules.

Since we cannot make assumptions about libraries out of our control (as this would break composability) we're actually (potentially) breaking the pointer aliasing rules by exposing these consts safely.

It's, of course, the argument that "No one should safely hand out mutable references to memory mapped registers when they don't know if others will do the same". But this could easily be extended to "No one should safely hand out references to memory mapped registers when they don't know others will hand out mutable references to registers", reaching a contradiction.


I like your idea of turning the global singleton into a scoped singleton. But I won't be able to think through everything and write up an intelligent comment tonight. So I will rather save it for one of the upcoming days. But I will comment one more thing tonight

Why do we generate these Peripherals that will safely trade a CriticalSection for register access?

Because it's memory safe to do so.

Let's overlook the fact that it's (allegedly) breaking the aliasing rules. If I were writing a TCP wrapper that had two equivalent send methods, As long as only one was used everything would work fine, but when they were used together there would be a chance of deadlocking (depending on racy stuff). It would be totally safe to expose such interface. Wouldn't it still be a bad idea?

@pftbest
Copy link
Contributor

pftbest commented Oct 7, 2017

I think they do break the pointer aliasing rule.

No, they are not. To explain it, it's better to read language reference, not a nomicon

https://doc.rust-lang.org/reference/behavior-considered-undefined.html

It has a precise definition on what is allowed and what is not. For example

Creating this U32Cell would be sound if we knew that there could never exist mutable references to this location in memory.

No it would be unsound in any case, because your example is breaking rules 5 and 6:

  • &mut T and &T follow LLVM’s scoped noalias model, except if the &T contains an UnsafeCell. Unsafe code must not violate these aliasing guarantees.
  • Mutating non-mutable data (that is, data reached through a shared reference or data owned by a let binding), unless that data is contained within an UnsafeCell.

Also there is a note in the docs:

The UnsafeCell type is the only legal way to obtain aliasable data that is considered mutable. In general, transmuting an &T type into an &mut T is considered undefined behavior.

So in a nutshell, you are not allowed to have more than one &mut during execution of a function, but you are allowed to mutate data using non mutable (shared) reference if your data is inside UnsafeCell.

Our registers all have UnsafeCell inside, thats why it is OK to mutate them using shared reference. That is why our current model is sound.

@kjetilkjeka
Copy link
Contributor Author

Thanks for making the UD and "safe rust" more clear to me @pftbest. There's just one more thing I don't understand, that is:

Doesn't the argument of exposing const peripheral in an UnsafeCell rely on that no other library will expose the memory space not in an UnsafeCell? Would it not make sense to expose a config like register that never will change "on it's own" without using an UnsafeCell?

@hannobraun
Copy link
Member

@kjetilkjeka

If several peripherals share a common clock source it would make sense to require a read reference so they can't be changed while the peripheral is in use.

I think modelling our types in the way you describe is highly desirable, but I really don't see how this could be achieved on the register level (see @japaric's comment about the hardware changing registers under our nose).

I think this should be left to a higher-level layer, maybe an implementation of embedded-hal. I think at a higher level, you could effectively use &mut self and lifetimes to prevent invalid usage patterns. And with @japaric's proposal, we could even be sure that no other code is messing with our API's guarantees (as long as nothing goes rogue and accesses peripherals using the unsafe function).

@japaric

Thoughts on the above proposal?

This looks very good to me. I really like how you can use the unsafe function for zero-cost access in your application, while still being protected if some library tries to access the peripheral behind your back.

This follows from the "must be called once" requirement, but maybe it should be made clear in the documentation that the unsafe function is exclusively for use by applications or application-controlling frameworks, never for library code.

One caveat: I've only taken a cursory glance at RTFM so far. I think I fully understand your proposal, but maybe I'm missing some implication.

a read operation on the register performed on the processor can toggle the bits of the register, thus mutating it, in some cases (I think I have seen this behavior before; I could be misremembering).

I've definitely seen this. The LPC82x UART error flags clear on read (see user manual, section 13.6.3).

@kjetilkjeka
Copy link
Contributor Author

@japaric, I like your "non-global singleton" idea.

If I've understood things correctly, it's assumed that libraries generated with svd2rust will be the only way people are going to access hardware from rust. If someone were to make a "competing" library with other semantics then this singleton might not be a singleton after all. This will be the case as long as memory mapped IO is not handled in the language itself. The end game should probably be to have some sort of register allocation onto some address space that is compile-time guaranteed to only happen once for each compilation. To do this (less safely) in runtime, for now, totally makes sense.

At this point I'm not sure what the "immutable" (&-) vs "mutable" (&mut-) buys us for
registers since even if you only have an "immutable" reference to a register (a) the peripheral
hardware (the actual electronics in the microcontroller chip) can mutate the register right under
your nose (the peripheral hardware effectively has a "mutable" reference to the register) and (b) a
read operation on the register performed on the processor can toggle the bits of the register,
thus mutating it, in some cases (I think I have seen this behavior before; I could be
misremembering).

This is the case for some registers. On the other side, you have registers that can only be mutated by CPU (not peripheral). For these, it might be valuable to have a &mut reference. It doesn't make sense to throw away possible guarantees that a config registers can't be mutated by library code because a data register can be mutated by the peripheral.

It's also known how peripherals can mutate registers, making it possible to write drivers on top of as long as you know that you're the only part of code that have access to the registers.

If you need interior mutability it won't be a problem to introduce this on a layer above the code generated with svd2rust.


re (b) if you know that a read operation on a register modifies its contents should &mut self be
required on the read method? Would keeping it as &self be an issue in practice? Can we detect if
this will be the case for a particular register from the information in the SVD file? I think these
are important question to answer before implementing japaric/cortex-m-rtfm#36 and removing
interior mutability from registers (if we do both).

What I'm currently thinking:

if you know that a read operation on a register modifies its contents should &mut self be
required on the read method?

yes!

Would keeping it as &self be an issue in practice?

It would probably work fine since most people only use one driver at the time for their peripherals anyway. Perhaps it could let some bugs through in the cases of peripherals used for different drivers (GPIO, Power, Clock gating). It would be better to just let some reads mutate without a mutable reference than letting all writes do it as well.

Can we detect if this will be the case for a particular register from the information in the SVD file?

In theory, yes! The svd specification requires readAction to be set if there are side effects upon read "If not set, the register is not modified."

This is definitely going to be the case in practice as well, as we all know how good chip manufacturers are to write up to spec SVD 🙄


This also means that your driver would now be able to hold state and preserve it across task runs.
Yay!

This is not only a benefit for RTFM, but rather all rust code targetting embedded as it's enabling composability.


What is the drawback of removing these constants?

Then, I think, you don't have any way to use peripherals unless you are using RTFM. This would not
be a problem for me but not everyone wants to use RTFM.

RTFM doesn't have monopoly on macros calling peripheral = unsafe{ Peripherals:all() };, something similar could be generated from svd2rust. I wouldn't even mind calling it from the main function manually as long as it meant that other code couldn't mutate config registers safely.


If several peripherals share a common clock source it would make sense to require a read reference so they can't be changed while the peripheral is in use.

I think modelling our types in the way you describe is highly desirable, but I really don't see how this could be achieved on the register level (see @japaric's comment about the hardware changing registers under our nose).

I think this should be left to a higher-level layer, maybe an implementation of embedded-hal. I think at a higher level, you could effectively use &mut self and lifetimes to prevent invalid usage patterns. And with @japaric's proposal, we could even be sure that no other code is messing with our API's guarantees (as long as nothing goes rogue and accesses peripherals using the unsafe function).

@hannobraun, this is exactly what I'm talking about. The problem is that with the current code generated from svd2rust code can safely mess up the module settings and it is thus impossible to construct an interface with such guarantees.

@pftbest
Copy link
Contributor

pftbest commented Oct 13, 2017

@kjetilkjeka Sorry, but I don't see where would you get the &mut reference from, and how do you propose to manage it. Your argument with a "competing" library gets even more severe when we have &mut references. Having two libraries that both use UnsafeCell to expose the same registers is safe and sound, but when one of the libraries creates a &mut reference, this is suddenly UB and is very dangerous.

Also even when there is only one library present, there are still many corner cases. How would you share the same register between two drivers? Suppose we have i2c peripheral and spi peripheral and their clocks are in the same clock control register, how would it work? How would you share a &mut reference between interrupts and the main thread?

There is also a danger for users that might want to do more than a library has provided. For example we have an SPI library that consumes &mut reference to a whole SPI peripheral. And inside this peripheral there is one register that library doesn't know about, maybe because it doesn't support this functionality yet. In the current system the user can just modify a register without any issues, but if we have a &mut reference inside SPI library, the only way to do this would be modifying the library itself. Writing some unsafe code to access this register in a main program would be incorrect and cause UB. Some people may not be aware of that danger.

@kjetilkjeka
Copy link
Contributor Author

kjetilkjeka commented Oct 14, 2017

@kjetilkjeka Sorry, but I don't see where would you get the &mut reference from, and how do you propose to manage it. Your argument with a "competing" library gets even more severe when we have &mut references. Having two libraries that both use UnsafeCell to expose the same registers is safe and sound, but when one of the libraries creates a &mut reference, this is suddenly UB and is very dangerous.

Yes, handing out &mut references to registers is obviously unsafe. And using UnsafeCell is obviously somewhat better, it is (to my understanding) strictly speaking still unsafe.

My argument is: If it's ok for svd2rust to hand out references under the assumption that it's the only library doing it, then it must be ok for other libraries as well. And if you're handing out references to registers under the assumption that you're the only one doing it, you might as well hand out &mut references. If you safely want to hand out references you need to do a check globally to ensure that this is safe. Since a library never can check anything globally (due to being a local component) it "can't" be done safely.

Well, it actually can be done safely. But it would require global analysis. This can be done in the main function similarly to what rtfm does. Or preferably by having some compiler plugin handing out these references 0 or 1 times. (don't ask me how this would work, I'm just dreaming)

If constructing the &mutreference was unsafe, and all other ways to construct a reference (either with unsafe cell or not) was unsafe as well, a library wouldn't be able to do it (as ensuring this is safe needs a global analysis and a library is by nature local). So when I'm constructing my &mut reference in the beginning of the main function (by using an unsafe block) I can manually verify that the aliasing rule is upheld.

Also even when there is only one library present, there are still many corner cases. How would you share the same register between two drivers? Suppose we have i2c peripheral and spi peripheral and their clocks are in the same clock control register, how would it work? How would you share a &mut reference between interrupts and the main thread?

Most of the time they will only need non-mutable references after the initialization is done. This means that I can do the init and hand out & references.

If several drivers needs &mut references interior mutability needs to be introduced on a level over the generated code. If several threads/contexts need the reference we will need to give it some wrapper that ensures it will be Sync as well. (Maybe we "need" to construct more wrapper types that will play well with the peripheral/embedded case but this is possible.)

In your specific example, I would assume that the clock control driver handed out some & reference containing an interior mutable way to access this register for both i2c and spi peripheral.

There is also a danger for users that might want to do more than a library has provided. For example we have an SPI library that consumes &mut reference to a whole SPI peripheral. And inside this peripheral there is one register that library doesn't know about, maybe because it doesn't support this functionality yet. In the current system the user can just modify a register without any issues, but if we have a &mut reference inside SPI library, the only way to do this would be modifying the library itself. Writing some unsafe code to access this register in a main program would be incorrect and cause UB. Some people may not be aware of that danger.

I wouldn't say that this is the worst price to pay. Unless you're doing something mega hacky you should probably fork the driver and add the feature anyway. Alternatively, if it's a config (and the library doesn't clear registers), you could just set it before handing off the &mut. As long as you need an unsafe block to cause UD I'm fine with it.

@pftbest
Copy link
Contributor

pftbest commented Oct 14, 2017

My argument is: If it's ok for svd2rust to hand out references under the assumption that it's the only library doing it

I don't think there is such assumption. You can have two or more copies of svd2rust generated peripherals in your application and it would work fine. There is nothing special in svd2rust that requires a guarantee that there is only one copy of it.

@kjetilkjeka
Copy link
Contributor Author

kjetilkjeka commented Oct 14, 2017

My argument is: If it's ok for svd2rust to hand out references under the assumption that it's the only library doing it

I don't think there is such assumption. You can have two or more copies of svd2rust generated peripherals in your application and it would work fine. There is nothing special in svd2rust that requires a guarantee that there is only one copy of it.

You're right, the assumption is rather "No other libraries hands out references to registers without putting them in an UnsafeCell".

How do we know that is the case? I guess the almost good enough answer is "This can interfere with other libraries. And they shouldn't do that without doing a global analysis first. But they can't do a global analysis since they're libraries. So they shouldn't do it at all".

But the same argument is applicable with handing out references inside an UnsafeCell as well. You should do the global analysis to see that no one hands out these references without putting them in an UnsafeCell. But we can't do this analysis due to being a library. So we can't say its safe.

It's like having a closed down one-way street. You can drive down it in the night because there are not any cops around. And you're totally fine even if other people drive down it as well. Except when a guy wants to drive it the wrong way since the streets closed down and there shouldn't be any traffic anyway. I for sure know who is "the bigger idiot" but it doesn't make the guy driving down a closed down street right. Any of these actions could only be made safe if the driver had the ability to do a global analysis (see every other cars intention)

@pftbest
Copy link
Contributor

pftbest commented Oct 14, 2017

the assumption is rather "No other libraries hands out references to registers without putting them in an UnsafeCell".

Well, I still think that this is also not the case here. I think svd2rust would work correctly even if &mut library present. svd2rust doesn't care about other libraries, it would be compiled correctly in any case.
But the converse is not true, &mut library would be miscompiled when svd2rust is present. So only &mut library needs a guarantee that it is the only one.

@kjetilkjeka
Copy link
Contributor Author

Well, I still think that this is also not the case here. I think svd2rust would work correctly even if &mut library present. svd2rust doesn't care about other libraries, it would be compiled correctly in any case.
But the converse is not true, &mut library would be miscompiled when svd2rust is present. So only &mut library needs a guarantee that it is the only one.

I'm partially agreeing. I'm also partially thinking that this is (in a more subtle way) the same as handing out a pointer of an UnsafeCell wrapped value placed right above the top of the stack and blaming the code that pushed to the stack when something bad happens. (yes i know it's not entirely the same)

@hannobraun
Copy link
Member

@kjetilkjeka

this is exactly what I'm talking about. The problem is that with the current code generated from svd2rust code can safely mess up the module settings and it is thus impossible to construct an interface with such guarantees.

Honestly, I'm perfectly fine with an API that just promises to work correctly, as long as the user makes sure nothing else touches the peripheral.

@kjetilkjeka
Copy link
Contributor Author

@hannobraun

Honestly, I'm perfectly fine with an API that just promises to work correctly, as long as the user makes sure nothing else touches the peripheral.

I'm inclined to agree. But my main problem is that other libraries might mess things up as well.

A real-life example:
I'm currently working on a library for a protocol running on top of can called uavcan. The can driver needs to work in a special way to be compatible with this uavcan library, so a specialized or adapted can driver will need to be made as well. It would perhaps be very practical to "invisibly" configure clock gating and pll from within this driver, and it would work fine in isolation. But if other drivers that is in use were to use the same they might configure the pll completely differently and only the last configured driver would work.

On the other hand, wouldn't be amazing if I could configure the pll first and give a read-only reference (without interior mutability) to the can driver. It would both know how the pll was configured and know that as long as it held this reference no one could modify the pll.

It's not like it's impossible to write embedded systems without these features. We've (probably) all written embedded systems in C. But I see this as an opportunity to get some "bang for my bucks" using Rust. And I don't see being able to mutate everything in a critical section as a specially good idea or valuable feature.

@hannobraun
Copy link
Member

@kjetilkjeka
You don't have to convince me. I'm all on board with your vision here. However, I just don't see a way to nail all of this down on the lowest level. Another library can always mess us up by being a bad citizen. Even if we had a Rust compiler from the future that would know all about memory-mapped I/O, that still wouldn't help us when we link to a C library.

It might be possible to combine static analysis of the Rust source code with static analysis of the compiled program and use device-specific knowledge about the address space, to determine if there are any rogue actors. If such a tool existed, that would be great, but it seems more like a long-term vision, if anything.

All I'm saying is this:
We can achieve those guarantees right now using higher-level code, as long as the user holds up their end.

On the other hand, wouldn't be amazing if I could configure the pll first and give a read-only reference (without interior mutability) to the can driver. It would both know how the pll was configured and know that as long as it held this reference no one could modify the pll.

Why does it need to be a a reference to the PLL register(s) though? Write a driver for the PLL. Tell the user (via documentation) that only that driver is allowed to access the PLL. Pass references to the driver (mutable or immutable, as required) to your other drivers.

Of course, those drivers need to cooperate for this to work, and I think embedded-hal is a good first step here. If we can do more to help on the svd2rust level (like @japaric suggested above), I'm all for it.

@kjetilkjeka
Copy link
Contributor Author

@hannobraun

You don't have to convince me. I'm all on board with your vision here. However, I just don't see a way to nail all of this down on the lowest level. Another library can always mess us up by being a bad citizen. Even if we had a Rust compiler from the future that would know all about memory-mapped I/O, that still wouldn't help us when we link to a C library.

Rust is pretty young language and on the infant stage for use in embedded. Isn't it now we should ask ourself what features that would be constructive to have for development close to hardware?

When linking to a c library all bets are off. That's why calling c functions are "unsafe". The library author for this code needs to make sure it's safe. Any library can also mess anything up by calling unsafe and provoke UB.

So let's not nail everything down to the lowest level. But let's not accept status quo, discuss this thoroughly and try to make it even better than it currently is. Jorge Aparicio suggested a way to remove the static peripheral definitions that create a more composable interface that will make it harder to create unwanted race conditions. This is great! All I'm asking is, what if we had a compiler plugin that could hand out references, would this be 1: possible or impossible, 2: better or worse?

Why does it need to be a reference to the PLL register(s) though? Write a driver for the PLL. Tell the user (via documentation) that only that driver is allowed to access the PLL. Pass references to the driver (mutable or immutable, as required) to your other drivers.

It doesn't have to be a reference. But for this to work the library writers need to agree that there needs to be some control of who is allowed to do what. I think having a reference is an intuitive way to express "who is allowed to do what". The compile-time checking is also a nice feature.

Handing out mutable references in critical sections does the exact opposite. It sends the message that you're allowed to do everything in the world(like reconfiguring the pll) as long as you do it in a critical section.

Using a reference is just my suggestion to how one could use the ownership model to disallow reconfiguration. If you think that using a reference will make things worse, please explain. If you think there's a better way to solve the problem, please explain. If you think this is just an inconvenience and not really a problem, and would prefer to not fix the inconvenience, please explain.

Removing the static peripheral definitions is probably of higher importance than the move away from interior mutability. This will (hopefully) force people writing drivers to at least "ask for permission" before changing it.

Of course, those drivers need to cooperate for this to work, and I think embedded-hal is a good first step here. If we can do more to help on the svd2rust level (like @japaric suggested above), I'm all for it.

I support the idea of cooperating against making things consistent and nice on the driver level. But I also think we should improve things when possible on the register/peripheral level. Someone is going to have to write the drivers implementing the embedded-hal traits, and they're going to appreciate this effort. Sometimes the traits in embedded-hal are not going to be general enough and then people will have to write other drivers.

My point is that just because things are consistent on the driver level we shouldn't forget to make things nice on the lower levels as well.

@hannobraun
Copy link
Member

Rust is pretty young language and on the infant stage for use in embedded. Isn't it now we should ask ourself what features that would be constructive to have for development close to hardware?

I fully agree. I never meant to inhibit discussion, and I apologize if it came across that way. All I meant to express is that there exists a solution that is, in my opinion, "good enough for now". That doesn't mean we shouldn't think about better solutions.

Using a reference is just my suggestion to how one could use the ownership model to disallow reconfiguration. If you think that using a reference will make things worse, please explain. If you think there's a better way to solve the problem, please explain. If you think this is just an inconvenience and not really a problem, and would prefer to not fix the inconvenience, please explain.

I think there has been a misunderstanding at some point. I wasn't arguing at all against using the ownership model. I was merely pointing out that you can use the ownership model and references on the HAL/driver layer (whatever you want to call it), even if the registeres themselves use interior mutability.

My point is that just because things are consistent on the driver level we shouldn't forget to make things nice on the lower levels as well.

I agree.

@japaric
Copy link
Member

japaric commented Oct 17, 2017

(Sorry that I've been away from this discussion for several days)

@hannobraun

I really don't see how this could be achieved on the register level

I also want to note another problem: even if the hardware doesn't modify some registers in parallel
to your program (e.g. because of changes in the environment) you can still break the "shared
references (&-) are immutable" through "spooky action at a distance": i.e. you modify register A
and that operation also mutates register B. An example of this are the BSRR and ODR registers of
the GPIO peripheral found in STM32 microcontrollers.

This follows from the "must be called once" requirement, but maybe it should be made clear in the
documentation that the unsafe function is exclusively for use by applications or
application-controlling frameworks, never for library code.

Definitively. Actually I didn't properly document the safety requirements of calling that unsafe
function. The requirement for safety is not only that it must be called once but it must also be
called before any call to their safe version. Thus the appropriate place to call such function is
right after the microcontroller boots and RAM (.bss / .data) is initialized.

I've definitely seen this. The LPC82x UART error flags clear on read (see user manual, section
13.6.3).

Thanks for providing an example.

@kjetilkjeka

If I've understood things correctly, it's assumed that libraries generated with svd2rust will be
the only way people are going to access hardware from rust. (..)

Indeed. That's a problem. You don't even need a competing library to run into this problem. This can
also happen if you use two different minor versions of a device crate (because you end linking to
two different crates). The different version problem can be fixed, I think, by making use of
symbols. Something like this (untested):

#[no_mangle] // important attribute
static mut USART1: bool = false;

pub fn usart1() -> Option<USART1> {
    // uses the `USART1` static as a guard
}

pub unsafe fn _usart1() -> USART1 {
    USART1 = true;

    USART1 { _0: () }
}

This way the USART1 static variable from v0.6.0 and v0.7.0 will collide at link time (because the
symbol name is the same) and you won't be able to use both libraries.

This unmangled symbol idea could be extended to competing libraries but it would require some sort
of agreement.

This will be the case as long as memory mapped IO is not handled in the language itself. The end
game should probably be to have some sort of register allocation onto some address space that is
compile-time guaranteed to only happen once for each compilation.

From what I have heard miri has, or is going to have (?), a mechanism to detect memory aliasing
when interpreting programs (kind of like a sanitizer). That might help here but miri would have to
learn about hardware stuff like interrupts, I suppose.

A language feature would be an ideal solution but I don't think this (peripheral / register block
aliasing) is that much of a problem in practice to warrant a whole language feature.

This is the case for some registers. On the other side, you have registers that can only be
mutated by CPU (not peripheral).

This still has problems. See the "spooky action at a distance" problem mentioned at the top.

For these, it might be valuable to have a &mut reference.

How would you model the "spooky action at a distance" phenomena using &mut- references?

Note that my main concern here is not being able to do this correctly, and in an automated
fashion. IMO, we should continue to provide the "you can mutate via a shared (&-) reference" on
all registers. In the future we might be able to move some registers to a "read through &- and
write / modify through &mut-" API if SVD files provide enough information to do so.

The svd specification requires readAction to be set if there are side effects upon read "If not
set, the register is not modified."

TIL. Unfortunately only 23 of 548 files in the SVD database are making use of this field. If you
apply this logic in svd2rust you'll end up thinking that all register reads are free of side effects
on the rest of devices, which is very likely not true in the remaining 525 cases.

@japaric
Copy link
Member

japaric commented Feb 7, 2018

I believe this has been fixed by the new singletons approach (cf. #158)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants