Preface
Using rust for rocketry isn't exactly a new thing, but it is quite honestly one of the best ways one can do it. Providing memory safety and a level of security you just simply cannot find (at least easily) in other languages.
Choosing a Language
When choosing a language for a new project, especially something as important as an embedded system or highly-reliant state management system, for my use case this was a launch control or data acquisition software, you start with the language you want to build it in. Oftentimes this comes down to C or Python depending on the size of your device. But, in my opinion python is not a great choice if you can afford going against it. But now there is a new player. So, why/why not rust in embedded/micro systems?
Now, I'm not the devils advocate for rust, but contrarily I do not think it is perfect. So, you might be wondering? What's wrong with it?
What's wrong with rust?
Development speed
This one is said again and again, but I think it's important to mention. It tends to take an individual a while to write what might be only a few lines in say, python.
Sometimes, this is a good thing. Let me give you an example of what I mean.
The following code describes a brief initialization and reading of a sensor in python and rust. I know this isn't exactly what would happen in each language as you might end up using a HAL or otherwise to complete the task, but the fundamentals is important here.
"""
main.py
"""
# Initialization
class Sensor:
def __init__(pin, type):
# ...
def read():
# ...
# Using It
sensor = Sensor(15, "Light Sensor")
print(sensor.read())
/// main.rs
/// Initialization
trait Readable<T> {
fn read(&self) -> T
}
struct Sensor { ... }
impl Sensor {
fn init(pin: u32, type: SensorType);
}
impl Readable<bool> for Sensor { ... }
/// Using It
fn main() {
let sensor = Sensor::init(15_u32, SensorType::LightSensor);
println!("Sensor Read: {:?}", sensor.read());
}
At a first glance, you may not understand what I am getting at, but lets dive a bit deeper.
In rust, you can define what is the equivalent of a C vtable, using what are known as
traits
. This means, with little abstraction,
you can define common methods, and most important, enforce how they are used across multiple
different structures.
This means, if you wanted to, you can define any number of backends for how to read your sensor, whilst containing consistency in how they interact, such that they perform and are interacted with identically. This is amazing.
But whats the catch?
This is hard to write, and oftentimes harder to keep track of. Now, by no means is this an impossible task. But it means you have to be extra careful in how you manage and store each piece of code. Although, opens many possibilities.
Is this achievable in python/insert-other-language-here?
Yeah? It always is, but depends how you want to compare them. You can create classes, and inherit from them but python doesn't provide any good type safety that you would want in a performance-critical (i.e. rocketry) environment. Other languages like Java do similarly, but nothing I've come across compares to how well it's done in the rust ecosystem, with it being so light, whilst being extensible and very, very typesafe.
Self-induced complexity
A common trend in rust is making code longer than it needs to be, too many tab-indents, over-implementing traits, etc.
This comes often from misunderstandings of the Result
or Option
type. Observe the following:
let value: Result<Value, Error> = ...;
let unwrapped_value = match value {
Ok(val) => val,
Err(err) => {
eprintln!("{}", err);
// break out someway.
}
}
This is so verbose, and unnecessarily so. Sometimes you may even get a Result<Result<V, E>, E>
, and it may not be
your fault (although this shouldn't really happen if you plan well), it can get really ugly.
As a result (ha), this can lead many developers to using... .unwrap()
. The panic ensues!
But there are always easier ways, using the ?
operator, such as value.action()?;
to auto-propagate the error,
can be very well implemented to ensure not only cleaner code, but improve readability and possibly performance.
Hence, rust's complexity is avoidable, but also a very, very easy trap to fall into.
Developer Supply
Often, it takes a while to both learn and start writing the rust language, and not everyone wants to invest such time in a language without a large quantity of jobs, why not just learn TS, Python, C, Java or Go? As a result, there aren't many developers. So, finding developers or proposing the software write for a new project be in rust can be difficult.
What's the solution? train people up.
At what cost? Time and money.
So is this really doable? Oftentimes not, and for those people I don't advise venturing into the rust ecosystem if the gains would be negligible, but the cost on time and labour be high. It just isn't worth it. If your team is willing to learn it, and able to learn various resources quickly, go for it.
Lifetimes
This one is kind of a joke but lifetimes are a pain in the ass to deal with. That's all I have to say. Genuinely harder than C pointers.
Okay, so Rocketry?
A big problem with choosing a non-c solution for embedded solution is often an existing code-base or library requirement. Now, for my project this wasn't too big of an issue, as one example of which was used LabJacks for our data acquisition.
They provided both C/C++ and Python bindings for the C library, but nothing for rust. However, using libloading, I created
a minimal binding that implemented all the functions we would need for it, called ljmrs.
Where, using libloading
we load the dylib or dll functions and execute them, the target environment for such a program
being the raspberry pi, a not-too-power-consuming but also somewhat-powerful system.
Now, if your library you need is complex, or not so usable with libloading or a rust C binding library, this may not be so easy. In which case, using the existing bindings are often your only option, and that is fine.
So, we simply have to build a state management system over this to handle all the logic and we are good to go... right?
Well, not always. You also have to consider build targets, if your dependencies will work with your current, and future build targets, etc. What might this mean? Well, say you target your software for a rpi, but want to make the system lower in power consumption so switch to a custom PCB. But, you were using tokio, or ssh, or something else. Now, this doesn't work anymore.
It is fair to say this would be a design flaw, that you would want to build with practices that are extendable, i.e. only a primary thread, and using a more widely implementable IO interface, like I2C, SPI, or even Serial. Which is completely fair, but is something that needs to be considered in-depth before beginning.
Verdict
In verdict, there are many things to consider when building an embedded solution, a language is a large part of it, and shouldn't be overlooked.
Happy Coding,
Benji