Introduction

In a world where AI suppose to write code instead of you, to replace teachers, friends and lovers.
This book is an rubberducking for me to learn rust.
Someone told me once, when you can explain complicated concept in such way that others can understand easily then you have mastered that concept (I don’t remember who told me that).

The original idea was to touch those fundamental Rust concepts in order to facilitate the first impact of someone who have just started with Rust.

Chapters are organized in a didactical logic instead of cluster of logical topics, which means starting with small and simple concepts then evolving in more complex things.

When possible i will try to link directly the Rust Book in order to avoid duplication.
A lot is inspired by the official learning materials provided by Rust, please read them they are the best.

why rust?

Frankly there are too many video already available online 🤯.

A non exaustive list of good reasons:

  • Good defaults.
  • Restrictive but still possible to opt out.
  • Safe defined as non Undefined Behaviour.
  • Good tooling.
  • Maximized checks on compile time.

Some personal opinion: There is a good complain in the community that rust is slow to compile compared to C++, I would argument that the C++ compiler time is even longer: in order to make a safe C++ code it is necessary to run Static Code Analysis and Dynamic Code Analysis, since for each of this tool have several vendors that give different results, you may need to run more than one, now sum the time needed to run all this tools and the effort to rework the code to make it conform to the findings and you will have your answer of which language is the slowest to compile.

I am gloss over the huge effort of maintaining the tooling for C/C++ ultimately making the language inefficient:

Rust Lang shakes away that constant fear of shooting yourself on the foot.

Playground

For those that want to have an initial taste of Rust and who want to avoid the hustle to setup the there are a couple of possible playgrounds.

mdbook playground

You can edit and run rust code directly in the editable code block:

fn main() {
    // you can modify the code here!
    println!("Hello World!");
}

Container

You can use the container provided with the repository of this book.

git clone https://github.com/shelby1eo/ya-rust-introduction
cd ya-rust-introduction
make all-interactive
cd examples/hello-world/
cargo build
cargo run

Aquascope Playground

cognitive-engineering-lab provides an interactive visualizations of Rust programs called Aquascope Playground.

Install on your own host

Please follow the instruction provided by your distribution, here some good link:
nixos rust Installation
Rust book chapter Installation

VariablesDifferent way

Declaration

// print_type_of ref: ref: https://stackoverflow.com/questions/21747136/how-do-i-print-the-type-of-a-variable-in-rust
fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>());
}

fn main() {
    // The type is specified after the variable name.
    let a: u8 = 42;
    print_type_of(&a);
    // i32 is the default if not specified.
    let a = 42; 
    print_type_of(&a);
    // type can be coerced.
    let a = 42u64;
    print_type_of(&a);
}

Mutability

Variable is inmutable by default, followinf code will fail:

fn main() {
    let a  = 3;
    a = 6;
}

mut is used to make a variable mutable:

fn main() {
    let mut a  = 3;
    a = 6;
}

Scope & shadowing

Variables can be shadowed: it is possible to declare the same variable name, following portion of the code will only see the last declaration.

fn main() {
    let a:i32  = 1; let a_ptr: *const i32 = &a;
    println!("{} {:?}", a, a_ptr);
    let a: f32  = 2.0; let a_ptr: *const f32 = &a;
    println!("{} {:?}", a, a_ptr);
    let a  = 3u64; let a_ptr: *const u64 = &a;
    println!("{} {:?}", a, a_ptr);
}

Scope works similar to C++ which is the secrion contained by a {}

fn main() {
    let a:i32  = 1; let a_ptr: *const i32 = &a;
    println!("{} {:?}", a, a_ptr);
    let a: f32  = 2.0; let a_ptr: *const f32 = &a;
    println!("{} {:?}", a, a_ptr);
    {
        // this is a different scope, variables declated here are droped at the end of it.
        let a  = 3u64; let a_ptr: *const u64 = &a;
        println!("{} {:?}", a, a_ptr);
    }
    println!("{} {:?}", a, a_ptr);
}

Data Types

For deep dive please read The Rust Book Data types.
Here we are going to look in details to the following points:

  • u32
  • array/vector
  • String/Str
  • Stack/Heap

Numeric Types

To a first glance built-in integer types are very similar to the types available in C/C++. Table 2-1: Integer Types in Rust

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
arch-bitisizeusize

u32 basics

Actually rust build-in types are basically classes std u32.

Rust does most of the checks in compiling time, try to execute the following code blocks.

When possible overflow are detected at compile time.

fn main() {
    let a = 200u8;  // type specified
    let b = 200;    // type inference
    let _c = a + b;
}

The default reaction of rust is to panic in case of runtime error.

#![feature(strict_overflow_ops)]
fn main() {
    let a = 200u8;
    let b = 200u8;
    let _c = a.strict_add(b);
}

Types are more similar to classes with convenient basic functionalities.

fn main() {
    let a = 200u8;
    let b = 200u8;
    let c = a.saturating_add(b);
    println!("{c}");
}
fn main() {
    let a = 200u8;                  // by default all variables are immutable
    let mut c = a.checked_add(1);   // mut: to declare a mutable variable
    println!("{:?}", c);

    let mut c = (u32::MAX - 2).checked_add(3);
    println!("{:?}", c);
}

Array/Vector

Stack is faster, but it size need to be fixed at compilation time. Heap is more flexible (e.g. can be grown) but it is more costly in term of access time.

Array are located in the Stack:

Array cannot be grown, but you can slice it:

fn main() {
    let ji = ["Pollo", "Chicken", "Pavo", "Poulet"];
    let dinner = &ji[0 .. 3];
    println!("{:?}", ji);
    println!("{:#?}", dinner);
}

It is possible to put the variable into the heap:

  • Box: more efficient, efficient when want to transfer ownership of big data.
  • Vec: growable, has more feature than Box with the cost of using more memory.

Vec are over allocated because when reaching its maximun size the full array need to be reallocated and the memory copied.:

Play here:

#![allow(unused)]
#![feature(vec_into_raw_parts)]
fn main() {
    let mut array: [i32; 3] = [0; 3];
    let bytes: [u8; 3] = [1, 0, 2];
    let months = ["Pollo", "Chicken", "Pavo"];
    array[0] = 5;
    let boxed_slice: Box<[u8]> = vec![0; 100_000_000].into_boxed_slice();
    let array2: Box<[i32]> = Box::new([1,2,3]);

    // demo memory reallocation:
    let mut vec = Vec::new();
    let vec_ptr = vec.as_ptr();
    println!("pointer to non allocated memory to address {:?}", vec_ptr);
    vec.push(1); // by default this will over allocate a capacity of 4
    // allocate vec2 order to occupy the memory after vec so it will be forced to reallocate once hit the size 4
    let mut vec2 = Vec::new();
    vec2.push(5);
    let mut vec_ptr = vec.as_ptr(); // aliasing
    println!("1: {:?}", vec_ptr);
    vec.push(1);
    vec_ptr = vec.as_ptr();
    println!("2: {:?}", vec_ptr);
    vec.push(1);
    vec_ptr = vec.as_ptr();
    println!("3: {:?}", vec_ptr);
    vec.push(1);
    vec_ptr = vec.as_ptr();
    println!("4: {:?}", vec_ptr);
    vec.push(1);
    vec_ptr = vec.as_ptr();
    println!("5: cap is hit, reallocation and copy {:?}", vec_ptr);
    vec.push(1);
    vec_ptr = vec.as_ptr();
    println!("6: {:?}", vec_ptr);
}
the Rust syntax, is very explicit, this is one of the biggest complain about rust, but argubly it has its advantages. in the same time it is just straight forward WEIRD!!! look to the following two examples:

std::vec and std:boxed are logically related related, one is growable the other is not.
but the init syntax is very different:

fn main() {
    // std::vec init with no elements:
    let mut a: Vec<i32> = Vec::new();
    // std::vec init with elements:
    let mut a: Vec<i32> = Vec::from([1,2,3]);
    // std::vec init with element simplified by a macro:
    let mut a: Vec<i32> = vec![1,2,3];
    // std::boxed has predefined size
    // the type and the declaration looks so different!!!
    let mut a: Box<[i32]> = Box::new([1,2,3]);
}

String/Str

String

Rust has several String types, to go deep into the rabbit hole: All Rust string types explained.
Here we gonna stick to the simple std::String and std::str.

Lets Start by referencing good sources:

LSS(Long Story Short): In rust all strings are encode in UTF-8 we will gloss over the advantages and reasons, google docet.

The important thing to know is that the difference from ASCII where each symbol is represented by a byte, in UTF-8 a symbol can be represented by up to 4 bytes. UTF-8 encoding ref: wikipedia

    // lets take the following 3 examples:
    let hello = String::from("ciao"); // c(u0063) i(u0069) a(u0061) o(u006f)
    // 你(u4f60) 好(u597d) it seems that Chinese is not so complicated ^^.
    let hello = String::from("你好");     
    // न(u0928) म(u092e) स्(u0938 u094d) ते(u0924 u0947) you cannot count chars by eyes anymore.
    let hello = String::from("नमस्ते");
    // this is super critical.
    let hello = String::from("🐖💨💩");

String manipulation

Basic String manipulation

The official docs has some good example.

fn main(){
    // How to declare a string
    let mut string = String::new();
    println!("{}", string);
    let mut string = String::from("💩💩💩💩");
    println!("{}", string);

    // basic string manipulation
    let mut string = String::new();
    string.push('🐖');      // Push a utf8 character.
    println!("{}", string);
    string.push_str(", ");  // Push a string.
    println!("{}", string);
    string.push_str("नमस्ते");
    println!("{}", string);
    let ptr = string.as_ptr();  // Check address of the first character.
    println!("{:?}", ptr);
    string = string.replace("🐖", "💩"); // Will this allocate new memory? (next chapter)
    println!("{}", string);
    let ptr = string.as_ptr();
    println!("{:?}", ptr);
    let string2 = String::from("💩💩💩💩");
    string = string + &string2; // String concatenation. (next chapter)
    println!("{}", string);
}

different type of strings

Rust has several types of strings, here we gonna only to show the two basics ones:

  • std::strings is a string.
  • std::str is the string literal or a slice.

An exaustive explanation of all strings type can be found here All Rust string types explained.

Aquascope at the moment cannot render string literal, so we need look directly to the addesses to determine if it is allocated in the stack or heap.
fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>());
}
fn main() {
    // Rust has several string types:
    // string literal stored into the stack and they are of type &str
    let my_str: &str = "initial contents 🍄";
    print!("String literal has type: ");
    print_type_of(&my_str);
    // It can be converted in a String type
    print!("to_string will convert it to the type: ");
    let mut my_string = my_str.to_string();
    print_type_of(&my_string);

    my_string.push_str("aa");
    let string2 = String::from("123456");
    // it is also possible to get the pointer to the string container.
    println!("my_str_ptr =    {:?} stack", my_str.as_ptr());
    println!("my_string_ptr = {:?} heap", my_string.as_ptr());
    println!("String2_ptr =   {:?} heap", string2.as_ptr());

    // slices
    println!("");
    println!("##############################################");
    println!("slices std::str can point to both stack and heap");
    println!("my_str_slice_ptr =  {:?} stack", &my_str[1..my_str.len()].as_ptr());
    println!("String2_slice_ptr = {:?} heap", &string2[1..string2.len()].as_ptr());
}

Indexing into Strings

Rust do not allow you directly to index over a String. try to execute in order to get the compiler error:

fn main() {
    let hello = String::from("नमस्ते");
    let b = hello[0];
}

The reason is that a String in rust is more complex that a String in C which is just an array of char with a terminating ‘\0’. A String in Rust is implemented as a wrapper over Vec<>, to see the details click on the binoculars:

Furthermore since in Rust a String is encoded as UTF-8 not all characters are rapresented by a char, for example स् is rapresented by u0938 u094d. Rust took the choise to not allow direct indexing may lead to confusion.

As pattern that you will see by going deeper in Rust, the default behaviour of rust is very strict and safe, but it always allows you to do everything, it just requires an extra effort. A way to index an String is to use a slice:

fn main() {
    let hello = String::from("नमस्ते");
    let slice_hello = &hello[0..3];
    println!("{}", slice_hello);
    println!("{:?}", slice_hello.as_bytes());
    println!("{}", slice_hello.as_bytes()[0]);
}

The first logic question would be, why bother to slice it, why not allowing direct indexing? The answer is because during the slicing Rust perform for you a bondary check in such way that you would not slice a UTF-8 character in half. Try to execute the following code:

fn main() {
    let hello = String::from("नमस्ते");
    let slice_hello = &hello[0..2];
}

A more apropriate way to iterate over UTF-8 chars:

fn main() {
#![allow(unused)]
fn main() {
    for c in "[]Зд".chars() {
        println!("{c}");
    }
}

move-semantics

The title is a little misleading in this chapter we will focus on what the assignment operator = .

In Cpp there is the concept of move semantic for those familiar with it here some more readings:

Long Story Short, the move semantics move the ownership of heap allocated memory to the variable that is assigned to. The concept is the same as in C++ but rust have it as default behaviour so no need of extra syntax. For sake of optimization copy are avoided, but if necessary it need to be explicit.

typebehaviourcomment
scalarcopy
arraycopy
vectormoveThe original variable is not accessible anymore
StringmoveThe original variable is not accessible anymore
String literalcopy
In the string literal the data is not copied, only the memory related to the type get copied, in other word the pointer.

Scalar

Not much to say here, variables on the stack are not effected. For scalar there the move semantics is not really applicable since the memory get copied and the Copy trait is implemented.

fn main() {
    let a = 5;
    println!("{a}");
    let mut b = a;
    b = 1;
    println!("{b}");
    println!("{a}");
}

Array

Not much to be said here, memory allocated on the stack and the default behaviour is a copy. In later chapter will be shown how to avoid this memory copy via borrowing.

Vector

Finally things get interesting, in Rust when the memory is allocated on the heap the operator = will move the ownership of the memory to the assigned variable, note that the original variable will not be accessible anymore:

fn main(){
    let a = Box::new([1, 2, 3]);
    let b = a;
    println!("{:?}", b);
    println!("{:?}", a); // a have lost the access on that memory this line will not compile.
}

String

Since string is a wrapper over Vec the same behaviour happens here.

String literal

Current version of aquascope cannot render string literal so we have to check the pointer.
fn main(){
    let a  = "aaaa";
    let b = a;
    println!("a_ptr = {:?}", a.as_ptr());
    println!("b_ptr = {:?}", b.as_ptr());
    println!("{}", a);
    println!("{}", b);
}

Ownership

This is one of the fondamental concept of Rust!!!

In order to assure memory safety the following 3 rules are enforced by the compiler:

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

The operators that have an effect on the ownership are:

operatornameColumn3
=Assignmentwhen move, the ownership is also transfered
&Borrowownership is transfered. ownership is returned once new owner goes out of the scope

Scalar

Assignment operator =

Not much to be said about Simple Assignment: of a scalar, It is a copy the memory to another address on the stack. No ownership is transferred.

Borrow operator &

In rust the borrow-operator '&' and the derefence-operator '*' have very different meaning compare to C and C++, in rust & will also transfer the Ownership of the memory to the assigned variable.

ref: rust-lang borrow operator &. ref: rust-lang dereference operator *.

rust-lang defines the scope as: “A scope is the region of source text where a named entity may be referenced with that name.”
To keep it simple know a scope is the text enclosed by a pair of braces {} or when the variable is last used.

At the end of the scope of b the ownership is returned to a.

fn main() {
    let mut a = 5;
    println!("{a}");
    {
        let b = &mut a; // the ownership of the memory of a is transferred to b.
        *b = 1;
        // a is not accessible because memory ownership is transfer to b
        // println!("{a}"); // !!! try to uncomment this line.
        println!("{b}");
        println!("{}", *b);
        // b goes out of the scope, so a regain the ownership.
    }
    println!("{a}");
}

Array

Assignment operator =

Not much to be said about Simple Assignment: of a scalar, It is a copy the memory to another address on the stack. No ownership is transferred.

Borrow operator &

By borrowing it is possible to avoid unnecessary memory copy:.
At the end of the scope of b the ownership is returned to a.

Vector

Since String is just a wrapper to Vec, it will be omitted in this chapter.

Assignment operator =

The ownership is moved to b and at the end of its scope the memory is dropped.

fn main() {
    let mut a: Vec<i32> = Vec::new();
    a.push(1);
    println!("{:?}", a);
    {
        let mut b = a;
        b.push(2);
        println!("{:?}", b);
    }
    // println!("{:?}", b);
    // println!("{:?}", a);
}

Borrow operator &

At the end of the scope of b the ownership is returned to a.

fn main() {
    let mut a: Vec<i32> = Vec::new();
    a.push(1);
    println!("{:?}", a);
    {
        let b = &mut a;
        b.push(2);
        println!("{:?}", b);
    }
    // println!("{:?}", b);
    println!("{:?}", a);
}

Permissions

By default in rust all variables are not mutable which means that they have only read permissions.
Immutable variables enable the compiler to better optimize the code.
It is arguably a best practice in any programming language to make mutable variable only when it is necessary, defaulting to this behaviour avoid you to add const or constexpr infromt of all variable declaration therefore making the code cleaner.

When borrowing, depending on the mutability of the variable and the borrow variable it may remove the **Read** access from the original variable. Extensive examples are provided in the chapter.

Permissions-Scalar

mutable reference

Very similar to the const pointer to const variable in C and C++ but the positioning of the mut keyword is more intuitive:

  • Mutable reference: this means that the referenced memory can be modified.
#![allow(unused)]
fn main() {
    let mut a = 5;
    let b = &mut a;    
    *b += 1;
}
  • Mutable reference to immutable variable.
unlike C and C++ where a pointer to const pointer can be still modified if the pointer get reassigned to a non-const pointer, rust compiler will check this and not allow it, guaranteeing memory safety.
#![allow(unused)]
fn main() {
    let a = 5;
    let b = &mut a; // cannot borrow as mutable since a was declared as immutable.
    *b += 1;
}
  • b cannot be reassigned to reference another variable, because it is immutable.
#![allow(unused)]
fn main() {
    let mut a = 5;
    let mut c = 66;
    let b = &mut a; // immutable reference to mutable variable
    *b += 1;
    b = &mut c;
    *b += 1;
}
  • When b is mutable it can be reassigned.
#![allow(unused)]
fn main() {
    let mut a = 5;
    let mut c = 66;
    let mut b = &mut a; // mutable reference to mutable variable
    *b += 1;
    println!("{b}");
    b = &mut c;
    *b += 1;
    println!("{b}");
}

mutable reference effects on permissions

Lets take the following 4 usecases.

casesComment
b = &a;
b = &mut a;
mut b = &a;
mut b = &mut a;

b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. both a and b are accessible in R only.
  4. once b is last used in the current scope, a will recover its W permission.

b = &mut a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b are accessible inre R only but a lose the R permission.
  4. once b is last used in the current scope, a will recover its W permission.

mut b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b is reassigned to mut reference to c.
  4. b’s permission still is defined from the first assignment
Here a weird behaviour, since b is mutable, it should be possible to reassign it to a reference of mutable c. but the borrow checker do not agree with it. Is a feature or a bug?
#![allow(unused)]
fn main() {
    let mut a = 5;
    let mut c = 66;
    let mut b = &a;
    // *b += 1;
    println!("{b}");
    b = &mut c;
    a += 1;
    println!("{b}");
    *b += 1; // this line will not compile
    println!("{b}");
}

mut b = &mut a;

  1. a is declared.
  2. b takes ownership of a but with W permission.
  3. b is reassigned to mut reference to c
  4. b type is decided from the first assignment
Here a weird behaviour: it is not possible to assign to b a non mutable c.
#![allow(unused)]
fn main() {
    let mut a = 5;
    let mut c = 66;
    let mut b = &mut a;
    *b += 1;
    println!("{b}");
    b = &mut c;
    a += 1;
    println!("{b}");
    *b += 1;
    println!("{b}");
}

Permissions-Array

mutable reference effects on permissions

casesComment
b = &a;
b = &mut a;
mut b = &a;
mut b = &mut a;

b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. both a and b are accessible in R only.
  4. once b is last used in the current scope, a will recover its W permission.
fn main() {
    let mut a: [u8; 3] = [2; 3];
    let b = &a;
    // b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

b = &mut a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b are accessible inre R only but a lose the R permission.
  4. once b is last used in the current scope, a will recover its W permission.
fn main() {
    let mut a: [u8; 3] = [2; 3];
    let b = &mut a;
    b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

mut b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b is reassigned to mut reference to c.
  4. b’s permission still is defined from the first assignment
Here a weird behaviour, since b is mutable, it should be possible to reassign it to a reference of mutable c. but the borrow checker do not agree with it. Is a feature or a bug?
fn main() {
    let mut a: [u8; 3] = [2; 3];
    let mut b = &a;
    let mut c: [u8; 3] = [1; 3];
    //b[0] += 1;
    b = &mut c;
    //b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}
When reassiging the reference it must have the same size of the first assignment.
fn main() {
    let mut a: [u8; 3] = [2; 3];
    let mut b = &a;
    let mut c: [u8; 3] = [1; 3];
    b = &mut c;
}

mut b = &mut a;

  1. a is declared.
  2. b takes ownership of a but with W permission.
  3. b is reassigned to mut reference to c
  4. b type is decided from the first assignment
Here a weird behaviour, since b is mutable, it should be possible to reassign it to a reference of mutable c. but the borrow checker do not agree with it. Is a feature or a bug?
fn main() {
    let mut a: [u8; 3] = [2; 3];
    let mut b = &mut a;
    let mut c: [u8; 3] = [1; 3];
    b[0] += 1;
    b = &mut c;
    b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

variable scope

Since arrays are store in the stack the behaviour is similar to the scalar variables.

fn main() {
    let mut a: [u8; 3] = [2; 3];
    println!("{:?}", a);
    {
        let b = &mut a;
        b[0] += 1;
        println!("{:?}", b);
    }
    // println!("{:?}," b); // b is out of the scope and the memory is freed.
    println!("{:?}", a);
}

Permissions-Vector

Since String is just a wrapper to Vec, it will be omitted in this chapter.

mutable reference effects on permissions

casesComment
b = &a;
b = &mut a;
mut b = &a;
mut b = &mut a;

b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. both a and b are accessible in R only.
  4. once b is last used in the current scope, a will recover its W permission.
fn main() {
    let mut a: Vec<i32> = Vec::from([1, 2, 3]);
    let b = &a;
    // b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

b = &mut a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b are accessible inre R only but a lose the R permission.
  4. once b is last used in the current scope, a will recover its W permission.
fn main() {
    let mut a: Vec<i32> = Vec::from([1, 2, 3]);
    let b = &mut a;
    b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

mut b = &a;

  1. a is declared.
  2. b takes ownership of a but only with R permission.
  3. b is reassigned to mut reference to c.
  4. b’s permission still is defined from the first assignment
Here a weird behaviour, since b is mutable, it should be possible to reassign it to a reference of mutable c. but the borrow checker do not agree with it. Is a feature or a bug?
fn main() {
    let mut a: Vec<i32> = Vec::from([1, 2, 3]);
    let mut b = &a;
    let mut c: Vec<i32> = Vec::from([4, 5, 6]);
    //b[0] += 1;
    b = &mut c;
    //b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}
As opposed of Arrays Vector can re-reference to Vectors of different sizes.
```rust,editable fn main() { let mut a: Vec = Vec::from([1, 2, 3]); let mut b = &a; let mut c: Vec = Vec::from([4, 5]); b = &mut c; } ```

mut b = &mut a;

  1. a is declared.
  2. b takes ownership of a but with W permission.
  3. b is reassigned to mut reference to c
  4. b type is decided from the first assignment
Here a weird behaviour, since b is mutable, it should be possible to reassign it to a reference of mutable c. but the borrow checker do not agree with it. Is a feature or a bug?
fn main() {
    let mut a: Vec<i32> = Vec::from([1, 2, 3]);
    let mut b = &mut a;
    let mut c: Vec<i32> = Vec::from([4, 5]);
    b[0] += 1;
    b = &mut c;
    b[0] += 1;
    println!("{:?}", b);
    println!("{:?}", a);
}

Functions

Statement-Expression

some theory

Rust is a expression-based language. Fundamental definitions are:

  • Statements: are instructions that perform some action and do not return a value.
#![allow(unused)]
fn main() {
let a = 1; // is an statement.
}
#![allow(unused)]
fn main() {
let a = (let b = 1); // since it do not return anything, a statement cannot be part of an assignment.
}
  • Expressions: evaluate to a resultant value.
#![allow(unused)]
fn main() {
let a = {
    let b = 1;
    b + 1 // this is an expression.
    // an expression at the end of a scope is the return value of the scope.
};
}
#![allow(unused)]
fn main() {
let a: u32 = {
    let b = 1u32;
    b + 1; // the semicolon at the end transforms an Expression into a Statement.
};
}

For details please check the Rust Book.

Statement-Signature

basic function signature

The order of the variable name and the type is different that C and C++, it is positioned in order to allow you to read from left to right.

fn my_function(my_arg1: u32) -> u32 {
    if my_arg1 > 10 {
        // return keyword can be used for early return.
        return my_arg1 - 1;
    }
    // return and semicolon can be omitted.
    my_arg1 + 1 
}
fn main() {
    // modify the value of a and try to execute this code.
    let a = 10u32;
    let b = my_function(a);
    println!("{}", b);
}

Statement-Arguments

Passing argument by value

By default the arguments are passed by value, but attention, as we have already seen in the previous chapter, the operator = behaves differently depending on the type:

fn myfunc(arg1: u32, arg2: Vec<u32>){
    println!("{}", arg1);
    println!("{:?}", arg2);
}

fn main(){
    let mut a = 10;
    let mut b: Vec<u32> = Vec::from([1, 2, 3]);
    myfunc(a, b);

    println!("{}", a);
    println!("{:?}", b);
}

Passing argument by reference.

Pay attention to the dereferencing operator *. It is not necessary when indexing into an Vector.
fn myfunc(arg1: &mut u32, arg2: &mut Vec<u32>){
    *arg1 += 10;
    arg2[1] += 10;
    println!("{}", *arg1);
    println!("{:?}", *arg2);
}

fn main(){
    let mut a = 10;
    let mut b: Vec<u32> = Vec::from([1, 2, 3]);
    myfunc(&mut a, &mut b);
    a += 1;
    b[0] += 1;
    println!("{}", a);
    println!("{:?}", b);
}

Statement-Return

keyword return

The keyword return is not necessary for returning expression at the end of the scope. The keyword return can be used if to return before the end of the scope.

fn my_function(my_arg1: u32) -> u32 {
    if my_arg1 > 10 {
        // return keyword can be used for early return.
        return my_arg1 - 1;
    }
    // return and semicolon can be omitted.
    my_arg1 + 1 
}
fn main() {
    // modify the value of a and try to execute this code.
    let a = 10u32;
    let b = my_function(a);
    println!("{}", b);
}

Truple

A tuple is a collection of values of different types.
It can be useful to:

  • return several variables from a function
  • group variables that are logically bounded.

return several values

fn myfunc() -> (u32, Vec<i32>) {
    let a = 42u32;
    let b = Vec::from([8, 9]);
    (a, b)
}
fn main(){
    let c = myfunc();
    println!("{:?}", c);

}

Truple can be named

fn myfunc() -> (u32, u32) {
    let coord: (u32, u32) = (1u32, 2u32);
    coord
}
fn myfunc2() -> (u32, u32) {
    let x = 4u32;
    let y = 5u32;
    let coord: (u32, u32) = (x, y);
    coord
}

fn main(){
    let c = myfunc();
    println!("{:?}", c);
    let c = myfunc2();
    println!("{}", c.0);
    println!("{}", c.1);
}

as argument

fn myfunc(arg1: (u32, i32)) {
    println!("{:?}", arg1);
}
fn main(){
    let c = (1, -1);
    myfunc(c);
}

Ownership

We have already show that passing or returning a Vector or String would result also in passing the ownership of its memory.
Lets observe in the following example this interaction:

  1. In rust there are not function prototypes, so the order where the functions are written doesn’t matter.
  2. Pay attention to the ownership of the String.
    • it is extremely clear what is the in and out of the function.
    • runtime impact is minimum since the memory is moved instead of copied.
fn main(){
    let mut a: String = String::from("TA");
    a.push_str("C");
    println!("{:?}", a);
    let b = myfunc(a);
    println!("{:?}", b);
}
fn myfunc(mut arg1: String) -> String {
    println!("{:?}", arg1);
    arg1.push_str("OS");
    arg1
}

Struct

Truple Struct

Enum

ToDo: enum with different types

Error-handling

Lifetime

dependencies

ControlFlows

Just read the chapter in The Rust Book ^^.

shelby.leo@proton.me
github
buyMeACoffee

Placeholder

Annex-1

The documentation of the interpreter aquascope do not document the options, here are some example of usage.

interpreter

aquascope,interpreter,horizontal

aquascope,interpreter,concreteTypes

aquascope,interpreter+permissions,boundaries,stepper,horizontal

permissions

aquascope,permissions,stepper

aquascope,permissions,boundaries,showFlows

aquascope,permissions,boundaries,stepper

Annex-2 Rust vs C++

featurerustC++comment
constdefaultcost or constexpenable better compiler opt
move semanticsdefaultneed explicit std::move(var)the c++ syntax is heavy