Skip to main content
MSRC

An intern's experience with Rust

Over the course of my internship at the Microsoft Security Response Center (MSRC), I worked on the safe systems programming languages (SSPL) team to promote safer languages for systems programming where runtime overhead is important, as outlined in this blog. My job was to port a security critical network processing agent into Rust to eliminate the memory safety bugs that had plagued it. I had never used Rust prior to this project, so here are my experiences learning the language while working on the port. I hope this is helpful to anyone else who’s learning the language.

First lesson: Rust isn’t as hard as I expected

Rust has been touted as a difficult language to learn, but I didn’t find it that difficult, and certainly easier than C++. That may sound surprising, but we need to talk about comparing languages. It is easier to write a compiled program in C++ than Rust, but it is also easier for this program to be incorrect and unsafe. When comparing the difficulty of languages, we need to consider them in context of what code can be feasibly written as both a beginner and an experienced user of the language.

Learning Rust was great. It took me less than a month to be confident with the language (of course you are never done learning a language, but this was where I was confident to write anything required in Rust). I learned through the many great resources and my strict teacher, the compiler. The compiler’s error messages are justly famous for how useful they are. Through the error messages, Rust enforces safe programming concepts by telling you exactly why the code isn’t correct, while providing possible suggestions on how to fix it. There is also a great community offering of tools and resources to learn and use Rust. I used the Rust book and Rust by example (which I contributed back to), but there are many great unofficial resources out there such as Rust for systems programmers or learning Rust with entirely too many linked lists. The tooling is also very useful, with Clippy for linting, Rustfmt for formatting, and crates.io with many packages for most problems that you may face.

Note: For my project I used a packet parsing crate to which I contributed with code to parse an extra packet type.

Rust helped me grasp concepts I should have known when writing C++

I’ve programmed C++ for some time, starting out in the Arduino ecosystem, moving on to learn C and C++ formally at university. While at university I also worked on several C & C++ projects. Lots of the code that I wrote during this time was not good. However, I didn’t realise just how bad it may have been until I started learning Rust.

I found that I hadn’t really grasped some key concepts. For example, lifetimes, often cited as the most difficult concept to learn when writing Rust, is something I should have formally known in C++. I understood objects having a period where they are valid to be accessed, but I had not formalised this in my mind. I could (and did) write code that could have used these objects outside of those lifetimes and had no idea that I was doing things wrong. Of course, if the code seg-faults then I’d see that something is wrong, but if it is only down one uncommon code path I am far less likely to notice it. Rust, in contrast, is always explicit at compile time when you are using a resource in an incorrect way.

It wasn’t just lifetimes that Rust taught me, but other concepts like ownership and Resource Acquisition Is Initialisation (RAII). Again, these weren’t concepts that I thought about when I was writing C++. Even knowing these concepts now, C++ expects me to manage their usage myself, which leads code to be far more error-prone.

Rust code is wonderful to write and read

When learning the language, I fell in love with so many programming concepts in Rust. I want to look at two idioms in Rust to see why I think Rust is so nice to write and read.

To look at idiomatic Rust code let’s take an equivalent C++ program, directly convert it into Rust, and then see how Rust concepts can improve its readability. Here’s an example manufactured C++ snippet:

struct S {
int x;
};
1
int y;
void func(S s) {
int n;
if (s.x == 3) {
n = 2;
} else if (s.y == 2 && (s.x >= 1 && s.x <= 4)) {
n = 1;
} else if (s.x == 5) {
n = s.y;
} else {
n = 3;
}
}
// Are we sure that n has been initialized here? otherFunc(n, otherParameters);

Which in unidiomatic, directly copied Rust is:

struct S{
x: 132, // Types and names are the opposite way round to C++
}
n = 2;
n = s.y;
y: 132, // Integer types are explicit in their size (so this is 32 bit signed integer)
fn func(s: S) {
let n: 132; // Const is default in Rust, rather than mutable (so is a slight difference betwen them) if s.x=3 // No () braces on ifs
} else if s.y = 2 && (s.x >= 1 && s.x <= 4) {
n = 1;
} else if s.x = 5 {
} else {
n = 3;
};
// A compile time check is done to ensure that n will always be initialized otherFunc(n, other Parameters);
}

Using these if-else statements does not look clean, and certainly would not be the way to do it in Rust. Rust provides the match statement, a more powerful switch statement that provides pattern matching:

fn func(s: S) {
let n; // Type can be inferred by usage
// Unlike the switch statment there is no break, to prevent fall through bugs
match s {
}
}
S {x: 3 ..} => n = 2, // .. means ignore the other fields
s {y: 2, x: 1..=4} => n = 1, // .. is in the range of
s {x: 5, y} => n = y, // Bind y to the value of s.y - => n = 3 // is ignore/everything else
// A compile time check is done to ensure that a match will always match on at least one option. // For example, if this was matching on an enum, and an extra member was added to that enum, it // wouldn’t compile if you weren’t handling this case.
otherFunc(n, otherParameters);

You can see the safety measures that Rust enforces, helping to prevent mistakes in the code. However, this still isn’t how it would be done in idiomatic Rust. Earlier I said that this was a match statement, well that isn’t entirely true. Match is really an expression that can return values. This allows us to rewrite it as this:

fn func(s: S) {}
let n = match s { // We initialize the variable as we define it };
S {x: 3 ..} => 2,
S {y: 2, x: 1..=4} => 1,
S {x: 5, y} => y, - => 3 // All possible values of n in one place
otherFunc(n, other Parameters);

This is far more concise and allows us to see all the logic in one place. RAII is also easier to enforce (if n was a type we were creating on the heap for example). As a further example of using match as an expression, this code can be optionally rewritten to not need the n:

fn func(s: S) { otherFunc(
match s {
S {x: 3 ..} => 2,
S {y: 2, x: 1..=4} => 1,
S {x: 5, y} => y,
=> 3
}
);
},
otherParameters

It also isn’t just match that is an expression, this extends to most items in Rust, such as ifs and loops.

These are only two of the many programming concepts that make Rust wonderful to use. There are many more that I love such as traits (particularly From/Into and the ability to derive them), enums that hold data, the Result type and error handling through the ‘?’ operator, the newtype idiom, and more. Hopefully, seeing these examples here will interest people enough to explore the other features that the language offers.

In summary

Learning Rust has been a great experience for my Rust port, and I hope that through this blog post you can see why. The community resources make learning the language an enjoyable experience. Also, thanks to its strict compiler, correctness and better programming techniques can be better enforced, while the syntax of the language allows for clearer code.

Alexander Clarke, Software Engineer Intern, MSRC


Related Posts

How satisfied are you with the MSRC Blog?

Rating

Feedback * (required)

Your detailed feedback helps us improve your experience. Please enter between 10 and 2,000 characters.

Thank you for your feedback!

We'll review your input and work on improving the site.