Rust applicability to small embedded codebase - getting discouraged

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.

1 Like

@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!

1 Like

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!

2 Likes

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.

4 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

We've gotten way too philosophical here. And frankly, I think there is a lack of understanding about some of this. I've tried to be clear but I'm obviously communicating badly. I thought I was laying out a clear case why small embedded system use static allocation extensively, don't use the heap, and only use the stack for passing around parameters. (They don't put all the data on the stack as locals during initialization in main). Obviously, I've done a bad job. But it doesn't mean I'm wrong.

We will agree to disagree.

So instead, if anyone is interested, I'm taking a more practical approach with a real world example here (Rustifying a real world embedded example?).

Just don't try to tell me to avoid static allocation for this application. If that is what is suggested as the only viable approach, I will never in a million years sell Rust to our group. Or myself.

So I've spent a bit more time reading all the replies and the points about lifetimes not just applying to memory management issues are well taken. Mea culpa.

The goal here is simply to start to map some Rust like ideas onto our application space.

A lot of people made a lot of good points. But I'm not sold on moving away from static allocation. And the idea that there is no way to protect data from inadvertent access is not true IMO. That's a lot of what data hiding and public vs private access and OO design is about, and it also works when data is statically allocated.

1 Like

I think this gets at some of the talking past each other that's been happening in this thread. Some of the terminology. When I hear "local", I think "on the stack". And "global" has nothing to do with static vs dynamic allocation. I get the feeling that folks have been assuming that when I say static I mean global, where global means accessible to any code. That was never an intent of anything I said.

I'm not yet familiar with the techniques that @VorfeedCanal refers to, and I want to learn. And I'm intrigued by HW management via lifetimes, also new to me.

Our application space is full of singleton classes, I'd say almost all the code fits into that model. So what I've often done coding in C++ is define these classes with appropriate access. In the main module, one instance of each class is allocated outside main and made "static", which really means private in C++. In the main function, I can set up connections between these singletons, protecting against inadvertent access and implying the rules of the game. But all the allocation is static, only the references are local and passed around. This means (almost) all data is allocated at compile time. But "static" never means "global", far from it.

(In case you wonder why I don't use static classes in C++, it's because they are just broken. I tried it)

It's new to most people. It's something people are actively investigating, not something which is “new normal”, yet. Also, keep in mind that you would need heavy doze of macros for these techniques to be viable. Otherwise you would drown in the boilerplate code.

But the idea is quite simple: instead of passing references around you pass empty structures with attached lifetime. When you need to access the actual data — you turn it back into normal reference. Note how in the new version all these helper functions receive “references” to the data they are working with, but in the generated code all these references are removed and turned into direct access to global variable.

That's how Rust does it, too. You main can use unsafe and get access to all these variables (which is safe because there no one before it) and then it passes references (or even zero-sized references) around.

The rest of the code never accesses the global variables directly.

At least that's the idea.

4 Likes

And you can easily switch between direct access to single global variable and pointer passing.

Your “business logic” don't even need to know whether it receives phantom reference to singleton or whether this particular device have few such buffers.

It pulls reference passed for it and that's it. Only your “HAL crate” knows what exists in a single copy in the hardware and what is duplicated.

But yeah, it's obvious that you would need a lot of code (which wouldn't be actually embedded into image) before you can teach the Rust compiler to understand what your are doing.

Take a look around crates.io, I'm pretty sure you are not the first person who tries to create something like that.

When Rust developers say "global," they typically mean accessible to any code with visibility of the item. Something being "global" is distinct from visibility. So even a non-pub static is global (within the module), because it's accessible by any code inside the privacy barrier.

&mut access to a global is inherently unsafe, because of the UB like in this example:

static mut DATA: Data = Data::INIT;

unsafe fn a() {
    let data = &mut DATA;
    b();
    &mut *data; // UB 💥💥💥
}

unsafe fn b() {
    let data = &DATA;
}

The reason for this UB is that &mut are unique (LLVM noalias, C restrict) for the entire time they are live[1]. Because a static mut allows you to acquire &mut without any sort of guard against aliasing &mut, accessing them is unsafe.

This is also the core idea behind unsafe. Access to a static mut is unsafe, but you can make use of a static mut safe by encapsulating it with a safe interface.

The following uses unsafe to define and get access to the static allocation, but is entirely sound:

fn main() {
    // potentially polyfilled, e.g. with a critical section,
    // or just an unsafe assertion that main is never called
    static REENTRANCY_GUARD: AtomicBool = AtomicBool::new(false);
    if REENTRANCY_GUARD.swap(true, Ordering::Relaxed) { panic!(); }

    let foo_manager: &'static mut FooManager = {
        static mut FOO_MANAGER: FooManager = FooManager::new();
        // SAFETY: FOO_MANAGER is not nameable outside this scope
        //         and we are not reentrant thanks to the above guard
        unsafe { &mut FOO_MANAGER }
    };

    // do whatever, call whatever functions, etc.
    // at this point you're entirely safe again!
}

There's nothing "wrong" about using unsafe like this. The entire point of unsafe is to write some trusted code which does something UB-prone (e.g. getting a unique reference to static data), and provide a safe interface to the rest of the code so they are isolated from the potential for UB.


If you want to use non-Send/Sync data, you can do a similar trick. You can create a singleton non-Send/Sync token in the same way, then gate any access to other non-Send/Sync resources on providing a copy of the token as witness that you're on the same thread.


  1. The exact time of when references are "live" is still an open question, because Rust doesn't have a formal memory model. However, they are at a minimum live from the time they are created to the time they are last used, as well as for the entire body of a function they are a parameter for (even if unused). ↩︎

11 Likes

This is very helpful. Even though reentrancy is not a concern for our App, this got me started on a pattern for statically allocated singletons.

After thinking about it all for a while, I realized that the whole idea of statically allocating mutable data is really tied up with a programming model that is essentially dominated by singletons. So much so that heap based allocation just isn't necessary. I think that is one of the reasons that there has been certain amount of talking past each other.

The trick is coming up with the right singleton models and provide protection at the boundaries to the singletons, and private access to the static mutable data from within the singleton. I'm well on my way to thinking through this. I'll probably create a separate post to get advice on how to do this more properly.

Thanks to everyone for helping me with this thread, it's gotten me over the hump!! I've converted an existing C/C++ module to Rust and it's going well. I'm pretty much sold on Rust, and look forward to future enhancements to the language. It's about time to put C/C++ in the rear view mirror for new projects.

@VorfeedCanal @H2CO3 @kpreid @afetisov @Hyeonu

9 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.