A computer program may crash or hang due to a wide variety of reasons. Memory access violations/segmentation faults are most of the time rooted in unsafe memory handling, while hangs and unpredictable behavior could sometimes be attributed to concurrent, unsynced or out-of-sync access to memory.
Rust is a new programming language,
the primary goal of which is to eliminate these kinds of issues
by creating a language in which memory safety and safe concurrency
are just as important as keeping the generated code as fast as possible.
Rust has just recently reached its first stable release, 1.0.0 — but
it already has a vibrant community, blooming ecosystem of crates
(modules) in its package manager and developers harnessing its
capabilities in a variety of projects.
If you were into systems programming before, this the above intro should have been enough to pique your interest in trying Rust — but what if you have never touched the low-level yet? What if the last time you heard the words "C" and "stack/heap allocation" was 10 years ago in CompSci 101?
The real boon is that, while Rust provides the performance generally only seen in low-level systems languages — most of the time it will feel like a high-level language! Here are just a few examples of how you could leverage Rust in practical applications:
The IoT era and the expansion of the maker movement brought about a real democratization of hardware projects. Whether it is the Raspberry Pi, Arduino or one of the young titans like the BeagleBone or Tessel, you could choose from a slew of languages to code your hardware projects in, even Python or JavaScript.
There are times, however, when the performance these languages offer is simply not adequate. Other times the microcontroller hardware you are aiming for is just not suited to the runtimes these languages require: slow chips with tiny memory reserves, or ultra-low power applications still require a close-to-the-metal language. Traditionally that language has usually been C — but as you might have guessed, Rust is the new kid on the block.
Rust already supports a wide variety of exotic platforms. While some
of this is still in experimental phase, support includes
generic ARM hardware, the Texas Instruments
TIVA dev board
and even the Raspberry Pi.
Some of the newest IoT boards like the Tessel 2 even
come with official, out of the box Rust support!
Studies prove Rust is already great for HPC. You don't even have to rewrite your whole application in Rust: Rust's flexible Foreign Function Interface provides efficient C bindings so you could start rewriting your app module by module, slowly transitioning towards a better developer experience. This new experience will result in a performance on par with the old code or better, but with a more maintanable codebase with less errors, which scales much better on a high number of cores.
Rust is great for rewriting performance-sensitive parts of your application in. It interfaces well with other languages via FFI and has a tiny runtime that lets it compete with C and C++ in most cases, even when resources are limited.
Despite the work-in-progress nature of the language, there are already business-critical, live production applications that are making a good use of Rust for some time now: for example Yehuda Katz's startup, Skylight uses high-performance Rust code embedded in a ruby gem for data-crunching.
If any of the above got you sufficiently hyped, one question might arise:
"So what is the best way to dive into Rust, then?"
There are lots of great all-rounder tutorials covering Rust in general, and more specific aspects aspects of the language. For example, the Rust blog has great articles on various facets of developing Rust applications. There are also some excellent talks that will help get you started - one of those is Aaron Turon's talk at Stanford University. He explains the main concepts and motivations behind Rust nicely, and the talk in general serves as the perfect aperitif, starting you on your journey to learning the language:
Embed? Aaron Turon's Stanford Seminar on Rust
Here it is worth noting that you should be wary of old (that is, more than a few months old, from 2014 or earlier) talks and posts on Rust (and especially code!). Since the language was less stable then, you might stumble into things that do not even exist in 1.0 (much less compatible with current, post-1.0.0 codebases).
That said, no talks or tutorials could replace actually writing code, could they? Rust's got you covered in that regard, too! The Rust Book has been conceived to help you get started — from installation, through your very first Hello World to providing an in-depth reference for all the core language features, you will find it all there in one convenient place.
There is one more resource worth mentioning here: Rust by Example guides you through Rust's key features,from the most basic ones to the arcane powers of traits, macros and the FFI. What really makes Rust By Example an invaluable resource is the live-editable, in-browser runnable examples present in all of the articles. It even saves you the hassle of downloading & compiling Rust, as most of the examples you could try (and edit) from the comfort of your living room couch, just as they are!
Not that downloading and installing Rust was much of a trouble, anyway.
You could head to the homepage and download
the appropriate binary/installer from right on the first page. The
downloads contain all the tools you need to get started (like rustc
,
the compiler, or cargo
the package manager), and are available
pre-built for all platforms (Windows, Linux & OSX).
So what is it then, that makes Rust so unique, so different?
Arguably, it's Rust's unique ownership model that allows it to really shine — eliminating whole classes of errors related to threading & memory management, while easing development & discovering errors. It is also invaluable in keeping the language runtime minimal and the compiler output lean and performant.
The basic principle of Rust's ownership model is that every resource could
belong to one and only one "owner". A "resource" here could be anything —
from a piece of memory, to an open network socket or either something more abstract
like a mutex lock. The owner of said resource is then responsible for using,
possibly lending out the resource (to other users), and finally cleaning it up.
All this happens automatically, with the help of scopes and bindings: bindings
either own or borrow values, and last for the end of their scope. As bindings
go out of scope, they either give their borrowed resource back, or in case
they were the owner, free the resource.
What's even better is that the checks required for the ownership model to work are executed and enforced by the Rust compiler and "borrow checker" during compilation, which in turn results in various benefits.
For one, since the correct operation of a program is asserted at compilation time — code that compiles could generally be considered safe with regard to most common types of memory & concurrency errors (such as use-after-free bugs or data races). This is possible because Rust would complain at compilation time if the programmer attempted to do something that violated the above principles, which in turn helps keep the language runtime minimal. By deferring those checks to compilation time, there is no need to perform them during runtime (code is already "safe"), or in the case of memory allocations, there is no need for runtime garbage collection.
While the thorough explanation of the ownership model is outside the scope of this article, both the Rust Book (linked above) and various talks and articles excel in explaining its principles. Understanding ownership principles and the borrow checker is essential in that these two serve both as the baseline for understanding the language, and the source for other other powerful aspects of the language.
For example, one of those aspects in which Rust really shines is concurrency and parallelism.
Shared mutable state is the root of all evil. Most languages attempt to deal with this problem through the ‘mutable’ part, but Rust deals with it by solving the ‘shared’ part.
— The Rust Book
Besides memory safety, paralellism and concurrency is the second most important focus in Rust's philosophy. It might be surprising, but the ownership system (coupled with a few handy traits) provides powerful tools for threading, synchronization and concurrent data access.
When you try some of the built-in concurrency primitives and start digging deeper into the language, what may come as a surprise, is that those primitives are provided by the standard library, rather than being part of the language core itself. This means that coming up with new ways to do concurrent programming is in the hands of library authors — rather than being hard-wired into Rust, restricted by the language creators' future plans.
The other benefit of Rust's model is what's succintly expressed as
"zero-cost abstractions". Thanks to the static type system, the
carefully selected features and having no garbage collector
Rust programs compile into performant and predictable code that's
on par with code written in traditional, low-level languages (such as C/C++).
By moving the various checks out of the runtime and getting rid of
garbage collection the resulting code mimics the performance characteristics
of those low-level languages, while the language itself can still remain
much more expressive. The (seemingly) high level constructs (strings,
collections, higher order functions, etc.) mostly avoid the runtime
performance hit commonly associated with them, and since the Rust compiler's
output is in the LLVM intermediate representation,
the final machine code makes good use of the LLVM compiler's cross-platform
benefits and all additional (current and future) optimizations automatically.
We could go on for hours about how with Rust you never have to
worry about closing sockets or freeing memory,
or how traits & macros give you the incredible flexibility for
overloading operators, or even
working with Rust's functional aspect, juggling iterators, closures and
higher order functions…
but we wouldn't want to overwhelm you early on. Sooner or later you will
come across those anyway — and it's much more fun to discover for yourself
exactly how deep the rabbit hole is.
So now instead, let's finally see some code!
Without further ado — et voilá, your very first Rust code:
fn main() {
println!("Hello, Rust!");
}
/PS: could we made these an interactive "play-pen", similar to those at rustbyexample.com?/
What, that's it? Okay, I agree this might have been a bit anti-climactic, but there really are only so many ways one can write a Hello World program in Rust. Please bear with us while we elaborate with another, slightly more complex example — and for that, we will be relying on another timeless classic…
We are going old-school, with this other classic oldtimer, the FizzBuzz program! Fizzbuzz is a classic algorithm in which we count updwards for all eternity, replacing some of the numbers with fizz (for numbers evenly divisible by 3), buzz (for numbers divisible by 5) or fizzbuzz (divisible by 3 and 5).
Also to show our point (and to help you familiarize yourself with Rust's semantics), we have included the C and Python versions of the same algorithm:
Imperative fizzbuzz - C version
#include <stdio.h>
int main(void)
{
int num;
for(num=1; num<101; ++num)
{
if( (num%3)==0 && (num%5)==0 ) {
printf("fizzbuzz\n");
} else if((num%3)==0) {
printf("fizz\n");
} else if((num%5)==0) {
printf("buzz\n");
} else {
printf("%d\n",num);
}
}
return 0;
}
Imperative fizzbuzz - Python version
for num in xrange(1,101):
if num % 5 == 0 and num % 3 == 0:
print "fizzbuzz"
elif num % 3 == 0:
print "fizz"
elif num % 5 == 0:
print "buzz"
else:
print num
Imperative fizzbuzz - Rust version
fn main() {
for num in 1..101 {
if num % 3 == 0 && num % 5 == 0 {
println!("fizzbuzz");
} else if num % 3 == 0 {
println!("fizz");
} else if num % 5 == 0 {
println!("buzz");
} else {
println!("{}", num);
}
}
}
As you can see, Rust's syntax is pretty straightforward - it is somewhere in between C's and Python's complexity. You might have noticed the range notation in the for loop's declaration (similar to its Python counterpart), which should hint to more advanced features. To expand on this and show you the true flexibility Rust can offer, in the second example we will crank our fizzbuzz up a notch!
/PS: also it would be nice if we could group the 3 + 3 (imperative + functional) examples together to a switchable block, like those at http://tessel.io /
Rust calls itself multi-paradigm — so let's put this to the test, and rewrite our fizzbuzz-example in… functional style! To help put the code in perspective, we will implementations of the same algorithm in two other language — this time in Ruby and JavaScript (the EcmaScript 6 flavor):
Functional fizzbuzz - Ruby version
1.upto(100)
.map{ |num|
case
when num % 15 == 0 then "fizzbuzz"
when num % 3 == 0 then "fizz"
when num % 5 == 0 then "buzz"
else num
end
}.map{ |output|
puts output
}
Functional fizzbuzz - JavaScript (ES6) version
(new Array(100)).fill(0).map((i,n) => n+1)
.map((num) => {
if (num%3 === 0 && num%5 === 0) {
return "fizzbuzz";
} else if (num%3 === 0) {
return "fizz";
} else if (num%5 === 0) {
return "buzz";
} else {
return num;
}
})
.map(output => console.log(output));
Functional fizzbuzz - version
fn main() {
(1..101)
.map(|num| {
match (num%3, num%5) { // Wow! Pattern Matching FTW!
(0, 0) => "fizzbuzz".to_string(),
(0, _) => "fizz".to_string(),
(_, 0) => "buzz".to_string(),
_ => format!("{}", num)
}
})
.map(|output| { println!("{}", output); output })
.collect::<Vec<_>>();
}
Now we are talking! It's not only that we could mimic Ruby's functional style with cloures and functional method calls, but thanks to Rust's powerful pattern matching we can even trump Ruby's expressiveness.
Note: Rust's iterators are lazy, so we actually need the .collect()
call (which is called a consumer) for this to work — leaving that out
would cause that none of the above code would get executed. Check out the
consumers section of
the Rust Book to learn more about this.
Below you can see what happens in the pattern matching phase of our fizzbuzz-example a bit more elaborated:
...
// For pattern matching, we build a tuple, containing
// the remainders for integer division of num by 3 and 5
match (num%3, num%5) {
// When "num" is divisible by 3 AND 5 both
// (both remainders are 0)
// -> return the string "fizzbuzz"
(0, 0) => "fizzbuzz".to_string(),
// When "num" is divisible by 3 (the remainder is 0)
// Is "num" divisible by 5? -> we don't care
// -> return the string "fizz"
(0, _) => "fizz".to_string(),
// When "num" is divisible by 5 (the remainder is 0)
// Is "num" divisible by 3? -> we don't care
// -> return the string "buzz"
(_, 0) => "buzz".to_string(),
// In any other cases, just return "num"
// Note, that matching must be exhaustive (that is, cover
// all possible outcomes) - this is enforced by the compiler!
_ => format!("{}", num)
}
...
Here we can't go into great depths about how pattern matching or destructuring works, or what tuples are — there are great articles on all these topics in the Rust Book, the Rust Blog or over at Rust By Example, but we think this is a great way to demonstrate the features and nuances that make Rust both powerful and effective at the same time.
Now that you are all set and eager to dive in to creating cool stuff with Rust, you might want to ask what should that cool stuff be?
Well, that depends! You could start small, write a small app, a tiny library — upload it to crates.io. People have created huge amounts of exciting projects in Rust already — from pet *nix-like kernels, to embedded hardware projects, from games to web server frameworks, proving that the only limit is your imagination.
If you would like to start small, but contribute useful stuff while learning the
ropes and intricacies of Rust programming, consider contributing to some of the
projects already thriving on GitHub (or even the Rust compiler itself — which is
also written in Rust).
You could also start by rewriting small parts of your production app, and
experimenting with the results. Rust's
Foreign Function Interface (or FFI for short)
is intended as a drop-in replacement for C code — this means, with minimal effort
you could replace small parts of your application with Rust implementations
of the same code. But interoperability doesn't stop there — these C bindings
work both ways. This in part means you could use all standard C libraries in
your Rust apps — or (since a lot of programming languages come with standard
C interface libraries) make calls back-and-forth between Rust and
Python, Ruby or even Go!
There are virtually no languages you couldn't work together with:
want to compile Rust to JavaScript,
maybe even use inline assembly in your code?
You could!
Now get out there and build something awesome with Rust!
Also please don't forget - Rust is not simply a programming language —
but it's also a community. Should you ever feel stuck, don't be shy to ask
for help, either on the Forums or IRC,
or just share your thoughts & creations on Reddit.
Because Rust is indeed great — but it is the people like You who are going to make it awesome!
/PS: Skeleton & collected sources from my research: https://medium.com/@slsoftworks/1eee43ea526a /