Get started with Rust(Part I)

elijah samson
18 min readMar 1, 2024

--

Rust is a modern systems programming language created in 2006 and published in 2015 by Graydon Hoare, a software developer at Mozilla Research, as a personal project. It is a systems programming language focusing on performance, memory safety, and safe concurrency. Developers largely see it as a replacement for the mature C and C++, languages as old as they are powerful.

Ferris, the mascot of Rust

Rust features

Rust is a modern systems programming language focusing on safety, performance, and concurrency. It was designed to address common programming pitfalls and challenges, particularly in systems-level programming. Here are some key features that make Rust stand out:

  1. Memory Safety without Garbage Collection: Rust introduces a unique
    ownership system that enforces strict rules about accessing and managing memory. This prevents common bugs like null pointer dereferences, dangling pointers, and data races without needing a garbage collector.
  2. Ownership, Borrowing, and Lifetimes: Rust’s ownership system ensures that memory is managed efficiently and safely. Variables have ownership of their data, and the concept of borrowing allows functions to temporarily use data without taking ownership. Lifetimes ensure that references remain valid and prevent dangling references.
  3. Zero-Cost Abstractions: Rust allows high-level programming constructs
    and abstractions without sacrificing performance. Its “zero-cost abstractions” philosophy means that the abstractions used at the high level compile into efficicient machine code with minimal overhead.
  4. Concurrency and Parallelism: Rust’s ownership and borrowing model also enables safe concurrency and parallelism. The Send and Sync traits ensure that data can be safely transferred between threads and shared between threads. The async/await syntax simplifies asynchronous programming.
  5. Trait System and Pattern Matching: Rust’s trait system provides a
    powerful mechanism for defining shared behavior across different types.Pattern matching allows for concise and readable code when working with complex data structures.
  6. Pattern Matching: Rust’s pattern matching is expressive and powerful,
    enabling concise code for complex data manipulation and control ow. It helps in handling different cases and scenarios efficiently.
  7. Functional and Imperative Paradigms: Rust supports both functional and imperative programming styles. It allows you to choose the paradigm that best fits your problem while enforcing safety and performance guarantees.
  8. Crates and Package Management: Rust has a robust package
    management system with Cargo, simplifying project setup, dependency
    management, and building. Crates are Rust’s version of libraries, which can be easily shared and distributed.
  9. No Null or Undefined Behavior: Rust eliminates null pointer dereferences and undefined behavior by enforcing strict rules for memory access. The Option and Result types are used to safely handle the absence of values and errors.
  10. Cross-Platform Compatibility: Rust’s focus on portability and well-defined behavior makes it suitable for building cross-platform applications. It supports various operating systems and architectures.
  11. Ecosystem and Community: Rust has a rapidly growing and supportive
    community. The Rust ecosystem includes various libraries and tools covering various domains, from web development to embedded systems.
  12. Compiled Language: Rust is a compiled language, resulting in efficient and performant binaries. Its compile-time checks catch many errors before runtime, reducing the likelihood of bugs in production code.

Setting up Rust development environment

1. Download installers

Go to the Rust website and download installers for your operating system. Double click the installer and follow the steps to install Rust.

2.Use rustup to install

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

This will install rustup and the latest stable version of Rust.

To uninstall Rust at any time, just run rustup self uninstall.

The installation you just did came with the following tools:
cargo : The Rust package manager.
rustc: The Rust compiler is sometimes invoked by cargo since your code is also treated as a package.
rustdoc : The Rust documentation tool for documenting comments in Rust code.

rustc --version
cargo --version
rustdoc --version

Project Setup

Create a new project directory and create a file in it called hello.rs. In it, input this simple Hello World script:

Note: Rust standard is that all Rust-related websites use the .rs domain
extension.

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

Now, run this command from the project directory:

rustc hello.rs

If you use ls command to check the content of the project directory, you should see a new fille named hello . Run it like a bash script:

./hello
# Output is:
Hello World!

Great! You just used the Rust compiler to compile a Rust program into a binary file.

Note: The Rust compiler is one of the smartest and most helpful
compilers in the history of programming. The Rust community has reason to believe that if your code compiles without any errors, it is memory safe and almost bug-free.

using cargo

Let’s see what cargo is. Delete the project folder you just created and run the following command:

cargo new hello

Next, change the directory into into the src/ subdirectory and open the main.rs. You should see the content:

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

Great. You just used the Rust compilation manager to initialize a new binary project. Cargo is a very helpful tool that allows you to initialize, build, run, test, and release Rust projects and leverage existing Rust libraries in your project. There is an entire book to learn about Cargo, and its possibilities for your perusal.

Note: One of the greatest strengths promoting the adoption and evolution of the Rust programming language is the immersive documentation they provide for everything Rust-related. The libraries (called crates) available to Rust are available at Crates.io, the documentation for each crate is available at Docs.rs, jobs in the blockchain space are available at Rib.rs, and more.

Since we have not learned Rust, let’s pass on the rustdoc tool.

Next, run this command to compile and run the auto-generated program with Cargo:

cargo run

IDEs and tools

When writing Rust code, you will need all the help you can get. Install the Rust-Analyzer plugin from the extensions marketplace if you use Visual Studio Code. If you are a fan of the JetBrains ecosystem of IDEs, you should install the Rust plugin for your code and turn on the Cargo Check feature at the bottom of the window.

Hello, World!

We have already seen how to write “Hello, World” in Rust. In this section, we will demystify all the parts of the Rust syntax relevant to the “Hello, World” code in the previous section.

The structure of Rust projects

The important parts of a Rust project are seen when we use Cargo to initialize a new project. Our hello.rsthe project has the following files:

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

Let’s understand what they stand for.

  • Cargo.toml: stores metadata for the package. The contents are grouped into two sections package and dependencies . The dependencies section contains records of the names and versions of external crates used in the project.
  • .gitignore : stores files that Git should not track. Git is Rust’s official version control software, and the cargo new command initializes Git in the directory. When Rust code is built or compiled, a target/ sub-directory is generated containing the build les. Adding it to the .gitignore file is good practice to avoid pushing large unnecessary files to GitHub.
  • src/main.rs : The src/ subdirectory is where all Rust code is normally written. The main.rs file is the entry point for all binary crates.

Note: With Cargo, you can create two types of projects (preferably called packages or crates): binary packages/crates and library packages/crates. Let’s see how these are done.

Run:

cargo new libpkg --lib

You should see a new libpkg project containing a lib.rs file inside the src/ directory instead of a main.rs le. You’ll also notice an auto-generated test code inside the lib.rs file. Library crates do not have/need a main function and do not allow the cargo run command. You can only use the tests to check your code by running cargo test . You can trick Cargo by creating a main.rs file inside the src/ directory, but if you publish the project as a package, remember to delete it.

Understanding basic syntax and structure

We have been introduced to the main function from the “Hello World” code above. Most programming languages like Go and C/C++ also have the main function as the entry-point function. But the difference is that Rust’s main function can behave like every other function by accepting and returning values. Weird, right?

Let’s go further with understanding Rust’s syntax for now.

Comments for documentation are made with the double forward slash: // or ///. Try this out in the main.rs file:

fn main() {
// greet me on the terminal
println!("Hello World!");
}

The fn keyword is the Rust way of specifying that a code block is a function. All expressions and separate statements in Rust code must end with a semicolon, just like in C/C++. If you remove or skip it, the compiler panics.

Remove the semicolon after the closing brackets to print out the greeting and running .cargo run .

Surprise! It still works. Why? The Rust compiler can infer the meaning of your code even if you omit the semicolon at the end of a statement. Smart, right?

Let’s update the main.rs le to this:

fn main() {
let x = 5 // there is a missing semicolon here.
println!("The value of x is: {}", x) // there is also a missing semicolon here that can be omitted.
}

The output should be:

cargo run
Compiling hello v0.1.0 (/.../workspace/hello)
error: expected `;`, found `println`
--> src/main.rs:2:14
|
2 | let x = 5
|
^ help: add `;` here
3 | println!("The value of x is: {}", x)
| ------- unexpected token
error: could not compile `hello` (bin "hello") due to previous error

The compiler tells you exactly what went wrong: expected ; . It further gives you a helping hand:help: add ; here.

Add the semicolon at the end of line 2, and run the program. This works. But it isn’t ideal. Add another semicolon at the end of line 3 too. There you go; a properly written Rust code.

The letkeyword is a statement and the println!keyword (called a macro in Rust) is an expression. We will learn more about the use of the let keyword in the next section.

If you come from a C/C++ background, you can confirm that the main function in Rust receives arguments and return statements by updating the main.rs file and running it:

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

The -> symbol is how you specify return values. This is a more robust way to write the main function in Rust

We have seen how variables are created in Rust using the let keyword. But what kind of variables can we create? Let’s explore them. Remember to run cargo runevery time you update the code.

Variables

Rust supports the mutability and immutability of variables. If this sounds alien to you, it simply means that with Rust, you can make a variable have the ability to change its value or not change it during compile time. Let’s see this more practically.

let mut x = 5;  // mutable

let x = 10; // immutable

The variable xabove is generally a number. This variable cannot be changed throughout this code, so this won’t work:

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

The only way it will work is if we reassign xor copy the value into another variable:

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

Like this, the second instance of the variable x is different from the first. But then, you had to write two more lines of code to achieve this change. To avoid this, we can simply use the mut keyword, which stands for mutable when creating x:

fn main() -> () {
let mut x = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x is now: {}", x);
}

This works, and the compiler doesn’t panic like it did before.Now, let’s move on to data types.

Data types

Note: If you have configured your IDE or code editor to have the Rust plugin, you must have seen that a type inference was appended to x :

This was done by the rust-analyzer and means that you can write the code as:

fn main() -> () {
let mut x: i32 = 5;
println!("The value of x is: {}", x);
x = 10;
println!("The value of x is now: {}", x);
}

Sometimes, we will need to specify the exact type of a variable to avoid anomaly behavior in our code.

Scalar types

Rust has familiar scalar types like:

  • Integers: u8, i32, usize, etc.
  • Floats: f32, f64
  • Booleans: true and false (bool)
  • Characters: char

Compound types

Rust has compound types to group multiple values:

  • Tuples: group values of different types (let tup: (i32, f64, u8) = (1, 2.5, 3);)
  • Arrays: group values of the same type and fixed length ([1, 2, 3]; let arr: [i32; 5] = [1, 2, 3, 4, 5];)

Note: Rust has a reference that is being released every six weeks. It is the primary reference you should run to when you need to remember something, and it is available here. For example, you can find all the types available in Rust here. You can click on each link to and the subtypes under them.

Numeric types
Under Numeric types in the Primitive types collection, you should find Integer types, Floating-point types, and Machine-dependent integer types. You can also learn how types are specified. The i32 type we used above is for integers between 2³² — 1.
You have seen how to create numeric typed variables with and without type
inference. Let’s learn how to do so in other types.

Textual types

Under Textual types, you should find characters char and strings str . They are created like this:

fn main() -> () {
let single_alphabet = 'a';
println!("The single character is: {}", single_alphabet);
let second_alphabet: char = 'b';
println!("The single character with type inference is: {}", second_alphabet);
}

When we explore strings in Rust, you will be introduced to memory management, an intermediate aspect of Rust. Nevertheless, let’s explore how to create string variables in Rust:

fn main() -> () {
let my_string = "Hello World";
println!("The string content is: {}", my_string);
let second_string: &str = "Hello, other world";
println!("The string content with type inference is: {}", second_string);
}

Notice the ampersand (&) we used during the type inferred variable second_string . This is how strings in Rust work. If you create a string without it, the compiler will panic, and this is because Rust understands that strings can have a dynamic length during compile time, so it gets stored in a heap.

The non-complicated way to create strings in Rust is to use the automatic
dynamically sized String keyword:

fn main() -> () {
let another_string = String::from("Hello, world");
println!("The string content is: {}", another_string);
let new_string: String = String::from("Hey, world!");
println!("The type inferred string content is: {}", new_string);
}

From the Rust reference, you should take your time playing with the other Rust types. Let’s move on to the next section.

Sequence types

In Rust, sequential types refer to data structures that store a sequence of values in a specifc order. These types allow you to store and manipulate collections of items. Rust provides several built-in sequential types with characteristics and uses cases.
Here are some examples:

  1. Arrays: Arrays in Rust have a fixed size determined at compile time and
    contain elements of the same type. They are stored on the stack and are
    useful when you need a xed number of elements with a known size.
// Declaration and initialization of an array
let numbers: [i32; 5] = [1, 2, 3, 4, 5]

2. Slices: Slices are references to a contiguous sequence of elements within
another sequential type (like an array or a vector). They allow you to work with a portion of the data without copying it.

let numbers = [1, 2, 3, 4, 5];
let slice: &[i32] = &numbers[1..4]; // Slice containing elements 2, 3, and 4

3. Vectors: Vectors are dynamic arrays that can grow or shrink in size. They are stored on the heap and allow you to store various elements.

// Creating and modifying a vector
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3)

4. Strings: Rust’s String type is a dynamically sized, UTF-8 encoded string. It is implemented as a vector of bytes and provides a convenient way to work with text.

// Creating and manipulating strings
let mut text = String::from("Hello, ");
text.push_str("world!");

5. Ranges: Ranges are sequential types representing a sequence of values from a start value to an end value. They are often used in loops to iterate over a range of numbers.

for number in 1..=5 { // Inclusive range (1 to 5 inclusive)
println!("Current number: {}", number);
}

6. Tuples: Tuples are collections of values of different types, and they have a fxed size that’s determined at compile time. Each element of a tuple can have its type.

// Creating tuples
let person: (String, i32, bool) = ("Alice".to_string(), 30, true);
// Accessing tuple elements
let name = person.0; // Access the rst element (name)
let age = person.1; // Access the second element (age)
let is_adult = person.2; // Access the third element (is_adult)

These sequential types in Rust provide various options for storing and manipulating data collections. Depending on your use case and requirements, you can choose the appropriate type to effectively manage your data and perform operations effciently.

Functions

You were first introduced to the function, arguably the most important part of a program. We have seen the main function, so let’s learn how to create other functions.

Define with fn

Use fn to define a function.

Arguments and return values

Use -> to denote the return type.

Expression vs statement functions

unctions with just an expression evaluate to the expression. Statement functions use statements and optionally a return keyword.

fn square(x: i32) -> i32 {  // expression 
x * x
}

fn square_statement(x: i32) -> i32 { // statement
let result = x * x;
result
}

Here are examples of di erent types of functions in Rust:

  1. Function with No Return Statement or Argument:
fn greet() {
println!("Hello, world!");
}

In this example, the greet function doesn’t take any arguments or return any value. It simply prints out, “Hello, world!”.

2. Function with Argument and No Return Statement:

fn say_hello(name: &str) {
println!("Hello, {}!", name);
}

Here, the say_hello function takes a single argument of type &str (a string slice) and prints a personalized greeting using that argument.

3. Function with Argument and Return Statement:

fn square(n: i32) -> i32 {
n*n
}

In this example, the square function takes an i32 argument and returns the square of that argument as an i32 .

4. Function with Multiple Arguments and Return Statements:

fn calculate_power(base: f64, exponent: i32) -> f64 {
if exponent == 0 {
1.0
} else {
let mut result = base;
for _ in 1..exponent.abs() {
result *= base;
}
if exponent < 0 {
1.0 / result
} else {
result
}
}
}

In this example, the calculate_power function takes a base of type f64 and an exponent of type i32 . It calculates the base’s power raised to the exponent, considering both positive and negative exponents.

Modules

You will be familiar with modules if you know software engineering principles like hexagonal architecture. Generally, breaking down functions into different modules in a growing code base is best. We can do this in Rust by leveraging the mod.rs file, a specially recognized and reserved file for Rust projects. Inside the src/ directory, you can also have other subdirectories called modules. Using them in the project must be linked to the main.rs or lib.rs file.

In Rust, modules organize code into separate, named units, allowing you to group related functions, types, and other items. This helps improve code organization, maintainability, and readability by breaking your program into smaller, manageable pieces.

Here’s how you can work with modules in Rust. Inside the src/ directory, create a new directory called database. In it, create a mod.rs file and a model.rs file. The mod.rs file gives visibility to other files in any subdirectory under the src/ directory. To practically understand this, add the following code to the specified files:

mod.rs


pub mod model;

model.rs

pub fn create_user(name: &str, age: i32) {
println!("New user created: {} of age {}", name, age);
}

main.rs

pub mod database; // make the database module available in this file.
pub use database::*; // give this file access to all public modules and their functions (*) inside of the database module.

fn main() {
let name = "John";
let age = 35;
database::model::create_user(name, age);
}

Note that we are using a clean main.rs file. Run the code with cargo, and you should see:

❯ cargo run
Compiling hello v0.1.0 (/.../workspace/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `target/debug/hello`
New user created: John of age 35

What did we do? We created a subdirectory, added a model.rs le, and exported it with mod.rs file and exported it with model.rs . We then called the subdirectory inside the main function (as we would do in any Rust source file that needs the contents of model.rs), and then we used the
create_user function in the main function.

Note: In Rust, the double semicolon notation is used to call methods of a
module/package, not the dot notation like in Go. Since we already specified to use all modules public modules and their respective functions using this line pub use database::*; , we can shorten the calling technique to this: model::create_user(name, age); .

We can also do this:

pub mod database;
pub use database::model::*;

fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}

Or this:

pub mod database;
pub use database::model::create_user;
// use only the create_user function from the model module in the database package.
fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}

Ensure you get the logic before proceeding to the next section.

Note: Rust is feature-rich. For example, we can create separate modules inside one single Rust file using the mod keyword. We can also write modules inside the mod.rs. file instead of having a separate model.rs. file.

But this way shown above is a more maintainable and readable way to do it, as is the initial goal of modular programming. If your application is small and lightweight, you can do either, but use the approach taught here if it is bound to scale/grow bigger.

When creating custom data structures (precisely called models in software
engineering), structs are pretty useful. They are used to store variables that are related. For example, say we want to create a model of a User. We can do so in Rust like this:

struct User {
name: String,
age: i32,
account_balance: f64,
}

This is usually done outside of a function (in global scope) if we intend to use it in more than one place. To use the struct, we must have a method that implements it. A method is a function that is tied to a struct type. Let’s see how to implement methods for a type.

Creating Method Implementations for Structs

Implementing methods for a struct type in Rust allows you to define functions that operate specifically on instances of that struct. This is a powerful feature that promotes encapsulation and enhances code organization. To illustrate, let’s build upon the User struct example and implement some methods.

struct User {
name: String,
age: i32,
account_balance: f64,
}
impl User {
// A method to greet the user
fn greet(&self) {
println!("Hello, my name is {} and I'm {} years old.", self.name, self.age);
}
// A method to deposit money into the user's account
fn deposit(&mut self, amount: f64) {
self.account_balance += amount;
println!("Deposited {:.2} units. New balance: {:.2}", amount, self.account_balance);
}
// A method to check if the user is eligible for a discount
fn is_eligible_for_discount(&self) -> bool {
self.age >= 60 || self.account_balance > 1000.0
}
}

In the example above, we’ve defined three methods associated with the User struct. Let’s break down how they work:

  1. The greet method takes a shared reference ( &self ) to a User instance and prints a personalized greeting using the user’s name and age.
  2. The deposit method takes a mutable reference ( &mut self ) to a User instance and an amount parameter. It adds the specifed amount to the user’s account_balance and prints out the new balance.
  3. The is_eligible_for_discount method checks whether the user is eligible for a discount based on age and account balance. It returns true if the user is 60 years or older or their account balance exceeds 1000 units.

Using these methods, you can interact with User instances more intuitively and structured. Here’s an example of how you might use these methods:

fn main() {
let mut user1 = User {
name: String::from("Alice"),
age: 28,
account_balance: 750.0,
};

user1.greet();
user1.deposit(250.0);

if user1.is_eligible_for_discount() {
println!("Congratulations! You are eligible for a discount.");
} else {
println!("Sorry, you are not eligible for a discount.");
}
}

In this way, methods enable you to encapsulate behavior related to a struct and promote cleaner, more readable code. Rust’s strict ownership and borrowing rules help ensure that your code remains safe and free from common programming errors.

Note: You can split method implementations into separate modules to make code more readable and accessible.

In the world of Rust programming, one of its most distinctive features revolves around the concepts of ownership, borrowing, and lifetimes.

These concepts are fundamental in ensuring memory safety and preventing many bugs, including data races and null pointer dereferences.

This blog will delve deep into these concepts to understand how Rust achieves these goals in part(II).

That’s it, I guess.

Cheers, Happy Coding!

🙏🙏🙏

Since you’ve made it this far, sharing this article on your favorite social media network would be highly appreciated. For feedback, please ping me on X.

--

--

elijah samson

Software engineer (Backend, Web Dev). ✍️ I write about backend, data and other amazing stuff