Course 1: Rust Language

Rust Introduction: Cargo, Crates, Rust project, Hello Word, (2h on computers),

Pierre Cochard, Tanguy Risset

This course has been set up for students of the Telecommunication Department at INSA-Lyon (5th year), it is vastly inspired by the Rust book and many other resources on the web. It assumes that students do not have any programming experience in Rust, but have a strong programming experience in other languages (C/C++ and object-oriented languages in particular).

In addition to these documents, some others are available on Moodle https://moodle.insa-lyon.fr/course/view.php?id=10386, presenting the concepts covered in this course. Don't forget to check it before you start. Many of the information listed here come from https://www.rust-lang.org/learn/.

The course is organized in sections that have questions. In addition, you will find text boxes labeled Course:, in which important concepts are presented.

Course: What is Rust and Why Rust

Why should computer science engineers always learn new languages?

The use of the Rust programming language is growing exponentially, the number of useful libraries and projects that are available to developers is already huge. The main reason for that is that Rust provides safe memory management without a garbage collector, ensuring both performance and security. Its ownership system eliminates data races and segmentation faults. It enables efficient and concurrent programming while guaranteeing memory safety. It is associated with a powerful modern ecosystem and is suited for embedded systems, system programming as well as high-performance applications. Adopted by major industry players, Rust is emerging as a reliable alternative for secure and system-level development.

Setting up the environnement for using Rust

We're going to start by setting up the environment that will enable you to program in Rust. We recommend that you use Rust on your own machine, but the environment is already installed on the department computers.

This environment simply consists in having:

  1. An editor for programming, we strongly recommend Visual Studio Code that is available on all OS, for instance here: https://visualstudio.microsoft.com/fr/downloads/. Together with the rust-analyzer extension. See Appendix below for an introduction to Visual Studio Code.

  2. Install both the rust compiler (rustc) and cargo, with the rustup installer script.

cargo is Rust's package manager and build system. Its installation and versioning are managed by rustup.

Below is a summary for installing Rust and cargo on your laptop. The original complete instructions can be found here: https://doc.rust-lang.org/book/ch01-01-installation.html.

As a summary:

  • On linux or macOS, use the following command:

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

  • on Windows, go to https://www.rust-lang.org/tools/install and follow the instructions for installing Rust.

Course: What is cargo used for?

cargo manages many aspects that you need to understand to use Rust correctly:

  • Dependency Management: Cargo manages the dependencies of a Rust project. It automatically downloads and builds the required libraries and dependencies, making it easier for developers to include external code in their projects.

  • Project Configuration: Cargo uses a file called Cargo.toml to configure a Rust project. This file includes information about the project, its dependencies, and various settings.

  • Building and Compilation: Cargo handles the compilation process of Rust code. It can build the project, manage dependencies, and generate executable binaries. Developers can use Cargo commands like cargo build to compile the project or cargo run to build and run it in one step.

  • Testing: Cargo provides built-in support for testing Rust code. Developers can use the cargo test command to run tests defined in the project.

  • Documentation: Cargo can generate and serve documentation for the project using the cargo doc command. This is useful for both internal and external documentation (the HTML file that you are reading has been generated by cargo doc)

  • Publishing Packages: Cargo facilitates the process of publishing Rust packages to the official package registry, called "Crates.io." This makes it easy for others to discover and use Rust libraries and projects.

Rust Hello World

A Rust project is contained in a directory which has the name of the project. From now on, we suggest making a project in your home directory and keeping all your Rust code there.

We will use the cargo command to build our first hello_world project. Note that this is not mandatory, everything can be built by hand, the Rust compiler can be invoked without cargo by using the command rustc.

Execute the following command:

cargo new hello_world

Check the generated files:

  • Cargo.toml is the project configuration file written in the TOML (Tom's Obvious, Minimal Language) format1.
  • src/main.rs is the Rust main file (the program entrypoint).

Now, build your project with the cargo build command.

  • Where is the generated executable file?
  • What is the bang (!) after println?

Course: What about println!

As you can see on the Rust Standard Library documentation : println! is not a function, it is a macro.

Macros are called with a trailling bang (such as println!), they are a way of writing code that writes other code, which is known as metaprogramming. Understanding and declaring Macros is quite complex and will be seen later. But using them (for instance, println!) is usually very straightforward.

Variables, Types and Mutability

What is the problem with the program below? Does it compile?

let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");

What are the values printed by the program below?

#![allow(unused)]
fn main() {
let x = 5;
{
    let x = x + 1;
    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }
}
println!("The value of x is: {x}");
}

The notion of scope is quite important in Rust, a scope can be manipulated in the language as an object, we will see it in more details in TD4.

Functions in Rust

Functions in Rust have little difference with functions in other programming languages. They are declared with the fn keyword, and their parameters (arguments) are passed by value or reference.

Write a function fibonacci: fibonacci(n: i32) -> i32, which computes element n of the fibonacci sequence.

We will not use a recursive solution, but rather a for loop whose syntax will be: for i in 2..n+1 ( half-open range) and mutable variables.

We recall the definition of the fibonacci function fib:

fib(0)=1
fib(1)=1
fib(i)=fib(i-1)+fib(i-2)  for i >= 2

Generic types, traits and #derive directive

Just like in C++ with class and struct, Rust types can be defined and be implemented by using different methods. For instance, the following code defines the type Complex as a struct of two floats (type names begin with an uppercase character by convention).

#![allow(unused)]
fn main() {
struct Complex {
    re: f32,
    im: f32,
}

fn build_complex(re: f32,im:f32)->  Complex {
    Complex {re,im}
}       

let mut a = build_complex(2.3, 4.0);
println!("a = ({}, {})", a.re, a.im);
a.re = a.re+1.;
println!("a = ({}, {})", a.re, a.im);
}

Course: Generic Types

Like templates in C++, Rust enables the use of generic types in functions or struct, enums or method definitions. Here is a simple definition of a Point structure using integer or float coordinates:

#![allow(unused)]
fn main() {
struct Point<T> {
    x: T,
    y: T,
}

let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}

Modify the program of Complex creation above by using a type struct Complex<T> which uses a generic type

Course: Traits: Defining common behaviour

A trait defines a functionality or a behaviour that a particular type has and can share with other types. Traits are similar to a feature often called interfaces in other languages.

Many natural methods can be defined for any -- or at least many -- types. For instance the copy or clone methods (rust primitives) or the fmt method (of the trait std::fmt::Display, rust standard library) that enables to use println!. These methods are not defined by default when a new type is defined.

Traits are defined by the trait keyword. By convention they are named starting with an upper case, e.g. the Clone trait, it usually defines a method with the same name in lower case (here: clone())

If you want to clone a Complex, you juste have to write the implementation of the clone method:

impl Clone for Complex {
    fn clone(&self) -> Self {
        Complex{re: self.re, im: self.im}
    }
     // Now a.clone() can be used on Complex variables
}

Implement the Clone trait for the struct Complex<T> type defined before. You will have to use impl<T: Clone> to ensure that the T generic type implements the Clone trait.

By the way, do you know the difference between Copy and Clone?

Select your preferred answer:

  • Clone is a supertrait of Copy2

  • Copy is implicit, inexpensive, and cannot be re-implemented (memcpy). Clone is explicit, may be expensive, and may be re-implemented arbitrarily.

  • The main difference is that cloning is explicit. Implicit notation means move for a non-Copy type.

Course: Deriving traits

For certain traits3: (Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, etc.), the compiler is capable of providing basic implementations for some traits via the #[derive] attribute (In Rust, an attribute is metadata applied to code elements like functions, structs, modules, or crates, attributes are prefixed with # and enclosed in square brackets []).

These traits can still be manually implemented if a more complex behavior is required.

for instance, the Clone trait can be automatically derived for Complex type:

#[derive(Clone)]
struct Complex {
    re: f32,
    im: f32,
}

[... no need to implement Clone ...]

let a = build_complex(2.3,4.0);
let _c = a.clone()

[...]

How could we use println! to display Complex variables? Test the two following methods:

  1. Implement the std::fmt::Display trait for type Complex.

This will require to:

  • use std::fmt
  • search for the prototype of the Display trait
  • use the macro write! to print fields
  1. Derive the Debug trait that includes the fmt::Display trait and use the "{:?} format.

First example of Move semantics: the cube

In this first example we define a very simple data structure, a Cube with a single field c that indicates its size.

Write a Rust program that defines a structure Cube and prints its size

Can you print the cube itself? Can you write:

println!("My cube: {}", Cube{c:0.5});

If not, how can you make the cube printable?

Hint: You can derive the std::fmt::Display trait or the Debug trait

Define a variable x assigned to a given cube, print it and then define a second variable y defined by let y = x;. Then print x again, what is the problem?

Course: Move semantics

In Rust, move semantics refers to the ownership transfer of data from one variable to another. Rust enforces a strict ownership model where each piece of data has a single owner at a time, and ownership can be transferred (or "moved") when a value is assigned to another variable or passed as an argument to a function.

By default, an assignment such as let y = x implies a transfer of ownership of the content of x to y. The ownership concept will be studied further during next course.

In order to duplicate the cube (as it would be done in any language), one has to clone it or to implement the Copy trait. Deriving the Copy trait for Cube changes the semantic of the assignment: the assignment is now a copy, not a move.

Modify your program by cloning x into y

Introduction to Visual Studio Code

Visual Studio Code (often abbreviated as VS Code) is a cross-platform source code editor developed by Microsoft. It is compatible with Windows, macOS, and Linux, offering great flexibility to developers working in diverse environments. This lightweight yet powerful editor is designed to meet the needs of modern developers, providing a wide range of features.

Key Features of Visual Studio Code

Visual Studio Code stands out due to the following features:

  • Built-in support for multiple programming languages: VS Code supports a wide array of languages such as Python, JavaScript, C++, Java, and more, thanks to its extension system.

  • Extensions and customization: A vast library of extensions is available to add functionalities like debugging, version control, and language-specific tools.

  • Integrated debugger: VS Code provides an interactive debugging environment to simplify error correction in the code.

  • Version control integration: Seamless integration with Git and other version control systems allows developers to track code changes directly within the editor.

  • Integrated terminal: A terminal is available inside the editor, enabling command execution without leaving the application.

  • IntelliSense: This feature offers intelligent code completion and contextual suggestions based on syntax and variable types.

  • Cross-platform compatibility: VS Code works consistently on Windows, macOS, and Linux, ensuring a uniform user experience regardless of the operating system.

Thanks to its intuitive interface and powerful tools, Visual Studio Code has become one of the most popular editors among developers, whether they are beginners or experienced professionals. Its active community and frequent updates make it a reliable choice for addressing the evolving needs of software development.

How to use VS code efficiently for Rust

Installation with snap (Linux):

sudo snap install code --classic
  • Open the project directory:

    code myproject
    
  • Add the Rust Analyzer extension (left bar, small squares), search rust → install rust-analyzer

  • Go to Explorer

  • Ctrl-Shift-P: command palette

  • Ctrl-P: search for a file

  • Ctrl-Shift-E: explorer

  • Ctrl-J: command line inside VS Code

  • Ctrl-Shift-I: indent the whole Rust file

To compile:

  • Either use the Run button above main (Rust Analyzer)

  • Or Ctrl-Shift-P and type Run, then select

  • Or Ctrl-J and type:

    cargo build
    
1

You can have more documentation about the TOML format here: https://toml.io/en/ or here in french: https://toml.io/fr/. However, it is probably not necessary, TOML is quite simple to understand