What kind of language is Rust?

There are a number of systems programming languages that do not enforce a runtime environment with a scheduler and garbage collector. The most ubiquitous are C and C++. Rust can be used to replace either of them and improve both your productivity and safety at the same time.

Its data model is based on values rather than memory objects. In this respect it is somewhat similar to modern versions of C++ but better. Values can be passed to functions and returned from them. They can also be moved from stack storage to heap storage and back. Or they can be moved into data structures and out of them.

On the other hand, if you come from C or object-oriented languages, feel free to call them objects. They serve the same purpose and are for all practicle purposes the same thing.

Compared to other systems languages

It has a comprehensive type system. Each value in your program has a defined type. Unlike standard C, types can have destructors. Local values are therefore created at some point of the code and destroyed at the end of the block unless they get moved before the destruction point. Destruction code is guaranteed to run when the value reaches the end of its lifetime.

Rust code is generally much safer than C or C++. The language is designed to be very strict, memory-safe and thread-safe. Unlike C or C++, all your “safe” code is guaranteed to be memory-correct and thread-correct at the compile time. The compiler is responsible for the safety of your code. You are only responsible for safety of code that you mark “unsafe” to lift some of the restrictions.

Compared to higher level languages

You are still responsible for memory management in many ways. You decide which values are stored on the stack and which use dynamic allocations on the heap. This makes Rust different from languages like Python, Java or C# where you don’t need to (and cannot) control the storage.

In order to both meet the static safety requirements and designate memory usage you need to use additional tools from the standard library that are written in unsafe Rust by the library authors and your code is checked by the compiler for the correct usage of these tools.

Value ownership and move semantics

When you pass a value to a function, it is moved. It can no longer be used in your code unless it gets passed back. The same happens with other language constructs that consume your value. Let’s look at an example with a list of strings.

fn main() {
    # Initialize the list with two values
    let mut list: Vec<String> = vec![
        "alpha".to_string(),
        "beta".to_string(),
    ];

    # Add another value
    list.push("gamma".to_string());

    # Consume the list
    for number in list {
        println!("{}", number);
    }
}

This for-loop iterates over the list by consuming it. It is nothing else than syntactic sugar for calling .into_iter() on the Vec<String> converting the list into a consuming iterator. After consuming the list isn’t empty. It no longer exists. This is different from the move semantics in C++.

Note the mut keyword. You cannot modify the list unless you explicitly mark it mutable. You don’t need to mark it mut if you’re not going to modify it. Note that this only has local meaning. You can convert immutable owned values to mutable owned values easily. Also note that println!(…) is a macro rather than a function and that in Rust you can distinguish macro calls by the exclamation mark.

Value borrowing and references

You can avoid the move by forcing Rust to call .into_iter() on a reference &Vec<String> rather than the Vec<String> itself. You could also just use list.iter() to explicitly create a borrowing iterator.

fn main() {
    let list = vec![
        "alpha".to_string(),
        "beta".to_string(),
        "gamma".to_string(),
    ];

    for number in &list {
        println!("{}", number);
    }

    for number in &list {
        println!("{}", number);
    }
}

Note that you don’t have to specify the Vec<String> type if it is clear from the context. You can borrow the same list repeatedly. You could even access the list inside the for-loop. That’s only possible because immutable references are shareable and therefore you can have multiple such references to the same value.

Mutable references

Mutable borrowing works the same way as immutable borrowing except that in Rust a mutable borrow is exclusive. You cannot create multiple mutable borrows of the same objects that could overlap in time. This is to guarantee thread safety right in the language.

fn main() {
    let mut list = Vec::new();

    init_list(&mut list);

    for number in list {
        println!("{}", number);
    }
}

fn init_list(list: &mut Vec<String>) {
    list.push("alpha".to_string());
    list.push("beta".to_string());
    list.push("gamma".to_string());
}

Once init_list(…) call is finished, the mutable borrow is no longer in effect and you can once again do anything with the value. You can borrow it again or you can even consume it by moving into a for-loop. When working with Rust you have to understand when you own a value, when you move it and when it’s borrowed using a reference for reading or modification.

Note that the Vec<_> item type String is obvious from the call to init_list() and picked up by Rust even if it is only know later in the code. Also note that main() calls init_list(…) even if it is defined later in the source file. Rust isn’t particularly order about code order and doesn’t require forward declarations. Whether you choose to write functions top-down or bottom-up is up to you.

Library tools like RWLock or RefCell overcome the exclusivity of mutable borrows by using interior mutability through unsafe code to provide sharable data that can be modified.

How to use Rust tools

At the beginning, you can use one of many online tools for experimenting with Rust code.

  • Rust Playground
  • Compiler Explorer
  • ReplIt

When you need a proper project, you download the tools for any major operating system. You don’t need to interact with rustc compiler directly unless you’re doing something very advanced. All you need is cargo and its subcommands. Look up the details in the documentation or check additional online sources. Bring any questions to the next lessons or send them via e-mail.

https://www.rust-lang.org/learn

Quick introduction:

  • Create new project using cargo new example && cd example
  • Modify src/main.rs and possibly other files.
  • Run cargo run and see what happens.

You may look at cargo-script and similar projects for support for single-file Rust programs. Get familiar with the ReCodEx system for submitting homework. You will be submitting solution.rs module with your code that would be tested automatically.

Takeaways

  • Rust uses stack-based values that support safe ownership, moving and borrowing.
  • Code is organized freely. No headers, no forward declarations needed. Rust will link things together.
  • Knowledge of C++ may help you understand the design of Rust but is not required and may even be misleading.
  • To make your life simple you should accept Rust’s design and avoid trying to fight it.
  • Converting values between types is explicit. The type system is very strict but flexible.