Rust Mastery Week #1

SOUMITRA-SAHASOUMITRA-SAHA
14 min read

Hi there! My name is Soumitra Saha. You can find me on LinkedIn, Twitter, and GitHub. I am a Full-Stack TypeScript Developer with over 2 years of experience. In this blog, I will document my journey of learning Rust. I will post updates on my progress every week, either on Saturday or Sunday.

Why I am Learning Rust?

I'm working on a new side project to build a desktop application using the tauri framework. During this process, I decided to learn Rust for its native functionality. Through my research, I discovered that Rust is incredibly fast, similar to C and C++. Since C++ was my first programming language, this motivates me even more to learn Rust. I've also heard about Rust's excellent memory management and security features. It is widely used in low-level systems, which adds to my interest.

Rust Installation

First Install the rust from this site https://www.rust-lang.org/

  • Then go to Get Started

  • And follow the steps that are there, If you need additional help

Take help from YouTube for Rust Installation.

The following steps install the latest stable version of the Rust compiler.

Now check the version for confirmation of the installation

$ rustc --version
$ cargo --version

VS Code setup and extensions

The extension installation for VS Code is divided into three categories: Must-Have, Good to Have, and Optional.

  • Must-Have Extensions

    • rust-analyzer (by rust-lang.org)

    • CodeLLDB (Vadim Chugunov) – For debugging Rust code. The setup for the debugger is in the next section.

  • Good to Have Extensions

    • Better TOML (or Even Better TOML)

    • Crates (Seray Uzgur)

  • Optional Extensions

    • Error Lens

    • Tabnine – My preference for AI assistance while writing code. It's free to use.

    • Code Runner (Jun Han) - 26,530,769+ Downloads

Setting up the VS Code Debugger for Rust

CodeLLDB is must-have extension for debugging Rust program in vscode

In many cases, VS Code can automatically configure the debugger for your Rust program without additional setup. Follow these steps to verify if automatic debugging is enabled:

  1. Write a simple "Hello, World!" program (see the next section for the code).

  2. Set a breakpoint in your code by clicking to the left of the line number in the editor.

  3. Click the Run tab on the top bar.

  4. Select Start Debugging or press F5.

If the debugger starts successfully, you're all set! If not, follow the manual setup process below.

Manual Debugging Setup

If automatic setup does not work, you can manually configure the debugger:

  1. Click the Run tab on the top bar.

  2. Select Run Without Debugging or press Ctrl + F5.

  3. Choose LLDB from the list of debugger types.

  4. You might see some warnings. Click Yes -> Yes to proceed.

  5. VS Code will create a launch.json file in the .vscode folder in your current working directory.

For a visual guide, you can refer to this video: How to Debug Rust with VS Code

First "Hello, World!" Program

Let's start with a simple "Hello, World!" program in Rust.

Step 1: Initiate a Cargo Project

Cargo is Rust's package manager and build system. It simplifies the process of managing dependencies and building your projects, similar to go mod in Go or npm init in Node.js.

To create a new Cargo project, run the following commands in your terminal:

$ cargo new rust_learning_project 
$ cd rust_learning_project

This command will create a directory structure like this:

|-- src 
|     |-- main.rs
|-- .gitignore 
|-- Cargo.lock 
|-- Cargo.toml

Step 2: Write the "Hello, World!" Code

Open the src/main.rs file and add the following code:

fn main() {
    println!("Hello, World!"); 
}

Step 3: Run the Code

To compile and run your program, use the following command:

$ cargo run

You should see the output:

Hello, World!
Alternative: Using an Online Rust Compiler

For a quick run without setting up a local environment, you can use an online Rust compiler like OnlineGDB.

Or, for running a single .rs file you can use

$ rustc main.rs
// $ rust <file_name>

Anatomy of a Rust Program

In Rust, functions are defined using the fn keyword. The main function is special because it's the entry point of every executable Rust program. Here’s a simple example:

fn main() {
    println!("Hello, world!");
}
  • Function Declaration: The main function is declared with fn main(). It has no parameters and returns nothing. If there were parameters, they would go inside the parentheses ().

  • Function Body: The body of the function is enclosed in curly brackets {}. Rust requires these around all function bodies. It's a good practice to place the opening curly bracket on the same line as the function declaration, with one space in between.

  • Automatic Formatting: Rust has an automatic formatting tool called rustfmt. It helps maintain a consistent coding style. You can check the online documentation to see if it's included in your Rust installation.

Inside the main function, we have the following line:

println!("Hello, world!");
  • Indentation: Rust style is to indent with four spaces, not a tab.

  • Macros: println! is a macro, not a function. This is indicated by the exclamation mark !. If it were a function, it would be written as println without the !. We will discuss Rust macros in more detail in a later section.

  • String Argument: The "Hello, world!" string is passed as an argument to, which prints it to the screen.

  • Semicolons: Lines of Rust code usually end with a semicolon ;, indicating the end of an expression.

Compiling and Running Are Separate Steps

In Rust, compiling and running are separate steps, each serving a distinct purpose. Let's break down the process:

  1. Compilation: Before executing a Rust program, it must be compiled using the Rust compiler (rustc). For instance:

     $ rustc main.rs
    

    This command compiles main.rs and generates a binary executable.

  2. Executable Output: After successful compilation, Rust outputs a binary executable. You can view it using the shell's file listing command, such as ls on Unix-like systems:

     $ ls
     main  main.rs
    

    On Windows, it may appear as main.exe.

    Alternatively, on Windows CMD:

     > dir /B
     main.exe
     main.pdb
     main.rs
    
  3. Execution: To run the executable, use the appropriate command:

     $ ./main    # Unix-like systems
    

    or

     > .\main.exe    # Windows
    

    This executes the program, producing the desired output.

    If main.rs contained a "Hello, world!" program, executing it would print the message to the terminal.

For those accustomed to dynamic languages like Javascript or Python, the concept of compiling and running separately might seem unfamiliar. Unlike such languages, Rust is ahead-of-time compiled, enabling distribution of executables without requiring the recipient to have Rust installed.

While rustc suffices for basic programs, managing complex projects warrants a more sophisticated approach.

Introduction to Cargo

Cargo is Rust's package manager and build system, streamlining the development process by automating tasks such as dependency management, compilation, and project initialization.

Key Features:

  1. Dependency Management: Cargo simplifies dependency management by automatically fetching and managing libraries from the crates.io repository. Dependencies are specified in the Cargo.toml file.

  2. Build System: It handles compilation, building, and testing of Rust projects. Cargo automatically compiles and links project dependencies, ensuring a smooth development workflow.

  3. Project Initialization: Cargo facilitates project setup with the cargo new command. This initializes a new Rust project with the necessary directory structure and configuration files.

Example
  • Creating a New Project:

      $ cargo new my_project
    

    This command creates a new Rust project named my_project with the required directory structure.

  • Building and Running:

      $ cd my_project
      $ cargo build    # Compiles the project
      $ cargo run      # Builds and executes the project
    
  • Adding Dependencies: To include external dependencies, specify them in the Cargo.toml file under the [dependencies] section. For example:

      [dependencies]
      rand = "0.8.4"    # Adds the 'rand' library as a dependency
    
  • Updating Dependencies:

      $ cargo update    # Updates project dependencies to their latest versions
    

Cargo's intuitive interface and robust features make it an indispensable tool for Rust developers, enabling efficient project management and streamlined workflow.

There could be a full separate blog on Cargo. I am planning to write a comprehensive article on Cargo for my next blog post before my next week's update. Later, I will attach the blog link below. This will provide a detailed understanding of Cargo.

Variables and Mutability

In Rust, variables are immutable by default, meaning once assigned, their value cannot be changed. To make a variable mutable, use the mut keyword. For example:

let x = 5; // Immutable variable
let mut y = 10; // Mutable variable
y = 15; // Allowed

Constants

Constants in Rust, unlike variables, are always immutable and declared using the const keyword. They require type annotations and can be declared in any scope. Unlike variables, constants must be set to a value that can be determined at compile time, not computed at runtime.

Naming conventions suggest using all uppercase with underscores between words. For instance,

const HOURS_IN_SECONDS = 60 * 60
const PI = 3.14

Rust's compiler can evaluate certain operations at compile time, aiding readability and verification.

Constants remain valid throughout the program's execution, making them useful for universally applicable values, such as maximum player scores or physical constants.

Shadowing

When we declare a new variable with the same name as a previous one, the new variable overshadows the old one. We can shadow a variable by using the same name and repeating the let keyword. For example:

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}");
}

This program outputs:

The value of x in the inner scope is: 12
The value of x is: 6

Shadowing differs from marking a variable as mutable because attempting to reassign a shadowed variable without using let results in a compile-time error. Additionally, shadowing allows us to change the type of a variable, enabling reuse of the same name for different types. For example:

let spaces = "   ";
let spaces = spaces.len();

In contrast, attempting to achieve the same with mutability would lead to a compile-time error due to type mismatch. This understanding of variables sets the stage for exploring various data types in Rust.

Data Types

In Rust, every value is associated with a specific data type, which informs the compiler how to handle that value. There are two main subsets of data types in Rust: scalar and compound.

Scalar Types

Scalar types represent single values and include integers, floating-point numbers, Booleans, and characters. Let's explore these types:

Integer Types
  • Integers are whole numbers without a fractional component. Rust provides various integer types, both signed and unsigned, ranging from 8 to 128 bits in size.

  • The choice of type depends on the range of values you need to represent.

Example:
let signed:i32 = 42; // Signed Integer Type
let unsigned: u32 = 200; // Un Signed Integer Type

The integer type i32 has two parts to it: i - specifies signed integer type (can store both positive or negative value) 32 - size of the data type (takes 32 bits of space in memory)

Categories of Integer Data Types in Rust

Depending on the size of data, we can further classify the signed and unsigned integer type into various categories:

SizeSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
Floating-Point Types
  • Floating-point numbers are numbers with decimal points, represented by f32 and f64 in Rust.

  • f32 is a single-precision float, while f64 offers double precision and is the default type due to its higher precision.

Here, the f character represents a floating point number, 32 and 64 represent the size in bits.

Boolean Type
  • Booleans in Rust, denoted by bool, can have values of true or false.

  • They are one byte in size and are primarily used in conditional expressions.

Character Type
  • The char type represents single Unicode scalar values, allowing Rust to handle a wide range of characters, including emoji and accented letters.

  • Each char is four bytes in size.

Here, char represents the character type variable and we use single quotes to represent a character.

Example
fn main() {
    let letter: char = 'A';
    let emoji: char = '😊';
    let chinese_char: char = '中';
    let accented_char: char = 'é';

    println!("Letter: {}", letter);
    println!("Emoji: {}", emoji);
    println!("Chinese Character: {}", chinese_char);
    println!("Accented Character: {}", accented_char);
}

Compound Types

Compound types group multiple values into one type. Rust offers tuples and arrays as primitive compound types:

Tuple Type
  • Tuples can hold multiple values of different types within a fixed-length structure.

  • They are created by enclosing comma-separated values within parentheses.

Array Type
  • Arrays store multiple values of the same type in a fixed-length sequence.

  • They are useful when you need a fixed number of elements with the same type.

  • Arrays are specified by enclosing values within square brackets.

Example
fn main() {
    // Scalar Types
    let integer: i32 = 42;
    let float: f64 = 3.14;
    let boolean: bool = true;
    let character: char = 'A';

    println!("Integer: {}", integer);
    println!("Float: {}", float);
    println!("Boolean: {}", boolean);
    println!("Character: {}", character);

    // Compound Types
    let tuple: (i32, f64, bool) = (42, 3.14, true);
    let array: [i32; 5] = [1, 2, 3, 4, 5];

    println!("Tuple: {:?}", tuple);
    println!("Array: {:?}", array);
}

Type Inference in Rust

In Rust, we can create variables without explicitly specifying their data types. The compiler infers the data type based on the value assigned to the variable. For example:

let x = 51;

In this case, Rust automatically identifies the data type of variable x as i32 (the default type for integer variables) because of the value 51. This process is known as Type Inference.

Consider the following example:

fn main() {
    let x = 51;
    println!("x = {}", x);
}

Output:

x = 51

As you can see, we haven't explicitly mentioned the data type of the variable x. Rust inferred it as i32 based on the assigned value 51.

Functions

Functions in Rust are integral to the language's structure, serving as building blocks for program logic. The main function, commonly recognized as the entry point for Rust programs, exemplifies this role.

By employing the fn keyword, we can declare new functions, adhering to Rust's conventional naming style of snake case, where words are separated by underscores.

Let's delve into a simple example:

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

In this code snippet, we define a function using the fn keyword followed by the function's name and a set of parentheses to denote its parameters, if any. The function body is enclosed within curly braces, signaling its scope. We invoke functions by specifying their names followed by parentheses.

Consider the following enhancement:

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {}", x);
}

In this version, we introduce parameters to the function another_function. Here, x is an i32 type parameter. When calling this function with an argument (5 in this case), Rust's type inference deduces the parameter's type, facilitating smooth integration.

Furthermore, Rust distinguishes between statements and expressions within function bodies.

  • While statements execute actions and do not return values,

  • expressions yield results.

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

fn sum(a:i32, b: i32) -> i32 { 
    a + b
}

This expression-based paradigm aligns with Rust's design philosophy, promoting clarity and efficiency in code composition.

Note that there is no ; at the end of the a + b expression. This is because the ; would not return the result of the calculation a+b, but would return the unit type (). This is a common mistake for people coming from other languages. The () unit type is the only type that can be returned from a function that does not have a return type specified; in C this would be void.

Lastly, functions can return values, with the return type declared after an arrow (->). The return value corresponds to the final expression in the function's body. For instance, consider the function sum(a:i32, b:i32) -> i32:

In Rust, the return keyword is used to explicitly return a value from a function. However, unlike in some other programming languages, Rust functions don't require the return keyword to return a value. The reason for this is Rust's expression-based nature.

fn add_one(x: i32) -> i32 { 
    x + 1 // This expression is implicitly returned 
}

However, if you need to return early from a function or explicitly specify a return value, you can use the return keyword:

fn add_one(x: i32) -> i32 {
    if x >= 0 {
        return x + 1; // Explicit return with the return keyword
    } else {
        return x - 1; // Explicit return with the return keyword
    }
}

Reference | Books

  1. rust-lang book

  2. The Rust Programming Language (by Steve Klabnik and Carol Nichols)

  3. Rust for Rustaceans (by Jon Gjengset)

0
Subscribe to my newsletter

Read articles from SOUMITRA-SAHA directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SOUMITRA-SAHA
SOUMITRA-SAHA

About Me I am a Full-Stack Developer with approximately two years of experience in the industry. My expertise spans both front-end and back-end development, where I have worked extensively to build and maintain dynamic, responsive web applications. Technical Skills Front-End Development: HTML, CSS, JavaScript, Typescript Frameworks: React, NextJs, Remix, Vite Libraries: TailwindCSS, Bootstrap Tools: Webpack, Babel Back-End Development: Languages: Typescript, Node.js, Python, Go, Rust Frameworks: NextJS, Remix, Express.js, Django, Go (Gin), Rust Databases: PostgreSQL, MySQL, MongoDB Tools: Docker, Kubernetes DevOps: Version Control: Git, GitHub CI/CD: Jenkins, CircleCI Infrastructure as Code: Terraform Cloud Services: AWS, Lamda Other Skills: RESTful API Development Test-Driven Development (TDD), (vitest, jest, Cypress) Agile Methodologies Personal Interests I am passionate about learning new technologies and keeping up with industry trends. I enjoy exploring innovative solutions to complex problems and continuously seek opportunities to expand my expertise. In my free time, I engage with the developer community, contributing to open-source projects and participating in tech meetups.