Experimenting with Rust
Since the very beginning we have our own CI infrastructure. Initially we had Jenkins, now we proudly operate on GitLab. Nevertheless, a CI system is not enough: CD is a must for us to distribute our builds for testing. That’s why some time ago we developed a Ruby gem which is responsible for a couple of things: extracting iOS/Android artifacts from builds, uploading them into our servers and notifying third parties about it. The quality of this script is fine, but not as perfect as we would wish. So we decided to rewrite it.
We wanted the new CD application to be: fast, reliable, easily extensible and deployable on various platforms (Linux/OS X). I wanted to write it in a modern, statically typed language.
Swift - this was our first choice, but it quickly turned out it’s not yet available on other platforms than OS X. After some discussions and the inspiring attitude of Wojtek Erbetowski (our Head of Engineering) we made a verdict: Rust.
Rust is a compiled programming language developed by Mozilla Research and officially announced in 2010. It’s open source as well, so the language design is refined by the community. Version 1.0 was released in May 2015 and new releases are being published in a six weeks cycle.
Among lots of impressions about the language I’d like to touch four topics:
- memory safety,
- language structures,
- friendly compiler,
- package manager.
The core of Rust’s memory policy is driven by the idea of ownership and borrowing.
Each variable is created on stack by default (just like in C) and the current scope owns it. You can pass this variable to a function, but then it will take the ownership of the variable and you won’t be able to use it in the current scope. Let’s visualize this.
let a = 1; do_something(a); // Function takes the ownership. let b = a + 1; // Error! You can't use `a` anymore.
How to make it work? You have to borrow
a, so we have to pass a reference to the variable.
let keyword means that
a is immutable by default, so if we want to allow our function to modify
a it has to be instantiated a little bit differently.
let mut a = 1; // `mut` keyword enabled mutability. do_something(&a); // Reference is passed. let b = a + 1; // This will work.
(Update: Actually, even the first example will compile, because
1 is a primitive
Int type that implements
Copy trait, which means that
1 will be copied when passed. With more complex types, like
String, you can use the
clone() method to make a copy.)
Seems a little bit crazy, doesn’t it? But thanks to this the language is incredibly reliable in terms of memory management. And this is just the tip of the iceberg. E.g. a shared pointer to a mutable type
T allocated on the heap would look like this:
Rust is designed to be safe, especially in two areas: memory and concurrency. All potential issues are detected during compilation, so it’s almost impossible to write dangerous code.
Rust is not an object oriented programming language. You will not find classes hierarchy here (not even the
class keyword), polymorphism or runtime features. This may be quite confusing for most of programmers who are very used to think in an OOP way.
The basic data structure is
struct, which behaves like a standard class but cannot be inherited.
trait means the interface. A trait may derive from other traits.
struct can implement multiple traits.
enum type as well. It can hold different associated types and with the
match keyword (similar to
switch in other languages) it’s very powerful.
There are generics (templates) as well, but due to the lack of polymorphism they don’t always behave like most object oriented programmers would expect. The generic type is resolved in compile time, i.e. you can’t use two different structs even if they implement the same trait (interface).
Rust is very functional. Every function returns a value even if it’s an empty tuple
(). The language operates with iterators, iterator adaptors and consumers, so most built-in data types expose methods like:
filter, etc. This makes the language very clear and expressive.
The basic types that the standard library operates on are two enums:
Result. Optionals can have a value or not. Results can hold a success value or an error value. Thanks to this, error handling is very elegant and you will never be surprised by unidentified exception throws or illegible try-catch constructions.
A friendly compiler
The rust complier is called rustc. Amazing tool. This is the first compiler with such a clear and descriptive output I have ever worked with. rustc is not an enemy, but a friend to the programmer. Thanks to good language design, every encountered issue in your code is carefully analyzed and after posting the error, solutions to fix it are suggested. And if you’re not sure about the error you can run
rustc --explain E<error number> and you will be provided with a detailed explanation of what’s wrong.
The Rust installation contains a built-in package manager, Cargo.
Cargo can create new projects (binary or library) by providing you with a nicely set up standardized directory tree.
Cargo can manage dependencies - in the TOML configuration file you can specify everything you need. If you don’t know the library, just type
cargo search <keyword>.
Cargo can run tests - simple
cargo test runs unit tests written in source code files, integration tests from the tests directory and tests that are embedded in documentation code example comments.
Using Cargo feels very modern and simple. It’s totally integrated with the language and provides all necessary features that you may expect.
The Rust experiment was a challenge. There were a few moments of crisis, mostly due to my OOP habits and several language concepts that needed to be well understood.
On the other hand writing in Rust is fun. Fun that feels safe but powerful.
On the official website you will find this statement:
Rust is a systems programming language that runs blazingly fast, prevents nearly all segfaults, and guarantees thread safety.
It really is. Give it a try!