Friday, December 18

Programming with Rust

Do hardware hackers need a new programming language? Your first answer might be no, but hold off a bit until you hear about a new language called Rust before you decide for sure.

We all know real hackers use assembly language to program CPUs directly, right? Well, most of us don’t do as much assembly language as we used to do. Languages like C can generate tight, predictable code and are easier to manage.

Although some people use more abstract languages in some embedded systems, it is no secret that for real-time systems, device driver development, and other similar tasks, you want a language that doesn’t obscure underlying details or generate code that’s difficult to reason about (like, for example, garbage collection). It is possible to use special techniques (like the Real-Time Java Specification) to help languages, but in the general case a lean language is still what most programmers reach for when you have to program bare metal.

Even C++, which is very popular, obscures some details if you use things like virtual functions (a controversial subject) although it is workable. It is attractive to get the benefit of modern programming tools even if it does conceal some of the underlying code more than straight C.

About Rust

That’s where Rust comes in. I could describe what Rust attempts to achieve, but it is probably easier to just quote the first part of the Rust documentation:

Rust is a systems programming language focused on three goals: safety, speed, and concurrency. It maintains these goals without having a garbage collector, making it a useful language for a number of use cases other languages aren’t good at: embedding in other languages, programs with specific space and time requirements, and writing low-level code, like device drivers and operating systems. It improves on current languages targeting this space by having a number of compile-time safety checks that produce no runtime overhead, while eliminating all data races. Rust also aims to achieve ‘zero-cost abstractions’ even though some of these abstractions feel like those of a high-level language. Even then, Rust still allows precise control like a low-level language would.

High goals, indeed. Rust promises guaranteed memory safety, threads without data races, trait-based generics, type inference, a minimal runtime, and efficient C bindings (the compiler uses LLVM, by the way). You can download the software for Linux, Mac, or Windows. You can even edit and run example code in your browser right from the home page with no software installed.

Rusty Hardware

But, wait. I mentioned hardware hacker language, right? Since Rust targets Linux, it is usable with the many single board computers that run Linux. There is an unofficial repository that handles several ARM-based boards to make it easy to put Rust on those computers.

If you want to see an example of Rust on embedded hardware, [Andy Grove] (not the one from Intel) recently posted a hello world LED blinking example using Rust and a Beaglebone Black (see the video below). He also found a crate (a Rust library, more or less) to do digital I/O.

Of course, blinking an LED isn’t very compelling, but it does illustrate that the system will work on an embedded board. Rust does a lot of safety checks at compile time. It also has an unusual scheme that has variables that own memory. When the variable goes out of scope, the memory is deallocated. Obviously, then, only one variable can own some piece of memory at one time. You can “borrow” references to the variable, but when you do, you may prevent future changes to the data (at compile time).

Owning and Borrowing Memory

For example, suppose you have this (lifted from the Rust documentation):

fn main() {
 let mut x = vec!["Hello", "world"];
 let y = &x[0];
}

The variable x is mutable (and automatically typed). It owns the memory that makes up the vector create by the vec macro (macro invocation uses the exclamation point). Variable y is a reference to part of x. It doesn’t own any memory; it is just a reference. Then this code will fail to compile:

fn main() {
  let mut x = vec!["Hello", "world"];
  let y = &x[0];
  x.push("foo");
}

The issue is that changing x (with push) invalidates the reference to y. The compiler catches this (and is smart enough to know if the y variable fell out of scope before the change to x). You can also clone the vector element into a new variable (which would then own that copy of the element).

For special cases, there are ways to mark code unsafe and bypass the usual Rust checks. This is just one example of how Rust works–it takes a little reading and working with code examples to get a feel for all the differences. Luckily, the documentation is pretty good.

Other Tools and History

Rust also uses a tool called Cargo that manages the build process. In addition to compiling, it downloads and builds dependencies. Rust calls compilation units crates (I guess that’s a play on cargo). So where you think of libraries in C, you’d talk about crates when using Rust.

Rust isn’t exactly new. Mozilla employee Graydon Hoare started it as a personal project. Mozilla started sponsoring the project in 2009. Rust 1.0, the first stable release, occured May 15, 2015.

Do We Care?

Do we need a new bare metal language? Maybe. I haven’t tried Rust in a real project yet, so I don’t know if it will really perform or not. However, I applaud the idea of finding more problems at compile time. One thing I really like is that Rust doesn’t guess what’s best for you. There are multiple types of pointers, memory cells, and synchronization primitives that allow you to pick between different (well documented) trade-offs. As I mentioned, you can even mark code unsafe and sidestep some checks.

Launching a new language is hard. Only time will tell if Rust will find a home on hacker’s hard drives.


Filed under: ARM, rants

No comments:

Post a Comment