DEV Community

Cover image for Rust programming language: what is it & how to learn it?
Igor Cekelis
Igor Cekelis

Posted on • Originally published at barrage.net

Rust programming language: what is it & how to learn it?

Rust is a low-level systems programming language. While that might make Rust seem limited, it can be used to build many different types of applications. Rust is a tool, and tools are chosen depending on what you want to build.

First of all, what is Rust?

As mentioned before, Rust is an open-source systems programming language. Rust aims to be memory-safe, thread-safe, fast, and secure. To achieve this, it introduces some new concepts, like ownership, borrowing, and lifetimes, which are the main things that keep Rust memory safe. These concepts might seem foreign if you have not seen them explicitly as you will in Rust.

Thanks to these concepts, many errors in Rust are compile-time errors rather than runtime errors.

Rust is a statically typed language, unlike JS, Python, Ruby, or Objective- C. As your code compiles, you will get compile type checking, and the compiler will let you know if you have any unhandled errors. Rather than re-running your application and trying to replicate an error that happened, you can spend more time writing the correct code.

Let’s talk about ownership. In Rust, the compiler keeps track of which data “lives” in which scope or context. Because of this, you do not have to keep track of dangling pointers or references to parts of memory, which, if left unchecked, could leave you with segmentation faults or memory leaks.

For example, in C, you have a function that returns a reference/pointer to some data. Then you call another function on that same reference, the code works, and everything seems fine, but little do you know the first pointer (returned from the function) has now been freed. This leaves you with a dangling pointer, and if you are not mindful, you could accidentally try to use that pointer again, and you will end up with some unexpected behavior.

The Rust compiler keeps track of what function or what context holds what data at any given moment, so something like this most likely would not happen. At least not so easily, as the Rust compiler will let you know during compile-time that you have an error that needs to be handled.

This is where ownership comes in; as mentioned before, a context or function can hold or own data, meaning the data lives in the function’s scope. Rust will not let us access that data outside of that scope unless we explicitly say so. And even then, we need to explicitly tell the compiler how we want to use that data. This is called borrowing in Rust, and that’s what makes Rust memory safe. Because all data lives in its own scope/context, once we move out of it, Rust will look at all of the data inside that scope and deallocate it.

Here is an example of returning a reference to a string (&str) from a function and using it in another (main) function.

fn hello_v1() -> & str{
    "Hello, world!"
}

fn main() -> (){
    let message = hello_v1();
    println!("{}",message);
}
Enter fullscreen mode Exit fullscreen mode

The example above will not work because the “Hello, world!” string is deallocated after the “hello_v1 function is finished, so we cannot print it. Also, the error we get clearly states that we are missing lifetime parameters, so let's add them.

fn hello_v2<'a>() -> &'a str{
    "Hello, world!"
}

fn main() -> (){
    let message = hello_v2();
    println!("{}",message);
}

Enter fullscreen mode Exit fullscreen mode

Once we change our function and add the lifetime parameters, the compiler knows that we need that string to live outside of that function, so we get the message “Hello, world!” in our console.

Rust does not have an automatic garbage collector like some other languages, Java or Python, for example.

We don’t have to manually free or deallocate any memory. If we want data to live outside the function it was created in, we must tell the compiler explicitly that we do not want this data deallocated.

This might seem complicated at first, but you won't even notice it once you start writing the code.

What is Rust used for?

More than a few projects are created using Rust, and some of the well known are:

  • Mozilla built its browser engine called Servo.
  • Figma’s real-time syncing server, which is used to edit all Figma documents
  • An open-source virtualization technology called Firecracker is mostly being written in Rust.
  • NPM also uses this language to alleviate some of its CPU-bound bottlenecks.

Rust vs. Go

The most obvious difference between Rust and GoLang is simplicity. Becoming productive in Go takes much less time than it does in Rust. However, simplicity comes at a cost as Go lacks some Rust features like generics and functional programming.

Another difference is in memory management. Go has a garbage collector, while Rust’s memory management, as explained above, comes in the form of ownership and borrowing. While this might give an edge to Rust in performance, speed and flexibility, it can also be a setback in some cases.

Concurrency in programming, simply put, is the ability to execute more than one function or task simultaneously. Go has great support for concurrency in the form of Goroutines and channels.

While both of these features are also available in Rust (either using the standard library or third-party crates like Tokio), the main difference is, once again, simplicity. Writing concurrent applications in Go is easier than in Rust. Still, Rust, on the other hand, offers compile-time checking, being able to catch thread-safety bugs even before your program runs.

Considering compilation time, Go blows Rust out of the water, as the Go compiler does not have to run all the optimization checks the Rust compiler does. One thing they have in common is that they both produce a static binary as an output, which means that in order to run the compiled program, you don't need an interpreter or a virtual machine. Go is very well suited to build services and simple applications. For example, a web REST API was built to replace Java and C#.

Another key difference is that Go does not support macros, while Rust has a very powerful macro system. Rust is a systems programming language; therefore, it's a very good fit when you need efficiency and performance. Rust is very well suited for performance-critical applications such as web browsers, databases, operating systems, or libraries that rely on heavy mathematical calculations.

This does not mean that you can’t use Rust to build a web application, as Rust has great support for building web APIs in the form of third-party crates.

Rust vs. C++

Both Rust and C++ are system programming languages, which means they can write low-level code like operating systems and firmware for microcontrollers. Compared to C, both languages offer a lot of abstractions that make it possible to go high-level and write game engines and web applications.

Another similarity is that neither of them uses a garbage collector to manage memory. This makes code more efficient and faster. If you have ever used C, you will know that managing memory yourself is hard and often results in undefined behaviors or segmentation faults.

For this reason, C++ introduced smart pointers to mitigate some memory-related bugs. However, they are still limited in the number of guarantees they offer. Rust goes a step further and introduces the borrow checker (ownership, borrowing), preventing most of the memory safety bugs.

Another selling point for Rust is its rich type system, making it possible to prevent data races at compile time. Rust introduces two traits, Sync and Send. A type is Send if it is safe to send to another thread, and a type is Sync if it is safe to share between threads. This makes sharing memory between threads possible, but the compiler will prevent you from doing so unsafely.

This example shows how sharing the number between threads would be unsafe as the RefCell type is not Sync.

fn main() {
    let mut number = std::cell::RefCell::new(2);

    let new_thread = std::thread::spawn(|| {
        let mut reference = number.borrow_mut();
        *reference = 5
    });

    let mut reference = number.borrow_mut();
    *reference = 5;

    new_thread.join();
}

Enter fullscreen mode Exit fullscreen mode

We get this error:

let new_thread = std::thread::spawn(|| {
    |                      ^^^^^^^^^^^^^^^^^^ `RefCell<i32>` cannot be shared between threads safely
Enter fullscreen mode Exit fullscreen mode

This also highlights how great the Rust compiler is as it tells us exactly what the problem is.

This is an example of how to share data between threads and changing it:

fn main() {
    use std::sync::{Arc, Mutex};
   let number = Arc::new(Mutex::new(5)); // this number is in the main thread

   { 
        let number_copy = Arc::clone(&number);
        let new_thread = std::thread::spawn(move || { // create a new thread and pass in the num
            let mut reference = number_copy.lock().unwrap();
            *reference *= 5 // here we multiply our starting number by 5
        });
        new_thread.join().unwrap();
    }
    println!("{}",number.lock().unwrap().clone()); // now the starting number is 25

    *number.lock().unwrap() *= 5; // we multiply the starting number by 5 again

     println!("{}",number.lock().unwrap()) // here the number is 125    
}

Enter fullscreen mode Exit fullscreen mode

Another “version” of Rust called Unsafe Rust is more similar to C++. Working in Unsafe Rust is like telling the compiler to trust you and skip some of the checks it provides. You lose the safety guarantees a safe Rust compiler gives you, but you gain the ability to interact with the low-level aspects of the operating system/hardware. Those operations are inherently unsafe. Rust’s compiler is very conservative in its checks, meaning that it prefers to check and block a few valid programs/operations rather than allow many unchecked operations. This means that even if we know that some code is safe to execute, Rust might still not allow it unless we use unsafe Rust.

The areas where Rust definitely beats C++ and many other languages are package management and documentation. The official package manager in Rust is called Cargo. Using a package is as simple as adding a line to the cargo.toml, Rust's config file. Documentation for Rust is on a whole other level compared to any other language; everything can be found at doc.rust-lang.org.

Using an external library with C++ can be an issue, especially if you’re targeting multiple operating systems. There are some third-party options like Conan or Vcpkg, but they are far from being as standardized and easy to use as Cargo.

Of course, the C++ ecosystem is much larger. There are many more libraries for C++, so there might not be a library for something that already exists for C++. Rust does allow for FFI (foreign function interface), which allows you to interface with C code from Rust and thus interface with C++ libraries; however, this functionality is still limited for more complex cases.

Another similarity is macros. Both C++ and Rust allow them, but Rust’s macros are considered to be much more powerful and safer.

Rust has two types of macros: declarative and procedural.

Declarative macros are similar to ones in C++, but the key difference is that macros in Rust are hygienic in the sense that they can not interact with variables outside of their scope and cause any unwanted behavior.

Procedural macros are much more powerful and complex. They act more like functions: they accept code as an input, manipulate it, and return the enriched code as an output, all at compile time.

In conclusion, C++ is used far more often than Rust. That said, big companies like Microsoft, Google, and Apple are gradually integrating Rust with their products. C++ is not going away any time soon, thanks to its large ecosystem and legacy code built around it. Rust, however, is slowly beginning to be used as a system programming language.

What is the best way to learn Rust programming?

  • The most logical way to start learning Rust is to read the Rust book.
  • Depending on if you already have a specific application you want to build, you might want to skip the macros section and Unsafe Rust.
  • As always, start small. One web application I have built as a practice is a to-do list manager.
  • There are many useful sites where you can practice, like exercism.io or codewars.com.

As mentioned before, Rust has many useful third-party crates and tools; however, it already has a decent number of frameworks depending on what you want to do. Learning about them is the path you want to take next.

Rocket - a web framework built on the nightly version of Rust; it's boilerplate-free, type-safe, and has a large ecosystem. It also features rich, supporting cookies, streams, built-in templating, and JSON types.

Actix – a web framework also aimed to be more stable than a rocket; however, you will need to use third-party packages as it is newer and has less support.

Gotham – flexible web framework built on stable Rust, statically typed, and type-safe. Supports Async operations by using the Tokio project and Hyper.

Amethyst - is a game engine; it has a pool of features you might need to build a larger application. It also has better support for third party libraries.

Bevy - an open-source, newer, simple, data-driven game engine, heavily inspired by amethyst, supports real-time 2d rendering, 3d rendering, multiple platforms (Windows, Mac, Linux, and soon iOS and Android). Hot reload with fast compile times.

Druid - an experimental, data-oriented, Rust native UI toolkit. Based on Flutter and SwiftUI. Its current development is largely driven by its use in Runebender (a new font editor).

Top comments (0)