cargo), especially a package manager, which does a lot of things rightIt's rather simple. I wanted to create a game, and I started writing a tailored engine and the game foundation in C++. C++, however, is rather annoying if you want to write sane code (with smart pointers and compile-time checks). So I constantly searched for an easier way to write stable code, which especially gives me guarantees for multi-threading. Modern games should use all CPU cores available ;)
Somewhere around Q3/Q4 of 2016, I finally found a new solution to the problem. I read an article about a new programming language, which competes with C++ directly and has the goal of bringing system and application programming to the next level of stability and abstraction, without trading performance: Rust. A quick look at the sample code made me think that it looks pretty easy and has awesome guarantees.
fn main() {
let greetings = ["Hello", "Hola", "Bonjour",
"Ciao", "こんにちは", "안녕하세요",
"Cześć", "Olá", "Здравствуйте",
"Chào bạn", "您好", "Hallo",
"Hej", "Ahoj", "سلام"];
for (num, greeting) in greetings.iter().enumerate() {
print!("{} : ", greeting);
match num {
0 => println!("This code is editable and runnable!"),
1 => println!("¡Este código es editable y ejecutable!"),
2 => println!("Ce code est modifiable et exécutable !"),
3 => println!("Questo codice è modificabile ed eseguibile!"),
4 => println!("このコードは編集して実行出来ます!"),
5 => println!("여기에서 코드를 수정하고 실행할 수 있습니다!"),
6 => println!("Ten kod można edytować oraz uruchomić!"),
7 => println!("Este código é editável e executável!"),
8 => println!("Этот код можно отредактировать и запустить!"),
9 => println!("Bạn có thể edit và run code trực tiếp!"),
10 => println!("这段代码是可以编辑并且能够运行的!"),
11 => println!("Dieser Code kann bearbeitet und ausgeführt werden!"),
12 => println!("Den här koden kan redigeras och köras!"),
13 => println!("Tento kód můžete upravit a spustit"),
14 => println!("این کد قابلیت ویرایش و اجرا دارد!"),
_ => {},
}
}
}
So I did, what I give as advice to anyone wanting to learn and use a new language with a certain purpose in mind: I started out writing small applications in Rust, expanding to bigger and bigger stuff. And... it was hell. Rust does not only work entirely different than C++, because it is a FP inspired language (as opposed to OO in C++), but it also has a big set of very strict rules and the compiler checks them all. Applying these rules is sometimes difficult, and fixing the errors does not always have an apparent solution when you start out.
For example satisfying the borrow-checker, which makes sure that there is no pointer to a resource, to which is written, and no one can read while a write-pointer exists. The simple solution in the beginning was to just clone or re-create variables, which is very wasteful. Later on, I started to find pleasure in adding scopes, which destroy any variable created within at its end (there is no free or delete in Rust).
// This code in C will compile and run.
#include <stdio.h>
int main() {
int x = 5;
int* y = &x;
// do something with y
x = 6; // will not throw, but how does `y` know when new data is written? Undefined.
printf("%d", *y); // will output 6, but did we expect that? What if the value at that memory location was changed somewhere else in the code or even another thread, or was not yet changed?
return 0;
}
// Rust, however, prevents devs from doing dangerous things like that!
fn main() {
let mut x = 5;
let y = &x;
// do something with y
x = 6; // will throw, because there is a pointer to the memory location we change, oups!
println!("{}", y); // we want it to output 6
}
Above code will print the following error:
error[E0506]: cannot assign to `x` because it is borrowed
--> src/main.rs:9:3
|
5 | let y = &x;
| - borrow of `x` occurs here
8 |
9 | x = 6; // will throw, because there is a pointer to the content we change, oups!
| ^^^^^ assignment to borrowed `x` occurs here
// A correct solution, when a writeable pointer is needed
// Syncing across threads involves a bit of extra code, but will always be as safe as the below example
fn main() {
let mut x = 5;
{// scoped
let y = &x;
// do something with y
// for example `println!("{}", y);`, which will output 5
}// y is destroyed here
x = 6; // can write to x and will not throw
println!("{}", x); // will output 6
}
At least the compiler is very friendly and extremely helpful, since it explains what is wrong and often even offers a solution how to fix the problem. For example, if you use a wrong variable (or misspelled the name), it might ask you, if you meant a certain (other) variable and even outputs its name.
So even though learning Rust is hell, the overall user experience is quite good. At the time, there wasn't even any decent IDE support! Even today, there is only limited debugging support, but the compiler messages alone are helpful enough to build decent programs. Not having a debugger and the FP inspiration kinda enforce unit tests, so surprisingly the experience is rather good!
While learning Rust, I still had to write a lot of JS code. In the beginning, it was not that apparent to me, however with time and code reviews I noticed more and more potential for improvement in my old code, which is inspired by stuff I learned in Rust. Rust is not only an amazing language, but it is also a source of inspiration, which changes how I develop and improves my overall code quality in other languages, too! And only because I have to fight the compiler, until I learn to write good code :D
All in all, the language alone would be awesome, however today, in the age of people demanding good UX, just delivering a language and compiler is not enough anymore. At least since NPM, people want a simple way to include versioned libraries and have different tools and scripts available in a convenient fashion. Rust delivers. There is a tool suite called cargo, which mainly is the package manager, however also offers a number of additional tools and can be extended with plugins (for example rustfmt, which formats the source code according to style guides).
One thing, which still is missing, is a selection of stable libraries. Stuff, which should work just out of the box, like networking, graphics, etc., still is has no stable release. The language is quite young, and so is the ecosystem. However, the official Rust team supports important libraries and helps them reach the 1.0 milestone. That's awesome, and I expect 2018 to be a key year in that regard. However, not only libraries are important. What is also important are additional platforms. Rust is very cross-platform, and I find it harder to write platform-dependent code than cross-platform code. It just works by selecting a compiler target and compiling the code. Very simple. Plus there are very interesting targets, like WASM (wasm32-unknown-unknown) and AVR (in the works; Rust 4 Arduino, here we come)!
With important libraries being 1.0+ and many targets added, I am pretty sure that more and more professionals will start using Rust in favor of C or C++, wherever possible. Even today, there are reports of companies successfully rewriting their Python or Go applications in Rust and benefit from major speed-ups and a more stable code base.
As you can see, Rust still has a few rough edges, however the number of important advantages is dominant. The switch from C++ to Rust was time-consuming and difficult for me, however I have no regrets. As a bonus, since all the software does not exist, yet, and a lot of stuff is not ported, but rewritten from scratch to adhere to modern architecture and to improve the code base, I have the added benefit, that software written in Rust usually is opensource and makes use of the latest and greatest knowledge and improvements. For my game, I use the Amethyst game engine, which is centered around an ECS, so that it is able to scale to as many CPU cores as are available on x64 systems, while supporting Linux, Windows and macOS as primary targets (however probably even more untested ones). Even big engines, like UE4 and Unity still struggle with installing an ECS in their engines, even though in many places one would yield great benefits. The code, however, grew to what it is today, so a fresh start can be very useful!
So, to answer your question, I made the switch to Rust, because imho it is the next step in the evolution of programming languages. It adds important security guarantees, is tailored for modern hardware and performance is a key feature. On top of that, it delivers a tool suite, which is known from modern developments, like NPM, however even improves on them. That's what I want and need for my development, and that's why I use Rust.