USSTRocketry / MiniRockets

Making rockets that hopefully go UP!
MIT License
2 stars 11 forks source link

The Future of Aviation: Why Rewriting Our Avionics Codebase in Rust is Essential for Success #70

Closed frroossst closed 1 year ago

frroossst commented 1 year ago

Ladies and gentlemen, gather 'round and brace yourselves for the most audacious, grandiloquent, and downright impressive proposal you will ever encounter. Today, I present to you a compelling case for rewriting our avionics codebase in Rust, a language so cutting-edge it would make Elon Musk himself drool with envy.

Now, I know what you're thinking. "Why fix something that ain't broke?" But hear me out, dear colleagues. Our current codebase may be functioning as intended, but it lacks the resilience, robustness, and performance necessary to keep up with the demands of modern avionics. It's time to step up our game and embrace a language that can handle the challenges of the future.

With Rust, we can achieve unparalleled speed and efficiency, thanks to its low-level control over system resources and memory management. No more will we be bogged down by the overhead of dynamic memory allocation and garbage collection. Rust's strict compiler checks ensure that we catch errors before they become disasters, and its unique ownership model prevents the dreaded bugs caused by dangling pointers and data races.

But that's not all, folks. Rust is designed for concurrency, making it the ideal choice for handling the multiple tasks and systems required for aviation. Its lightweight threads and fearless concurrency model allow us to tackle the complexity of modern avionics with ease. And don't even get me started on Rust's security features. With its built-in memory safety and protection against buffer overflows, we can rest assured that our codebase is resistant to cyber attacks and other malicious exploits.

Now, I know some of you may be thinking, "But what about the learning curve? Won't we have to retrain our entire team?" Fear not, my friends. Rust's syntax is easy to learn and understand, and its community is vibrant and supportive. We can even integrate Rust with our existing codebase, making the transition seamless and painless.

In conclusion, dear colleagues, we owe it to ourselves, our passengers, and the future of aviation to rewrite our avionics codebase in Rust. It's time to embrace the future, and Rust is the language that will take us there. So let's do this. Let's rewrite our codebase in Rust and take to the skies with confidence and pride.

dhaussecker commented 1 year ago

Please fill out a project proposal if you are actually wanting to propose this. The core foundation of weather a project will be approved is the following: "do the simplest thing that can possibly work". If it is determined that this is not the simplest thing that could possibly work, it is not worth our time with our intense schedule coming up over the next year.

At this point I do not see any merit in porting over our entire codebase to Rust when we have something that already works. We are not trying to meet the needs of modern avionics, we are a student team trying to develop a whole entire system within a year. Our core principle is, to do the simplest thing that can possibly work to make this happen.

Project Proposal template can be found here: https://github.com/USSTR-Avionics/KnowledgeBase/wiki/Documentation-Templates#project-proposals

frroossst commented 1 year ago

Background and Motivation

Rust is an excellent choice for an avionics project because of its safety features and low-level control. In the aerospace industry, safety is critical, and Rust's ownership model and strong typing help avoid common programming errors such as null pointer de-references, buffer overflows, and data races that can lead to system crashes or vulnerabilities. Rust also has a small runtime footprint and can produce efficient code, which is essential for embedded systems with limited resources.

Furthermore, Rust's support for bare-metal programming makes it well-suited for avionics projects that require direct hardware access. Rust's syntax and semantics allow for low-level memory manipulation, and its zero-cost abstractions make it possible to write efficient code without sacrificing control.

Why is this valuable to work on?

Well, for so many reasons, firstly, it’s just more modern, modern c++ is nothing more than a hot pile of garbage over another hot pile of garbage. Don’t get me wrong, I love C and C++, and I will happily code in them over Java or Javascript. But, when we have a group of people working on performance critical and robust systems where we cannot ensure or assume that every member of the team knows what they are doing, we run into a problem. Now, this could very easily be solved, we just hire someone who does QA, well maybe not hire but just assign them the title, but even the best programmers are no match for the Rust compiler, which can within seconds tell you what you are doing wrong. And it tells you at compile time, meaning, with proper CI/CD (which is basically a compile check) it is impossible (literally!) to commit code to master that has memory bugs in it!

And as an additional bonus we have seen a massive push towards Rust from the industry, Rust has been voted the most loved language for the past 8 years [1].

The build system or the lack thereof in C/C++, we may either end up making a complicated make file or use CMake neither one of which are very fun to use. We cannot sem-ver control the libraries that we use, right now we need to maintain an archive of the libraries we use in a .zip format, which is rather caveman-esque. With Rust, every crate (or library) lives on crates.io and follows a semantic versioning system, so no matter when you build or who builds, as long as they have this tiny little Cargo.toml file they will have the same features and functions.

C/C++ lies! What I mean is, Undefined Behaviour and implicit behaviour sometimes the language just does things without telling you. Note how I am able to cause an integer overflow with just a couple of lines of c/c++ code.

#include <stdio.h>

int main()  
    {
    long long int x = 100000000000000;

    int y = x;

    printf("%lld\n", x);
    printf("%d\n", y);

    return 0;
    }
    // output
    /// 100000000000000
        /// 276447232

Now watch what happens with Rust code,

fn main() 
    {
    let x: u128 = 100000000000000;
    let y:u32 = x;

    println!("x = {}", x);
    println!("y = {}", y);
    }

// output
error[E0308]: mismatched types
 --> src/main.rs:4:17
  |
4 |     let y:u32 = x;
  |           ---   ^ expected `u32`, found `u128`
  |           |
  |           expected due to this
  |
help: you can convert a `u128` to a `u32` and panic if the converted value doesn't fit
  |
4 |     let y:u32 = x.try_into().unwrap();
  |                  ++++++++++++++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `hello` (bin "hello") due to previous error

Rust, literally doesn't let you compile!

You might say, "Oh, We're not that dumb, we would never make that mistake" welps, we literally made that mistake causing an integer overflow in the address while writing to the FRAM, link

And don't even get me started on the keywords, static and the scopes of variables, writing code shouldn't be this hard!

As one redditor put it,

If you have a team of people working on C/C++ code you have to make sure they are all very experienced and very careful. When reviewing code you have to be extra detail-oriented, especially for junior folks. Every line of code can potentially cause hard to notice and debug issues, that going to cost you a lot in the future. When you have a team working on Rust, you can have bunch of "junior-devs", one senior-Rust dev, to help them out, put #![forbid(unsafe_code)] in each crate, and when reviewing code focus on higher-level problems instead of suspiciously staring at each line for UB. Rust compiler is working like free couch for all your devs. The productivity gains are enormous.

Onwards, to more horrible examples! So, at many places in the code base we return macros like EXIT_SUCCESS or EXIT_FAILURE, which I love cause sometimes my dumb brain just doesn't know if 0 is success or a failure or was it 1? ugh! so the macros help with readability, but you see, there is not check or guarantee that the upstream parent that calls on a function that returns EXIT_SUCCESS or EXIT_FAILURE checks if the function was successful or not, but we would NEVER do that I hear you say! welps, look at this function forget checking things upstream we don't even return an EXIT_FAILURE!!! So even if the function fails to do what it is supposed to, we will read it as a success. And check this out where we simply just don't check if the function was successful or not, The solution, Rust's Error and Result types!

fn might_fail() -> Result<u8, String> 
    {
    Ok(1)
    }

fn main() 
    {
    let result = might_fail();

    match result 
        {
        Ok(value) => println!("Result is: {}", value),
        Err(error) => println!("Error: {}", error),
        }
    }

As you can see above, when you get a result, it is an enum, this enum needs to be explicitly checked for it's success or failure, otherwise, you guessed what happens! The compiler doesn't let you compile the code.

When working on complex systems with many moving parts, every little bit of help from the language helps, and if you haven't noticed, the error messages from the Rust compiler are so incredibly useful, not only do they tell you what you're doing wrong, but most often than not, they tell you how to fix it, wha-?

Furthermore, you can never segfault in Rust, every other c/c++ function, even those in the standard library can segfault.

C makes it easier to shoot yourself in the foot, C++ makes it harder but when you do, you blow your whole leg off, while Rust will gently put the gun's safety on, tuck you into bed and spoon you till your fall asleep

We can write perfectly safe C/C++ code but, it requires too much micro-management and hand holding of the programmers to ensure that, Rust allows us to offload all of that to the compiler. Sure, linters exist, but where ever there is a way to disable warnings, programmers will do so. We should build safety nets rather than guard rails.

And as the project gets larger it becomes out of hand to maintain it, with the lack of strong type system and type checking, it becomes so much more harder with the out of control number of include files, with Rust you just put a pub in front of the function, or struct. Rust's memory safety, type system, modularity, performance, and tooling make it more maintainable than C++ for large projects. By reducing the likelihood of common programming errors, Rust makes the code more predictable, easier to understand, and more efficient, which translates into lower maintenance costs over time.

Documentation

Tools that we will use

What we will document

  1. System architecture design
  2. Design decisions and trade-offs
  3. Implementation details
  4. User documentation

Scope

I will concede that a RIIR of the whole entire code base is unnecessary and unfeasible, and in an ideal world, I would want the Rust code to be the base from which we cross the FFI boundary to interact with lower level hardware. But, an even better approach is to continue writing our code base in C/C++ while we port parts of it to Rust, things that are fairly stable and not likely to change, for example we could re-write the CAN interface in Rust, or the FRAM interface, or parts of the code base that are mission critical could be Rusty. I personally do not expect anyone to code in Rust or even learn it. But in the grand scheme of things, as more embedded resources become available, Rust is going to be the obvious choice, not to mention the increased adoption in the industry.

Timeline

The timeline for this project will be as follows:

Week 1-2: Background research and scope definition. Week 3-4: Define preliminary requirements and identify potential stakeholders. Week 5-6: Create a detailed project plan and cost analysis. Week 7-8: Design and implement the system's core features. Week 9-10: Develop and conduct tests to ensure the system's safety and reliability. Week 11-12: Document the system's architecture, design decisions, and implementation details. Week 13-14: Finalize documentation, prepare the project for deployment, and conduct user acceptance testing. Week 15: Deployment and project review.

Limitations

Some of the potential challenges and their possible solutions are:

  1. Learning curve: Rust has a unique syntax and memory management model, which can be challenging for newcomers. To overcome this challenge, the team can allocate some time for Rust learning sessions, where experienced Rust developers can provide tutorials or code reviews to help new team members get up to speed with the language.

  2. Debugging: Rust is known for its strict compile-time checks, which can help catch bugs before the code runs, but it can also make debugging more challenging. To address this challenge, the team can use Rust's debugging tools, such as gdb and lldb, and use logging and assertions to help identify and fix bugs.

  3. Limited libraries and frameworks: Rust is a relatively new language, and it may not have as many libraries and frameworks as other languages like C++ or Python. To mitigate this challenge, the team can leverage the existing Rust libraries and frameworks, and also consider building custom libraries or modules if needed.

  4. Team communication: If not everyone in the team is familiar with Rust, it may lead to communication challenges during development. To overcome this challenge, the team can use collaboration tools like GitHub, Slack, or Discord to ensure effective communication and knowledge sharing.

  5. Code review and quality assurance: As Rust is a safety-critical language, it's essential to ensure that the code meets high quality and safety standards. The team can implement code review processes, use static code analysis tools, and conduct rigorous testing to ensure that the system is safe and reliable.

In summary, while not everyone in the student-led group may be familiar with Rust, the team can overcome the challenges by dedicating time to Rust learning, using debugging and testing tools, leveraging existing libraries and frameworks, ensuring effective communication, and implementing quality assurance processes.

Conclusion

The proposed project aims to build a flight computer for a small sounding rocket using the Rust programming language. The use of Rust in avionics projects can provide safety, low-level control, and efficient code generation. The project has the potential to produce a reliable and robust flight computer system, reducing the risks associated with spaceflight activities. The Rust language's safety features, ownership model, and strong typing can help avoid common programming errors, such as null pointer dereferences, buffer overflows, and data races, which can lead to system crashes or vulnerabilities. Furthermore, Rust's support for bare-metal programming makes it well-suited for avionics projects that require direct hardware access. The project's success can also help promote the use of Rust in the aerospace industry, where reliability and safety are critical concerns.

frroossst commented 1 year ago

I would also like to add, that we could, do the functional or business logic in Rust, while reserving C/C++ for hardware access.

frroossst commented 1 year ago

Just look at our current statemachine, it is so incredibly easy to get it wrong, you must break or return the control flow in every case of the switch case statement, this is so easy to miss that it is classified as a common vulnerability by the common weakness enumeration org, and I don't feel like opening my 214 textbook, but a missing break (or might've been a missing default clause regardless the point still stands) caused a telecom company in the 90s, millions of dollars.

Look at this code, there is no check on the value that can be passed to the arg of type int

statemachine_t::e_rocket_state set_current_state_for_statemachine(statemachine_t::e_rocket_state& rs, int state)
    {
    switch (state)
        {
    case 0:
        rs = statemachine_t::e_rocket_state::unarmed;
        return statemachine_t::e_rocket_state::unarmed;
        break;
    case 1:
        rs = statemachine_t::e_rocket_state::ground_idle;
        return statemachine_t::e_rocket_state::ground_idle;
        break;
    case 2:
        rs = statemachine_t::e_rocket_state::powered_flight;
        return statemachine_t::e_rocket_state::powered_flight;
    case 3:
        rs = statemachine_t::e_rocket_state::unpowered_flight;
        return statemachine_t::e_rocket_state::unpowered_flight;
    case 4:
        rs = statemachine_t::e_rocket_state::ballistic_descent;
        return statemachine_t::e_rocket_state::ballistic_descent;
    case 5:
        return statemachine_t::e_rocket_state::chute_descent;
    case 6:
        rs = statemachine_t::e_rocket_state::land_safe;
        return statemachine_t::e_rocket_state::land_safe;
    case 7:
        rs = statemachine_t::e_rocket_state::test_state;
        return statemachine_t::e_rocket_state::test_state;
    default:
        rs = statemachine_t::e_rocket_state::unarmed;
        return statemachine_t::e_rocket_state::unarmed;
        }
    }

One small mistake and your rocket is stuck in unarmed mode this could be midflight or perhaps even more catastrophic. Instead, the alternative Rust code will NOT compile if the wrong state arg is passed in.

pub enum RocketState
    {
    Unarmed,
    GroundIdle,
    PoweredFlight,
    UnpoweredFlight,
    BallisticDescent,
    ChuteDescent,
    Landed,
    }

impl RocketState
    {
    pub fn get_state(&self) -> &str
        {
        match self
            {
            RocketState::Unarmed => "Unarmed",
            RocketState::GroundIdle => "GroundIdle",
            RocketState::PoweredFlight => "PoweredFlight",
            RocketState::UnpoweredFlight => "UnpoweredFlight",
            RocketState::BallisticDescent => "BallisticDescent",
            RocketState::ChuteDescent => "ChuteDescent",
            RocketState::Landed => "Landed",
            }
        }

    pub fn set_state(&mut self, new_state: RocketState)
        {
        *self = new_state;
        }
    }

We write horribly incorrect C/C++ code not because we want to, not because we can, not because we're too dumb to foresee the vulnerabilities. All of these are just off the top of my head, I can find half a dozen more if I go digging through the codebase. It's just that it is so so so incredibly easy for programmers to dismiss these potential flaws while they are programming and just want something to work and simply, get it done. And in all honesty it is an unfair expectation to have from a group of volunteer programmers to write strictly safe code.

Oh! and if you think we can simply use compiler warnings to help us with stuff like this, well some of this stuff still won't be caught, like the FRAM bug, or array out of bounds indexing. But, we will be buried in warnings, if you dig deep enough you will find a commit that is me just adding the #pragma GCC directive to suppress warnings coming from the libraries that we use. The libraries we use, which are written by senior C++ programmers are still vulnerable and not upto code guidelines and specifications. It is not us the programmers, but rather the language itself that is the root of this issue, hence, my push for using a different language.

dhaussecker commented 1 year ago

After review of the proposal, this project has been rejected under the following concerns: