Rust applicability to small embedded codebase - getting discouraged

It's a bit of boilerplate, but it's possible to set up a static which is usable similarly to a global mutable static in C but still prevents reentrancy. The pattern would look something like

struct StaticFooManager(RwLock<FooManager>);
static FOO_MANAGER: LazyLock<StaticFooManager> = LazyLock::new(Default::default);

impl StaticFooManager {
    fn get(&self) -> impl '_ + Deref<Target=StaticFooManager> {
        self.0.try_read().unwrap()
    }

    fn get_mut(&self) -> impl '_ + DerefMut + Deref<Target=StaticFooManager> {
        self.0.try_write().unwrap()
    }
}

There is a (not-unsafe, but program correctness) danger to this, though: it'll cause a panic if you call get or get_mut while a get_mut is live, or get_mut while a get is live. Because of that, I wouldn't really recommend this.

An alternative (though one with similar pitfalls) is to consider if you can emulate thread locals on your target. The simplest way would be just to abort if used off the main thread / from an interrupt. (Or more likely, just consider the existence of interrupts as fundamentally unsafe and "just" don't access the emulated thread locals from the interrupt handler.)

Instead, it's probably much easier to have a single struct Globals which you create at the start of main and define any functions which need access to multiple parts of the global data as methods on Globals. You can and should of course still split the globals into parts with their own structs, and any functionality which only needs access to one or a small number can be methods on that struct or take references.

It's just generally easier to pass around references than to rely on globals in Rust. IIRC, there's even some embedded-friendly crates which offer a way to have zero-sized references to static memory. I don't recall what crates they are, though, or I'd link them. This also serves to help the code self-document, as it's then immediately apparent what functions can touch what globals. Bundling up multiple references into context super-references can also help prevent ending up with too many arguments.

1 Like

@VorfeedCanal
Overall, this is an excellent reply, thanks so much. I've been thinking about it more, and many of my thoughts are along the same lines as some of your observations. More below:

Interesting observation about the refactoring.

Yes, I'm anticipating that things will get better as time goes by, the growth of the language is actually quite astounding. Some of the features that impress me apparently didn't exist before 2018.

I went back and reread some of the Rust Embedded book, and it is much more cognizant of the unique issues of embedded programming, including the frequent use of completely heapless apps like ours (which is BTW why we have so many static mutable structs - no hazards with new/malloc and delete/free) Over time, Rust may become even more friendly to that audience, especially if more small embedded code authors embrace it.

The question is whether we are "there yet", and at the moment I'm inclined to invest more time in determining the answer.

That's the line of thinking I'm now pursuing. And can the patterns we use to do this also be reused, to avoid duplication.

So that's exactly what I'm trying to work out now. And one motivation is to separate the dangerous code from the other code in terms of who authors that dangerous code. Those with that skill set are more difficult to find. In a larger team, those without that skill set would be guided towards the safe practices outside of the danger areas. The team will grow, and compartmentalizing the dangerous code will have greater and greater value going forward. That's a good selling point for Rust, justifying the delays required of master the appropriate Rust plumbing up front.

1 Like

Something that's been in the back of my head for a while is some sort of "thread coloring" system where statics could be safely mutably accessed by code that's "of the same color", with the assumption that you could only run one thread with that color (at least, at a time), and colored code can only run on that thread. The case I was thinking of was in games where you might want a input thread, a physics thread, a render (submit) thread then a bunch of workers.

Making these have access to mutable globals would be nice in some sense, but the main issue is that I can't sufficiently prove to myself that it would actually be an improvement over just passing the mutable reference to a type, as others above have described.

2 Likes

“Coloring” entire threads wouldn't be sufficient to satisfy &mut's non-aliasing guarantee because a recursive function could obtain overlapping borrows (reentrancy). You would need to label all the code run while holding &mut of the static as not having that color, and to make this usable you'd need one “color” per static (or even more elaborately, per field of the static, like the usual rules of independently borrowing parts of structs).

Alternatively, you could just never take a reference of the global. Since it's global data, you don't need to take a reference since anybody who needs to work with it knows where it is. Then similarly to working with #[repr(packed)], to use it you make a temporary copy in your stack space.

This isn't simple to do in Rust since many operations will implicitly introduce a reference (and it's unclear in some cases whether built-in operations semantically make a temporary reference (e.g. to call the trait version) or not) but it's reasonably common style for this style of C code.

It'd definitely be interesting to have a mode in rustc that disabled all autoref/autoderef behavior, but not really worth the implementation effort[1].


  1. With the one exception being when working with pointers and where not creating a reference is paramount. ↩︎

OK, so here's the thing.

  1. Rust is definitely a more sophisticated language than C (and more principled than C++) is, but it still gives you all the low-level control if you ask for it. So you can pretend you are in C, and continue using unsafe, static mut, raw pointers, and uninitialized memory left and right, whether or not it is warranted. People will frown upon you if you do that, but it is possible, and there is a very straightforward if not trivial translation from C to unsafe Rust.

    Having written compilers before, I would risk saying that mechanical translation of C to non-idiomatic unsafe Rust wouldn't even need full type checking. IIRC it could be done in a purely syntactic way (except for the fact that you need at least partial type checking in order to even parse C).

  2. Of course, basically everything in that code would (have to) be unsafe, because the many unsafeties of C is exactly what Rust is trying to prevent. What you are observing here is that Rust is not C. Rust is not C++, or Java, or Python, or JavaScript, or C#, or Haskell. Rust is Rust. And trying to write any other language in Rust, while definitely possible, won't ever be pretty.

  3. What you have to understand, though, is that we, humans, don't mean "mechanical translation to an ugly subset" when we talk about transitioning a code base from one language to another. (Maybe some of us do, and that's for the worse.) The point of using a different language is not to change the syntax of pointers from int *p to p: *mut int or to change the name of 32-bit unsgined ints from uint32_t to u32. To a first approximation, nobody cares about that (again, important exceptions apply, since C's syntax is context-dependent, so it's notoriously harder to parse than Rust's).

    Instead, the point of using a different language is, or should be, to harvest the benefits of that language and ecosystem. This should entail embracing the idioms of the new ecosystem, rather than trying to force other idioms or just downright outdated practice onto the new codebase. What you are essentially saying is: "Doctor, I'm writing C in Rust, and it hurts." To which the doctor will reply: "Then don't write C in Rust, and it won't hurt!"

  4. I will go one step further and say that the aforementioned practices around global mutable state are not considered decent even in C. Unfortunately, the embedded programming world has a tendency of declaring that "it works", and people then think that they can stop caring about good practice altogether, and just write the shortest, ugliest code that will happen to generate the machine code they think is (half-)correct. This, however, is not an intrinsic necessity even in embedded or low-resource systems.

    I have written my own hardware abstraction layer before, exactly because I was dissatisfied with the quality of some other HALs. I quickly ripgrepped the code base, and I found only 5 statics in the entire thing. Taking a closer look, only a single one of them is necessary (because it's the interrupt handler vector, which has to be global state, given that interrupts can't receive parameters on the AVR), and the rest of them would be trivial to move to a struct and be replaced with properly scoped locals.

    Apart from marginal writing convenience, there is no real benefit in extensively relying on mutable global state. On the contrary, it's harmful in the long run, because it hurts maintainability, and causes bugs to sneak in over the years. That is simply the nature of shared global state. The fact that Rust points out that global mutable state is highly unsafe is not Rust's fault – it's a feature, not a bug. Global mutable state being dangerous is solely the fault of global mutable state itself, and instead of basically complaining that Rust doesn't let you shoot yourself in the foot, you should look at and fix the problems it is trying to point out.

19 Likes

There's a tool that does the translation out there already:

It uses clang to parse the C, because while not as bad as C++, parsing C is still annoying, and requires evaluating preprocessor directives.

5 Likes

@H2CO3 So what are you proposing, using heap based dynamic memory allocation where it basically is not needed? You seem to portray the decision to use statically allocated mutable state in small embedded systems as some sort of intellectual laziness. When in fact the reason is always to avoid heap based allocation, and the attendant risks:

  • Memory leaks.
  • Accidentally dereferencing null pointers.
  • Running out of memory because you don't know that until runtime. Particularly important in a bare metal small embedded application.

Knowing at compile time how all your memory is going to be used (except for stack depth) is practical, which is why I've been doing this for decades. We aren't talking about writing a Linux OS here.

I've got no problem with trying to think through how to apply Rust's safe practices to this environment, and I'm going to provide a simple example here shortly, for all you Rustafarians to help with, because I'm having trouble thinking through it. But asserting that the practice of using static mutable state in all circumstances is just bad programming practice, that to me is just silly.

No, he's proposing to replace the global static mutable state with shared state initiated in the scope of the main function and (properly) passed down to modules and functions which need that state.

Rust's module system and visibility model lend themselves very well to that style.

3 Likes

How is that not static mutable state if not allocated on the heap? I guess I'm missing something basic here. Maybe you are talking about allocating on the stack? But if so, the we don't know how much stack space we use until runtime, back to the same hazard.
@erelde

Are you confusing static memory, stack and heap ?

1 Like

Maybe I am. You tell me. Is there a way to declare a variable (say it's a struct) in Rust that will not be on the heap or the stack, but will also not have a static lifetime? And in such a way that you can then see that memory in the .map file, so that you know the compiler knew about all your memory usage at compile time? (Other than temporary small stack storage). @erelde

No. I'm just proposing that it be local and passed around wherever its needed, instead of being global.

But you just said that you used a static before, which implies that its size must be known at compile-time. I don't think either C or C++ supports dynamically-sized statics, so this is not something that you can do in C or C++ but not in Rust. But then this also means that you can just move the entire thing to the stack.

@H2CO3 There is a fundamental disconnect here, and I'm trying to figure out what it is.

To me, declaring something static means that the memory will be allocated at compile time, generally in the same space as where the program code itself is allocated. That allocation will be visible by looking at the map file that can be generated by the linker. Is this not correct? Because if isn't then static means something very different than the general definition of static memory allocation.

The principal advantage of using that static storage for almost all significant memory usage in a small embedded app is that you know at compile time whether there is enough memory. There are other advantages, already stated in this thread.

You propose it be "local", but in most languages, "local" means stack allocated. So is Rust different? I sure didn't think so.

I'm not sure why you bring up dynamically sized statics, it seems irrelevant to the discussion, hence some of my confusion.

And after going to all the trouble to support a statically allocated memory model, for the reasons previously stated, why would one want to "move it to the stack"?

You brought it up, here:

I was falsifying this misconception. If you are paranoid about running out of memory on an embedded system, then you likely also avoid recursion and indirect calls. This in turn means that your call graph is a tree, and thus the maximally used amount of stack space can be inferred at compile time by looking at the call tree.

The only exception would be if you used dynamically-sized locals (which are officially possible in C and unofficially supported by most major C++ compilers, not by Rust though). But given that you are using statics, it is not possible that you would want to use dynamically-sized locals either, hence the above exception does not apply, and we are back at the situation where we can bound the stack space at compile time.

As I and others have explained several times in this thread, the reason for that would be to improve the structure and memory safety of the code.

This is a non-issue in Rust thanks to the borrow checker (unless you're writing unsafe code).

This is possible, but still quite unlikely. If you're willing to statically allocate all your data, then you're willing to accept a hard upper bound on the size of any used data structures. You can use the same bound with local variables.

This is harder to guard against. I'd say the way to prevent it in Rust is the same as in C: no recursion, no unbounded iteration, fixed-size storages, known upper bounds on stack usage (which can be computed mechanically if there is no recursion, no unbounded iteration and no unbounded dynamic storage, including alloca).

The primary difference between a mutable static and a forever-living local variable is that a static can be accessed anytime from anywhere. The local can only be accessed by the functions which get an explicit reference to it, and you can control precisely which kind of access you are providing (shared read-only, exclusive mutable, shared mutable via a runtime lock etc).

1 Like

I do not understand how a memory model with more dynamic memory allocation, either stack based or heap based, will improve the memory safety of the code. As an absurd but useful observation, if all data were static there would be no "lifetime" hazards with regards to memory!

IMO, you've got it backwards. The reason that one has dynamic memory allocation in the first place is to be able to reuse memory, with two different paradigms for heap and stack. If there were infinite fast memory, there would be no need to reclaim it. Language design would look completely different.

From my vantage point, Rust employs a lot of innovate techniques in order to deal with this reality. Dynamic allocation is necessary, so Rust tries to make it safe. Other languages do this with garbage collectors where the design tradeoffs are appropriate. And in C/C++ it's completely up to the programmer to manage memory responsibly, which is why there is a need for a language like Rust in the first place.

But in a small embedded system, it is often possible to largely not need to reclaim memory at all. So you have no heap, and stack based data is very small and just matches up with function call model. It is a totally sane thing to do, which is why it is done so often in embedded systems! If you don't think so, then we just disagree. But apparently some Rust programmers do think so, otherwise there would be no heapless crate.

Now where I do see the point of Rust's disdain for static mutable data is in the thread safety area. And that's where I'm trying to figure out how to work things out to be Rust like. I'll post a simple example in a separate thread so I can figure out how to be Rust like with some help from all the Rustafarians here. If that works out well, I'll be onboard with Rust for this application space. Rust has many wonderful features. But one rather large feature of Rust that has very little value in this application space is the management of "lifetimes". Because all the important data has a lifetime of "forever" while the application is running!

Indeed it's absurd and I think it's the main issue here.

You are, basically, saying: banks are doing it wrong

  • They are spending money on walls
  • They are paying for complicated locks
  • They hire these stupid security guys
  • Complex procedures

Let's do better, safer:

  • Put all the gold in the easy-to-access place
  • Ensure that there are no guards
  • Remove all the locks and demolish walls

They you would have no problems with lifetimes, no lost keys and everything would be very safe. Right?

Yet, somehow, none of banks ever follow that route. I wonder why.

The disdain for the static mutable data comes from very simple observation: there are no protections against… against anything really.

Any piece of you program can change any piece of data. No protocols (like “you can only change field which shows much much data is available only after data was actually added to some buffer”), no limitations.

Pile of gold with easy access for everyone.

No, it's not. Allocated memory may stay around forever. That's fine, Rust supports it with unsafe, not problem. But actual valid, useful, correct data? Nope. It comes and goes. Even in the embedded app.

What part of buffer is valid? Which buffer is open for drawing and which is frozen because hardware writes data there with DMA? Can we toggle pin A before pin B or is it forbidden?

Embedded space is chock-full to the brim with filetimes.

It really sounds as if you think lifetimes only exist to allow one to use dynamic memory without GC.

Nope. Not even close. Not even remotely close. Lifetimes (or, formally, affine type system, as it's known in the academic literature) was invented to handle specifically things other than memory, to control hardware. To track validity of data, not memory allocations.

Rust developers just noticed that if you have an affine type system then you can use it to control memory, too.

But it was originally invented specifically to safely control hardware which is, kinda, why most embedded systems exist in the first place.

The goal of lifetimes is not to ensure memory safety, it's just a side-story Rust embraced. Lifetimes are a tool which is designed to prevent access to the data (or hardware resource) when it's not valid to access said hardware resource. And, once again, they were invented and implemented in languages which used GC for memory management initially.

Heck, even Rust had GC in the beginning! Lifetimes weren't added to Rust to manage memory! They were supposed to manage other things.

It was just, eventually, found out that they are flexible enough to manage memory, too. But that wasn't the motivation for them!

1 Like

This is not a useful observation; it's missing the point. The problem with shared mutable state is not restricted to lifetime issues; the other big thing is race conditions (single-threaded race conditions included, e.g. iterator invalidation).

I do not.

First off, I'm still not suggesting you to use dynamic allocation; you are putting words into my mouth.

Second, my claim is not that you should use local rather than global state because it gives you infinite memory, or more memory, or more performant memory access. This has also nothing to do with stack vs heap vs static space. My claim is that if you use local variables rather than global ones, then you will have better code architecture that avoids most of the race conditions that also cause memory corruption. Whether these variables are stored in one segment of the memory or another doesn't matter in this regard, because all that matters is that they have bounded, scoped access, rather than everyone being allowed to observe and mutate them without restrictions.

I am still not saying that you should use heap allocation if you don't need it. I have no idea where you are getting this from, but it's not my point. Replacing globals with locals doesn't automatically involve heap allocation, either.

This is simply not true, or at least missing the point. Again, you seem to be thinking that the only thing Rust does is checks liftimes, and the only way to cause memory corruption is a use-after-free. You completely neglect the whole discussion around shared mutability, which is a separate concept, and is of primary importance when reasoning about local and global state. Even if your data is allowed to live forever (i.e., all of it is or candidate for being static), it's still important to disallow data races, because data races are in themselves cause memory corruption and errors, no matter where the racy data resides.

3 Likes

The first part of that sentence contradict the 2nd part. Race conditions are one form of “lifetime issues”.

Note that it's not “local” vs “global”. Rather it's “variables with lifetime tracking” vs “variables without lifetime tracking”.

You can put all your data into global variables and track lifetimes with zero-sized variables. They don't generate any code (creation, move and destruction of zero-sized variable doesn't need any code) yet still can be used to support safety invariants.

1 Like