Rust Basics: Taking the First Steps

󰃭 2023-06-29

Installing Rust

Getting Rust installed on your system can be done by following the guide from the official website https://www.rust-lang.org/tools/install.

As of writing this if installing on a Mac, Linux or WSL the recommended way is running this curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh on your terminal.

Verify the installation by opening the terminal and typing rustc --version. This should show the version of Rust installed on your system.

rustc --version
rustc 1.67.0 (fc594f156 2023-01-24)

Note: Rust is supported on various platforms such as Windows, macOS, Linux, and BSD.

Cargo

What is Cargo?

Cargo is the go-to package manager for Rust development. It simplifies the management of dependencies, building, and testing of projects, as well as the sharing of code libraries by publishing to crates.io - the public registry for Rust packages.

Creating a Rust Project with Cargo

To create a new Rust project with Cargo, follow these steps:

  • Open the terminal and navigate to the directory where you want to create your project.
  • Type the command cargo new <project_name> to create a new Rust project with Cargo.
  • Change into the project directory using cd <project_name>.
# cargo new hello

hello
   ├── Cargo.toml
   └── src
       └── main.rs

Once you have created a new Rust project with Cargo, you will see two main files in the project directory:

Cargo.toml: This file is the configuration file for your project. It contains information about your project, such as its name, version, and dependencies. You can add or remove dependencies as needed for your project by editing this file.

# Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

main.rs: This is the main Rust file for your project. This file contains the entry point for your Rust program.

// main.rs
fn main() {
    println!("Hello, world!");
}

That’s it! You now have a basic Rust project up and running using Cargo.

Running this is as straight forward as just typing cargo run on the console.

cargo run
   Compiling hello v0.1.0 (...)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Hello, world!

Not this shows that cargo is running a dev build to build a release version add --release to the cargo run command and you get an optimized release version.

cargo run --release

Variables

Variables in Rust are declared using the ’let’ keyword followed by the name of the variable, and an equals sign to initialize the value.

NOTE: Variables are immutable by default

Mutable and Immutable Variables

In Rust, variables can be either mutable or immutable. Mutable variables are declared using the let mut keyword, while immutable variables are declared using just the let keyword.

Example of Mutable Variables:

let mut x = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x after mutation is: {}", x);

Example of Immutable Variables:

let y = 5;
println!("The value of y is: {}", y);
y = 10; // This line would throw a compile error because y is immutable

Constants

Constants in Rust are variables whose value cannot be changed once set. They are declared using the ‘const’ keyword.

Example of Constants:

const MAX_POINTS: u32 = 100_000;
println!("The value of MAX_POINTS is: {}", MAX_POINTS);
MAX_POINTS = 50_000; // This line throws a compile error because MAX_POINTS is a constant

SIDENOTE: Rust’s helpful error messages One of the things I like so far is the helpful error messages that explain what’s going wrong in the code. e.g in the previous case when declaring variables, if you make a mistake, Rust provides an error message that explains the issue and how to fix it.

For example, in the below compiler message

error[E0384]: cannot assign twice to immutable variable `y`
  --> src/main.rs:29:5
   |
27 |     let y = 5;
   |         -
   |         |
   |         first assignment to `y`
   |         help: consider making this binding mutable: `mut y`
28 |     println!("The value of y is: {}", y);
29 |     y = 10;
   |     ^^^^^^ cannot assign twice to immutable variable

when trying to assign a value to an immutable variable, we not only get the error message but also a help section to add the mut keyword to y (pretty neat if you ask me).

Scopes

In Rust, scoping refers to the visibility and accessibility of variables and other identifiers within a program.

One way to create a scope in Rust is by using curly braces {}. Anything defined within the curly braces will only be accessible within that scope. For example:

fn main() {
    let x = 5;
    {
        let y = 10;
        println!("x: {} y: {}", x, y);
    }
    println!("x: {}", x);
    println!("y: {}", y);
}

In this code snippet, the variable x is defined in the main scope and is accessible within the whole function. The variable y is defined within the inner scope created by the curly braces and is not accessible outside of it. The second println statement for “y” will result in an error because y is not defined in the main scope.

Another way to create scope in Rust is by using functions. The variables defined within a function are only accessible within that function. For example:

fn main() {
    let x = 5;
    let y = scope();
    println!("x: {} y: {}", x, y);
}
fn scope() -> i32 {
    let y = 10;
    y
}

In this code snippet, the variable y is defined within the scope of the scope function and is not accessible outside of it. If we try to print y in the main function, the program will not compile because y is not defined in the main scope.

A bit out of the “scope” of fundamentals :)

When a variable goes out of scope, the memory that was allocated for it is freed. This process is automatically handled by Rust’s memory management system.

One of the key features of Rust’s memory management is that it ensures that a variable’s memory is only freed when the variable goes out of scope and is no longer accessible. This is known as deterministic destruction (I need to learn more about this) and it prevents common bugs such as use-after-free and double-free errors.

For example, in the following code snippet:

fn main() {
    let x = vec![1, 2, 3];
    {
        let y = vec![4, 5, 6];
        println!("x: {:?} y: {:?}", x, y);
    }
    println!("x: {:?}", x);
}

The variable y goes out of scope when the inner curly braces close. At this point, the memory allocated for y is freed and can be used for other purposes. However, the variable x is still accessible within the main scope, so its memory is not freed.

Memory safety

Some of the features that make Rust a safe language:

  • strong type checking
  • automatic memory management
  • and the language was designed with a focus on safety from the ground up.

The language’s ownership model and borrow checker ensure that memory is always properly managed and cannot be accidentally accessed after it has been freed.

The compiler guarantees these at compile, e,g in the following examples:

  1. Uninitialized Variables:
let x: i32;
println!("The value of x is: {}", x);
// we get
error[E0381]: used binding `x` isn't initialized
  --> src/main.rs:23:39
   |
22 |     let x: i32;
   |         - binding declared here but left uninitialized
23 |     println!("The value of x is: {}", x);
   |                                       ^ `x` used here but it isn't initialized
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider assigning a value
   |
22 |     let x: i32 = 0;
   |                +++

In the above example, x is uninitialized and the rust compiler tells us we should consider assigning a value to it and it recomments an example let x: i32 = 0;

  1. Ownership and Borrowing:
let mut s = String::from("hello");
let r1 = s;
let r2 = s;
println!("{} and {}", r1, r2);
s.push_str(", world");

In the above example, s is a mutable String. Attempting to assign r2 to s (which would usually work in a language like python) leads to a compiler error value used here after move. This section requires a whole page in itself so for now the rust docs will do.

Alternatively following the compiler’s suggestions does fix our problem for now.

  1. Dereferencing Null Pointers:
use std::ptr::null;

let x = null;
let y = x.deref();

error[E0699]: the type of this value must be known to call a method on a raw pointer on it
  --> src/main.rs:23:15
   |
23 |     let y = x.deref();
   |               ^^^^^

In this example, x is declared as a null pointer, which means it doesn’t point to any valid memory. Attempting to dereference x will result in a compile-time error, preventing the use of a null pointer.

Using modules in Rust

The Rust module system allows developers to organize their code into different modules, which can be stored in separate files. The use keyword is used to bring an item from a module into the current scope. Think of import in python.

For example:

// In a module called 'my_module'
pub fn my_function() {
    println!("Hello from my_module!");
}

// In the main.rs file
use my_module::my_function;

fn main() {
    my_function();
}

Note: Modules in Rust are private by default, to make them accessible outside their scope, the pub keyword must be added.

To import dependencies, we use Cargo. To add a new dependency, you can specify it in the Cargo.toml file, and then use the use keyword to bring it into the current scope.

For example, to use the rand crate from crates.io, you add the following to your Cargo.toml file:

[dependencies]
rand = "0.7.3"

Then in your Rust code, use the use keyword to import the items from the rand crate:

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    println!("Random number: {}", rng.gen::<i32>());
}

P:S Crates.io is the public registry for Rust packages (kind of like pypi if you come from a python background), where you can find and download libraries and dependencies.