Rust applicability to small embedded codebase - getting discouraged

I've been looking for a better language than C/C++ for years for small embedded projects. Recently I found some time to finally look at Rust. Initially, I was very excited, it's a fresh take and there are a whole lot of things about it I like. I read a lot of the Rust Embedded book, ran into the headless crate description, and thought "These Rust guys really get it" WRT to codebases which largely allocate data statically. We have no heap in our application, for instance.

But having spent a bit of time actually to reimplement some of our code in Rust, it's not turning out to be so easy. Perhaps an example will illustrate things better. Our codebase - like so many small embedded codebases - consists of modules that are almost always singleton "Managers" of defined functionality, with largely private state data and narrow external interfaces. Threading is kept as simple as possible, data hiding is used, and data ownership is carefully defined. A sample overly simple "manager" might be defined like this:

static struct TableEntry {  // static really means private
  float value = 10;
  int tickDelta = 0;
  uint8_t flags = 0xf0;
};

class FooManager {
  int state1 = 3;        // actually some enum
  float value1 = 3.14;
  uint8_t flags = 0xaa;  // actually something meaningful
  // etc etc
  TableEntry pingpong_buf[2][256];
  TableEntry* current_buf = pingpong_buf[0];

  // Public and Private Methods that operate on FooManager
  // ...
};

FooManager singleton_foo_manager;

Now actually I have a templated singleton class but that isn't necessary in this description. The point is that this is a very concisely defined statically allocated singleton class, not much code is needed.

When I try to replicate this in Rust, I run into a bunch of problems:

  1. C++ generates default constuctors for initializing the member values, based on the values specified in the struct/class definitions. Rust does not.
  2. Initialization of statically allocated array of arrays of structures is therefore childplay. In Rust it requires generating Traits, and it isn't real convenient for statically allocated data. I've tried. It's just a lot more code, and frankly I'm not sure exactly why it needs to be that way.
  3. If I try to macroize this for convenience, I'm probably going to run into situations where debugging the code is confusing because the code is generated at compile time.

Since this singleton pattern is pretty standard in our code, I'll need to go through this for every struct that needs to be initialized, with additional overhead if it needs to go into a static array.

So here's my question - am I missing something huge here? Because if not, it's feeling like Rust is creating inconveniences for this type of programming, even if it is providing incredible value in safety and functionality for larger applications (safety, generic collections, the whole Enum thing which is pretty cool, polymorphism through Traits, no more header files, a new build system, and a truly powerful Macro facility). I'd like to convince my embedded team that moving to Rust is a big win in both safety and convenience, but on the convenience score for small embedded apps like ours, I can't even convince myself.

But I want to be wrong!!

1 Like

derive-new crate provides macro to generate constructor for initializing the member values, based on the values specified in the struct/class definitions.

Array initialization should not requires generating traits. Can you share your last attempts in code? It's hard to know the actual problem without code.

1 Like

Unfortunately that library doesn't seem to have any option generate a const constructor, so it won't be readily usable for initializing statics.


@jandyman Here's my attempt at translating most of your C++ code into Rust (it compiles):

#[derive(Copy, Clone)]
struct TableEntry {
  value: f32,
  tick_delta: i32,
  flags: u8,
}

impl TableEntry {
    const fn new() -> Self {
        Self {
            value: 10.0,
            tick_delta: 0,
            flags: 0xf0,
        }
    }
}

pub struct FooManager {
  state1: i32,
  value1: f32,
  flags: u8,
  pingpong_buf: [[TableEntry; 256]; 2],
  current_pingpong_buf: usize, // an index not a pointer because structs are movable
}

impl FooManager {
    pub const fn new() -> Self {
        Self {
            state1: 3,
            value1: 3.14,
            flags: 0xaa,
            pingpong_buf: [[TableEntry::new(); 256]; 2],
            current_pingpong_buf: 0,
        }
    }
}

// TODO: this needs to be in a cell to be mutable
pub static SINGLETON_FOO_MANAGER: FooManager = FooManager::new();

I agree that it's not convenient that you have to write the default values separately from the struct declaration, but there are no traits involved in this case (actually can't be, since const trait usage isn't yet stable).

But it sounds like you have some further difficulties beyond this. Can you describe them in more detail (with example Rust code) so we can give more specific suggestions? It's much easier to say “ah, this Rust code will be better if…” than to suggest how to write good code in the abstract.

9 Likes

Ah right const would be mandatory for statics and arrays. I guess it's a good time to send a PR to the crate.

@kpreid Now that you mention it, another problem I had would go away if I didn't have to try to write a fancy macro to get the defaults. So if that problem were solved, then needing to write an explicit constructor for FooManager itself would also probably not be necessary, so a lot of the seemingly unnecessary code would go away and it would look a lot better (a lot less code). It would basically cut the code in half. And this struct is a minimalist example. As the amount of data goes up, the code duplication goes up too. And since this is a lot of what we do ......

The other thing you mention above that I encountered as I was starting to get frustrated by all this was the whole mutable thing. I don't really understand the cell comment, why it is not possible to just create a mutable Manager. That is, a Manager that can mutate itself, or allow mutation from outside. I ran into comments like "static mutable structs are dangerous". Some example code I saw required you to write "unsafe" to get mutable static data, which feels like "what am I doing wrong here". I kinda get that static
mutable structs are dangerous if you don't do proper thread protection, but static mutable structs are part and parcel of this kind of small embedded codebase. Maybe you could explain the "Rust way" in this area to me, and does it result in a bunch of extra code?

I know they're commonplace in embedded C codebases, but other than communication between your main code and an interrupt, static variables aren't strictly required for embedded.

In a lot of embedded Rust I've seen, they'll encapsulate that interrupt communication inside some sort of abstraction that handles things like re-entrancy and all that. For the rest of the code, you can treat it like any other Rust application where state is stored on the stack (e.g. imagine initializing all your managers at the top of main()) and you pass &mut references into functions that needs access to a manager.

Another thing to consider is that using a singleton manager-based architecture inherently implies shared mutation. Straight away, that's going to lead to more friction because the language forces you to handle synchronisation and re-entrancy (hence the cells and all that). You might want to look at other embedded applications written in Rust and see how they've structured things, because there's probably an easier way.

5 Likes

I don't really understand the cell comment, why it is not possible to just create a mutable Manager.

The problem here is that the Rust language is strongly oriented towards thread safety by default. In a program with threads, arbitrary mutation of global (static) variables is a recipe for UB and other bugs. So, Rust's defaults are that you don't get to have mutable globals without some kind of protection (like a Mutex, which is one kind of “cell” in the sense I mean).

In a program which is known to be single-threaded, you can a use non-thread-safe cell (after doing something unsafe to pretend it is thread safe, because there is no way to ask for an exception to the "statics must be Sync" rule), but you still have to watch out for reentrancy — a function in the middle of modifying the global calling another function that modifies the global. That's UB in rust, because the rule is that you can never have more than one &mut reference to the same place active at once.

So, if your embedded code is in fact single-threaded — you've found another way the language isn't perfect for your application. On the other hand, if it is multi-threaded, then you almost certainly will benefit from Rust's safety mechanisms (in the form of fewer bugs), even if they mean more code to get started.

10 Likes

@kpreid OK, that is all super helpful, and along the lines of what I expected.

But it leads to the possible conclusion that Rust is not the right language for this application, when adding in the minor inconveniences of the default initialization of statics, which is unfortunate. I'd really like to get away from 50 year old languages and derivatives. But it does make sense that the default initialization of statics might not be "handled" in a language that discourages static initialization (because non mutable statics are not that useful LOL).

For the record, our application is not strictly single threaded. No application that handles interrupts is, any interrupt is a thread. But it basically has two "threads", other than lightweight interrupts, and even they are not formal threads with separate stacks and all that. Because the threading model is intentionally so simple, the thread hazards are just not there. We've all had decades of experiences dealing with these embedded issues, and the techniques to deal with them are well known. There is zero reentrancy, and resource ownership and handshaking are clear cut. Like I said, it is a small embedded code base.

Long story short - I'm not confident at the moment that I can convince folks to deal with the overhead of a language designed to handle hazards that we just don't have. If we were starting from scratch and the team was more abstractly focused, it would be easier. But I'm going to go study the Rust protections for multithreading and see just how much programming overhead they introduce.

1 Like

I know that't not the answer you are looking at, but… you wouldn't be able to do that.

I would recommend reading Andrew 'bunnie' Huang's blog post.

Some of our teams use Rust and while most of them are quite enthusiastic… and most of them note that it's slower and less convenient to white code in Rust (compared to C++). They win (and win big) later, when they need to support (and thus refactor their code) but if your code is written from scratch for every small project at may not be a net win.

Note that C++ got that feature after 24 years of development. Rust is only 12 years old. I'm pretty sure in next 12 years they would invent something to solve that issue.

Then Rust may not be the language you want. Or it maybe exactly the language you want. You just need to dig further and thing a bit more about what you actually doing.

Are they clear cut enough? Can you describe your approach to safety and teach compiler about it? That's the question.

I would try. Rust's idea is to, basically separate your code in two parts:

  1. Unsafe code. That goes into the bottom of everything, it's written by professionals, it embeds all these techniques to deal with them, it's code which you either don't change often or autogenerate. Naturally it would use unsafe, because most of the hardware out there is “unsafe”.
  2. Safe code. That one implements the “business logic” it may be implemented by intern or anyone with not a lot of experience… and then Rust guarantees it would work. At least it wouldn't be able to cause access to variables before they would be initialised, there wouldn't be data races and so on.

That's why before you would attempt to write any embedded code in Rust you have to think about whether it would be possible to “encapsulate unsafety”. Is it possible to separate your code into TCB and UCB ? How large would your TCB be compared to UCB?

If you couldn't imagine how you can encapsulate these “dangerous things” into 10% of your code (or less) then Rust makes no sense. Note that it's about 10% of code in your organisation, not in the individual project. Some embedded guys find out that Rust suits them well even if HAL which “encapsulates unsafety” is 50% (or more) of the individual project, but if they can reuse it and this amortize its costs over many projects then it still may be good things to have.

If you would rather keep things “simple” and prefer to teach everyone on your team to always remember about dangers which may be caused by interrupts, etc, then no, Rust is not for you. It wouldn't buy you anything over C or C++.

2 Likes

One point to consider is that you would generally design your system in Rust quite differently than in C. For example, do you really need all those global mutable statics? Maybe you do, but quite often this is done in C because it is much easier and safer than passing pointers to local variables, where you would need to manually track lifetimes. In Rust, the best way to deal with mutable static data is usually to allocate it on the stack in some top-level function (e.g. main), and pass down the mutable or shared references. Mutable references would allow you finer-grained control over who exactly can modify the data, but you could also wrap your data into a RefCell or a Mutex, and pass around shared references. If your access pattern really doesn't lead to data races in C, then locking the data inside of RefCell in Rust would also never panic, and would incur minimal overhead (just a simple integer increment). This would also help you avoid dealing with const contexts and their current limitations (e.g. no traits or for loops).

Now, this likely can't work if you need to access that data from an interrupt handler, but interrupt handlers are generally a terrible place to do any kind of heavy operations anyway. You never know what is the state of your other code and what is safe to do inside of interrupt handlers, if you try to access the static data.

6 Likes

FWIW, there are ecosystem crates/derives which offer a derivable const-capable default, e.g.

I can't vouch for any particular crate, but it is available, if a bit more annotation than just = default in C++.

I think the lang team would like to eventually support = default and const Default, but it's still a good way out, because language evolution and compiler development are a slow process and there's only so much bandwidth available.

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