Rust Essentials

My recall of what are essentials in Rust. It is a learning documentation. It is "recall and spaced repeation" approach.

Why

In C#, a language I have been coding for years, and of course, love it, a common error is NullReferenceException. It means your code holds a reference that points to "nothing", there is nothing in that location in the memory. In other word, it is invalid.

Memory is a limited resource and many programs try to get a share. Memory magement is crucial to any programming language. Unused memory must be returned to the operating system. In C#, there is Garbage Collector (GC). It has some overhead on your program. I am far from knowning the detail here.

So far, as I know, Rust was designed with those problems in mind. The language makes it impossible for developers to make those mistakes.

Heap and Stack

They are two structures of memory to store data. Stack is designed to store fixed-size values such as numerics, literals. Heap is designed to store unknown size data, known as Reference type.

When your code declares a reference type, what the code holds is a pointer stored on the Stack. The pointer knows where to look for the value on the Heap. And NullRefrenceException is when the point is on the Stack, but the location on the Heap has gone.

Heap and Stack are essential concepts for developers to understand programming languages.

Mutate or Immutable by default

Another common, nasty problem is the "race condition" – many paths (code) modify to the same location (data). Imagine that you have some money in your pocket. Someone else "access" your pocket and get some of it without your knowing. Surprise! It is a host of unexpected errors especially in the production.

Rust is designed with "immutable" by default. This code is valid in many programming languages except Rust

let name: String = String::from("Thai Anh Duc");

name.push_str("Oh No!");

Rust requires your "awareness". If you intend to modify something, say it explicitly with the mut keyword.

let mut name: String = String::from("Thai Anh Duc");

name.push_str("You Rock!");

Ownership

This concept was new to me. I have not thought of ownership at the programming language level. The "Ownership" concept appears to me when I do the domain design, data model.

A value always has one and only one owner in a scope. A scope is denoted by . Once the scope ends, everything inside the scope is dropped unless ownership is moved.

Ok, so who is the owner? and what does it own? Let’s look at this code

let name: String = String::from("Thai Anh Duc");

There is a variable name which is a String that has a literal value "Thai Anh Duc". In short, there are "variable" and "value".
"Variable" is the owner.
"Value" is owned by the variable (the owner).

let name: String = String::from("Thai Anh Duc");
// This works fine because name is the owner
println!("My name is {name}");

// owner is moved to
let it_is_mine: String = name;

// This code will not compile because what name owns was moved to it_is_mine.
// In other word, name points to nothing
println!("My name is {name}");
// This is ok
println!("My name is {it_is_mine}");

The ownership is applied for reference types. Value types are different. The value is copied. Each variable owns its own data. The below code works fine

// This is a literal of string, it is fixed-size. The value is stored in the stack
let name = "Thai Anh Duc";
// This works fine because name is the owner
println!("My name is {name}");

// Another copy of data is created and copied_name owns the new data
let copied_name = name;

// Both work fine
println!("My name is {name}");
println!("My name is {copied_name}");

Move

The transfer of ownership is a Move. The actual data is unchanged (the data on the heap).

Copy

Another copy of data on the stack is created for the new variable. Both variables operate on their own data. It is safe.

Drop

When a scope ends, Rust performs a "Drop" to release memory allocated in the scope. Notice that a reference type is clean up unless its ownship is moved to the consumer. It is done by returning a value.

fn main(){
    let name: String = String::from("Thai Anh Duc");

    let hi = move_ownership(name);

    println!("{hi}");
}

fn move_ownership(name: String) -> String{
    let say_hi : String = String::from("Hi: ") + &name;

    say_hi
}

Reference

In the previous example, the &name is used to access the location of the variable. It is the pointer to a location on the heap

Bottom lines

I recalled and documented what I have learned from Rust Understanding Ownership.

Rust Data Types

The official reference is here. It is a wonderful and well-written resource. As a learner, I want to write things down in my own languages or repeat what has written there.

Rust is a statically typed language. Just like Java, C#, … it must know the types of all variables at the compile time. Usually, the compiler does that by two means. One is that types are supplied explicitly. The other is by inferring from the value, the complier makes the best guess from a value.

I usually prefer the explicit type approach, especially numeric values.

// Explicit declaration
let age: i32 = 38;

// Implicit declaration. The compiler will figure out the type
let age = 38;

A few things from that simple statement

  • let: Rust keyword to define a variable
  • age: variable name
  • :: seperator between variable name and type
  • i32: variable type, in this case, it is a 32bit integer. Variable and type are seperated by a colon :
  • =: assignment, assign a value to the variable
  • 38: variable value

Shadow and Mutate

Shadow is the ability to define a new variable with the same name. It means that the "new" variable can be of a different data type.
Rust is immutable by default. To mutate a variable, it might be declared with mut keyword. It requires extra attention from the developers. You should know what you are trying to do.

fn shadow() {
    let x = 5;
    let x = x + 1; // which is 6

    {
        // Create an inner scope
        // shadow the x variable
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x in the outer scope is: {x}");
}

fn mutate_scope() {
    let mut x = 5;
    x = x + 1; // which is 6

    {
        // Create an inner scope
        // shadow the x variable
        x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x in the outer scope is: {x}");
}

Tupe and Array

Tupe allows developers to construct a new structure (or data type) that holds different data types.
Array is a fixed size structure of the same data type.

fn data_types() {
    // Besides the normal types seen in many other languages, tupe and array are interesting to explore here
    let tup: (i32, &str) = (38, "Thai Anh Duc");
    println!("Hi, my name is {}, {} years old", tup.1, tup.0);

    // Deconstruct tup into individual variables
    let (mut age, mut name) = tup;
    println!("Hi, my name is {name}, {age} years old");

    age = 10;
    println!("Hi, my name is {name}, {age} years old");

    name = "TAD";
    println!("Hi, my name is {name}, {age} years old");

    // Array is fixed size
    let _months = ["January", "February", "March", "April"];
    // Auto generated values from a feed one
    let auto_values = [10;3];
    println!("{}", auto_values[2]);
}

With those basic data types, one can write an application.

Rust on Linux

Enter the new uncomfort zone with Linux and Rust. This post documents the process and will serve as a reference for later usage.

Setup a Linux box

Having another machine with Linux operation system is cool but not necessary. Welcome to the Windows Subsystem for Linux 2 (WSL 2).
I am on Windows 11, I follow the instruction from Microsoft Docs.
With the Terminal and Powershell installed, run the command

wsl

The terminal displays the help with possible commands. I want to understand them a bit before actually executing any command.

wsl --list --online

Displays all the distributions. The Ubuntu is the default if none is specified. Let’s use the default. Remember to run as Administrator

# Explicitly specify the distribution for clarity
wsl --install --distribution Ubuntu

Nice! Installed and rebooted.

Create a folder (dir) to store code

$ mkdir code

Visual Studio Code with WSL

I am following the document here.

  • Install Remote Development extension
  • Navigate to the Ubuntu terminal, type code .. Magic begins

Rust on Linux

The Rust documentation is rich. I follow its programming book

$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

After successfully installed Rust on Linux box, I need to restart the shell. It is to ensure that system understands the new environment variables. Otherwise, it does not understand the changes

# This will not recognize as a command
$ cargo
# Reset the profile/shell (bash shell)
$ source ~/.profile
# This will work
$ cargo

Write some code and struggle

fn main() {
    println!("Hello Rust 101");
}

I got the first error "linker ‘cc’ not found"
Ok. Get it the linker

$ sudo apt install build-essential

Enjoy the fruit

# Compile the code
$ rustc ./main.rs

# Run it
$ ./main

Summary

So what have I accomplished so far? I have setup a new development environment which includes

  • A Linux box running on Windows 11 using WSL 2
  • Visual Studio Code remote development. It allows me to stay on the Windows and write code in the Linux box. It is neat and straightforward. VS Code is amazing
  • Install and write a "hello word" rust application

What a great way to start a weekend!