Rust Embedded Programming Part 1: Introduction

This is my first ever blog post. I hope you enjoy it. I'm writing about Rust and embedded programming. Not only that, but I'm currently learning Rust and starting to really like it. So I'm definitely jumped on that hype train.

I started with embedded programming back in the day when I received a Lego Mindstorms set as a Christmas gift instead of a cool PlayStation. However, this experience sparked a passion for programming and embedded development in me. So why not learn Rust and discovering my passion further.

This will be a series of blog posts about Rust and embedded programming. I will contain the following parts:

So better stay tuned for the following parts. But now enough of the introduction. Let's start with the basics of Rust.

Why Rust?

That is 100% the first question you should ask yourself when dealing with a new programming language. Why should we use Rust? And why for embedded programming? If we have a look at the shiny Rust landing page, there are several arguments for it:

The Basics

Let's first cover the real basics. Of course, there are excellent docs out there how you can start learning Rust. You should definitely check out the official Rust book to start with. But let me show you my favorites of Rust and get our hands dirty on the basics.

Variables and Types

// variables are immutable by default
let foo: String = String::from("Hello, "); // immutable
let mut bar: String = String::from("Hello, "); // mutable
// foo.push_str("Building IoT!"); //❌
// bar.push_str("Building IoT!"); //✔
// variable types can be inferred
let inferred_var = String::new();
fn cube(x: i32) -> i32 {
    x * x * x
}

Structures and Enums

struct User {
    active: bool,
    username: String,
    sign_in_count: u64,
}
// Instantiating
let user = User {
    active: true,
    username: String::from("Alice"),
    sign_in_count: 1,
};
impl User {
    // Function definition (no reference to Self)
    fn new(active: bool, username: String) -> Self {
        User {
            active,
            username,
            sign_in_count: 0,
        }
    }
    // Method definition (mutable or immutable reference to Self)
    fn set_active(&mut self, active: bool) {
        self.active = active;
    }
}
enum Colors {
    Red,
    Green,
    Blue,
}

impl Colors {
    fn is_part_of_yellow(&self) -> bool {
        match self {
            Colors::Green => true,
            Colors::Blue => true,
            _ => false,
        }
    }
}
let color = Colors::Blue;

let color_str = match color {
    Colors::Red => "Red",
    Colors::Green => "Green",
    Colors::Blue => "Blue",
};

if let Colors::Red = color {
    println!("This is red");
}

Options and Errors

There can be only one

Rust brings it own memory management. It is called the ownership-model. It is the reason why Rust is memory safe. At compile time. Since it is a unique concept of Rust, this is definitely the part where I struggled most at the beginning. I still get overwhelmed by it sometimes. But there were some key moments where I understood it a little bit better. So let me share that moment with you.

For a rule of thumb, keep those rules in mind:

Have a look at the following code snippet:

fn main() {
    // ferris becomes owner of the string
    let ferris = String::from("Hello rusty ferris");
    // rusty becomes owner of the string 
    let rusty = ferris;
    // ferris can't be used anymore. This won't compile
    println!("{}", ferris);
    println!("{}", rusty);
}

In Rust, there can be only one owner of a value. So ferris becomes the owner of the string. If we assign ferris to rusty, rusty becomes the owner. In the Rust language, we call that "move". So ferris is moved to rusty. After that, ferris can't be used anymore.

So how can we fix this. We can use reference to ferris instead of moving it. This is called "borrowing" in Rust. We can do that by using the & operator.

fn main() {
    // ferris becomes owner of the string
    let ferris = String::from("Hello rusty ferris");
    // rusty borrows a reference to ferris
    let rusty = &ferris;
    // ferris and rusty can be used now
    println!("{}", ferris);
    println!("{}", rusty);
}

Now let's try to mutate rusty. Since Rust is a pretty explicit language, we have to tell the compiler that we want to mutate the value. We can do that by using the mut on ferris. And we also need to use &mut on rusty to mutate it.

fn main() {
    // ferris becomes owner of the string
    let mut ferris = String::from("Hello rusty ferris");
    // rusty borrows a mutuable reference to ferris
    let rusty = &mut ferris;
    rusty.push_str("!");
    println!("{}", rusty);
}

Ok, that works. We are now able to mutate rusty. But what happens if we try to use ferris again? This won't compile. We can't have a mutable and immutable reference at the same time.

    // ferris becomes owner of the string
    let mut ferris = String::from("Hello rusty ferris");
    // rusty borrows a mutuable reference to ferris
    let rusty = &mut ferris;
    rusty.push_str("!");
    // this won't compile. We can't have a mutable and immutable reference at the same time
    println!("{}", ferris);
    println!("{}", rusty);

And that is actually pretty clever by the compiler. Because this might cause a data race. If we use ferris on a different part of the code, we won't expect that it is mutated somewhere else. So the compiler prevents us from doing that.

So, to tame the borrow-checker, keep those three rules in mind:

Conclusion

This was a really shallow dive into the fundamentals of Rust. We had a look at the variables and data types. We also touched Structs, Enums and their special implementation with Options and Results. And finally, we had a look at the ownership model of Rust. With the h three rules in mind, you can start trying some Rust coding. Just use references and pass ownership, and you will start learning how it works. Try to read what the compiler tries to tell you. The compiler is your best teacher. And before you start to get frustrated, just follow the compilers advice and use clone(). Just like Uncle Bob said:

Make it work, make it right, make it fast.

Hopefully you are not disappointed at this point that we haven't covered embedded programming yet. But it is important to get a grasp about the fundamentals of Rust. They play a big role in embedded programming. So stay tuned for the next part of this series. Where we will figure out the Rust embedded concepts and how we can use them on a microcontroller.