Introduction to the Rust Programming Language: Getting Started

CodiLime
11 min readMay 25, 2023

--

Rust, developed by the Mozilla team, saw daylight in 2010. This programming language has continually gained a lot of attention among the developer community — in 2022, it was the most loved programming language for the seventh year in a row.

What makes this language so important? You can find answers to this question in our other article about its popularity and why Rust is worth using.

Read on to find information about the most important Rust-related aspects and get to know this programming language better. Below you will find a short overview of Rust, its advantages, and examples of Rust applications.

Rust overview

Rust was designed to help with safe concurrent systems programming. It is a statically typed language that provides low-level control over system resources, such as memory and CPU usage.

Thanks to the specific features listed below, it is often used for system-level programming, such as operating systems, web browsers, game engines, and network services.

Rust provides:

  • Memory safety — no null pointer dereferences, buffer overflows, or other common memory-related errors that can lead to security vulnerabilities and crashes.
  • Concurrency — the built-in concurrency model allows multiple threads to communicate with each other and share resources safely.
  • High-performance level — a minimal runtime overhead makes Rust suitable for performance-critical applications.
  • Zero-cost abstractions — efficient data structures and algorithms can be used without any additional runtime overhead, which allows developers to write high-level abstractions without sacrificing performance.

What are Rust’s main advantages?

Every programming language has its strengths that separate them from the rest. What features make Rust outstanding?

First, memory safety — as mentioned above, Rust has a safe memory management system that prevents common memory-related errors. Rust is also often chosen thanks to its great performance and concurrency support. Rust provides low-level control over system resources, making it suitable for applications where performance is a top priority. It has a built-in concurrency model that handles concurrent programming with ease. What’s also important is that Rust is cross-platform and can be compiled to run on different platforms, including Linux, macOS, Windows, and even embedded devices.

How to get started with Rust?

To start with Rust, and there is no surprise here, you need to download and install a Rust compiler, via Rustup as a recommended method. After installation, you can start your Rust journey.

A short instruction on how to compile and run a Rust program:

  1. Write the Rust code in a text editor or an integrated development environment (IDE).
  2. Save the Rust code to a file with the .rs extension.
  3. Open a terminal or a command prompt and navigate to the directory where the Rust code is.
  4. Type the command rustc <filename>.rs to compile the Rust code. This will produce an executable file with the same name as the Rust code file.
  5. Type the command ./<filename> to run the Rust program.

Every step is clearly explained on the official Rust website, so the whole process is a piece of cake.

Here’s a simple “Hello World” example:

The first program that most developers are familiar with is the simple “Hello World!” — only a few steps are needed to print the string:

The source code of the Hello World program with the usage of println! macro:

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

To generate binary, use the Rust compiler: rustc.

$ rustc hello.rs 

As result, rustc will produce a hello binary:

$ ./hello
Hello World!

Click ‘Run’ above to see the expected output and add a new line with a second println! macro so that the output shows:

Hello World!
I'm a Rustacean!

The most important concepts in Rust with examples:

  • Ownership

Ownership is a unique feature of Rust that helps developers with memory management in a consistent and safe way. Every value has an unique owner responsible for allocating and freeing the memory associated with a particular value. With explicit ownership we eliminate all sorts of memory-related issues like memory leaks, use after free, and others.

Ownership is enforced by a set of rules, including move semantics, which transfer ownership from one variable to another, and borrowing, which allows temporary access to a value without taking ownership. Thanks to these rules, the Rust programming language ensures that memory is always correctly managed and multiple program parts can access the same data safely and efficiently.

Here, you can find more about ownership in Rust.

fn main() {
let original_variable = String::from("hello");
let moved_variable = original_variable;
println!("{} world!", original_variable);
}

Code in playground >>

Output:

error[E0382]: borrow of moved value: `original_variable`
--> src/main.rs:5:27
|
2 | let original_variable = String::from("hello");
| ----------------- move occurs because `original_variable` has type `String`, which does not implement the `Copy` trait
3 | let moved_variable = original_variable;
| ----------------- value moved here
4 |
5 | println!("{} world!", original_variable);
| ^^^^^^^^^^^^^^^^^ value borrowed here after move

This example shows how ownership of the variable can prevent memory-related issues. We cannot use or borrow variables which we did not have ownership of. In this code, original_variable is a String that owns the string “hello”. We then move ownership of the “hello” string to move_variable. Finally, when we try to print the variable, the compiler gives us an error that we “borrow of moved value: `original_variable`”.

Since Rust’s ownership system only allows one owner of a piece of memory at a time, using original_variable after moving ownership is forbidden.

  • Borrowing

Borrowing enables safe and efficient data sharing between different program parts. In Rust, borrowing allows a piece of code to temporarily “borrow” a reference to a value without taking ownership of it. Because only the owner is responsible for the memory management of the data piece, the borrower of the data explicitly states that he will only access the data and management is done somewhere else. As a result, the multiple parts of a program can access the same data without copying or moving it, which can be both more efficient and safer. Rust’s borrow checker enforces strict rules to ensure that borrowed references are always valid and used correctly.

Click the link, to read more about borrowing.

fn main() {
let mut x = 5;
let y = &mut x;
add_one(y);
println!("x is now {}", x);
}
fn add_one(num: &mut i32) {
*num += 1;
}

Code in playground >>

Output:

x is now 6

In this example, a declared mutable variable x and its value is set at 5. After that, we can create a mutable reference y to x using the &mut operator.

We pass y as an argument to the add_one function, which takes a mutable reference to an i32 value. Inside the function, the * operator is used to dereference the num reference and add 1 to the underlying value.

Next, we print out the value of x after calling the add_one function. Because y is a mutable reference to x, modifying the value of y inside the add_one function also modifies the value of x.

In this example of borrowing, we pass a reference to the x variable to the add_one function instead of passing the variable itself. This allows us to modify the value of x without moving ownership of the variable.

Lifetimes

Lifetimes are a concept which ensures that references don’t outlive the data they reference and prevents what is called “dangling references”. Lifetimes are present throughout the whole codebase and most of the time are implicitly deduced from the code itself. But sometimes lifetimes cannot be deduced and need to be explicitly annotated. It is worth noting that annotating lifetimes does not affect how long the reference will live but rather is a description of the lifetime relationship between references.

This concept can feel unfamiliar because most programming languages do not have lifetime annotations as it is a concept tightly bundled with the borrowing mechanism.

fn main() {
let string1 = String::from("hello");
let string2 = String::from("world");
let result = longest(&string1, &string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

Code in playground >>

Output:

The longest string is world

Here, we have two string slices x and y with lifetime ‘a. The function longest takes the two string slices x and y as input parameters and returns a reference to the longest string slice. The lifetime ‘a specifies that the returned reference should have the same lifetime as the input parameters.

Inside the function, the lengths of x and y are compared to determine which string slice is longer, and return a reference to that string slice. Because both input parameters have the same lifetime ‘a, the returned reference is guaranteed to be valid for the same duration.

In the main function, we call longest with two String variables string1 and string2. We then print out the result returned by the longest function, which is the longest string slice of the two.

Concurrency

Rust provides powerful and safe abstractions for concurrent programming. By leveraging ownership and type checking all sorts of errors can be prevented before software is run. Adding threading, messaging and safe memory sharing systems, Rust provides a concurrency model called “fearless concurrency”. This model emphasizes static analysis tools and good design patterns to prevent common concurrency-related bugs.

The most fundamental building block is the thread, which can be created with the std::thread::spawn() function.

use std::thread;

fn main() {
let handle1 = thread::spawn(|| {
for i in 1..=5 {
println!("thread 1: {}", i);
}
});

let handle2 = thread::spawn(|| {
for i in 1..=5 {
println!("thread 2: {}", i);
}
});

handle1.join().unwrap();
handle2.join().unwrap();
}

Code in playground >>

Output:

thread 1: 1
thread 1: 2
thread 1: 3
thread 1: 4
thread 1: 5
thread 2: 1
thread 2: 2
thread 2: 3
thread 2: 4
thread 2: 5

Two threads are created by using the thread::spawn function. The first thread prints the numbers 1 to 5, and the second thread prints the numbers 1 to 5 as well.

After starting the threads, we call the join method on each handle to wait for the threads to finish executing before exiting the main function.

In this concurrency example, we are running multiple threads of execution at the same time. The println statements in each thread will interleave with each other and be printed out in a non-deterministic order.

Rust provides several tools for concurrent programming, including threads, channels, and locks. These tools can be used to build concurrent applications that take advantage of multiple cores and avoid blocking operations.

More concepts that facilitate using Rust

The above list of various concepts help with using Rust to its full potential. But they are not the only key concepts. What else can make the developers’ work more efficient and easier? Let’s take a look:

  • Cargo — Rust’s package manager, Cargo, provides a unified way to manage dependencies, build projects, and publish packages.
  • Traits — Traits define a set of methods that a type must implement, allowing for generic programming and code reuse.
  • Option and Result — Option and Result are built-in enums that provide a way to handle errors and missing values in Rust.
  • Macros — Macros allow for metaprogramming which is basically writing code that generates other code.
  • Unsafe code — Rust allows for writing unsafe code when necessary, but it’s designed to encourage safe programming practices.

A brief overview of the Rust ecosystem

The Rust programming language offers a broad list of helpful libraries, packages, frameworks, and tools that are constantly being improved and developed by the Rust community.

It is worth mentioning that the toolkit should be adjusted to your project requirements and personal preferences. However, below you can find some of the more popular choices among Rust enthusiasts.

One of them is the Rust package manager Cargo which facilitates publishing Rust packages to the official Rust package registry. When it comes to libraries available for Rust, the common choices are Rocket, Diesel, Serde, Tokio, and Actix.

Widely-used Rust tools are Rustfmt, Clippy and Rust Language Server, which facilitate formatting Rust code, improving code quality and error checking.

However, the above names are only a small sample of the available Rust resources. You can find an unofficial list, curated by the community, here.

Rust community and learning sources

Many entry-level developers have appreciated the supportive and helpful Rust community and the variety of learning resources and tools available to help developers learn Rust. Here are some of the most popular resources for Rust:

If you are interested in comparisons of Rust with other languages here you will find comparisons with the most popular languages regarding security, speed, and more.

Rust in practice

Rust is used in many projects, from system programming to web development and beyond. Some notable examples of Rust in action include Stylo, a pure-Rust CSS engine that powers the Firefox web browser, and Redox, an experimental operating system written entirely in Rust. Rust’s performance, safety, and concurrency features also make it a popular choice for serverless computing, with projects like Amazon Firecracker using Rust to implement lightweight virtualization for cloud-based applications.

For more real-life examples which show how the Rust language works, check out applications like:

  • Iron, that demonstrates how to build a web application.
  • Raytracer, an application that shows how to write a graphical application.
  • Chat server, that helps with understanding how to write a networked application.

What kind of projects will level up with Rust?

Rust might be a good choice for projects with crucial requirements like high performance, precision control over threads, and memory safety. This programming language is especially appreciated in projects connected with:

  • Operating systems or microcontroller applications — Rust as a language compiled to the native bytecode allows getting down to the bare metal and operating close to the hardware with performance known from other low-level languages and benefits of all the zero-cost abstractions features.
  • Replacement for software parts — Performance-sensitive parts of the application can be written with Rust and easily integrated with projects based on other programming languages, without the need to rewrite the entire product. For example Python-based projects can benefit from Rust using PyO3.
  • Embedded systems — Rust often appears in embedded and bare-metal development, as it provides direct access to hardware and memory. Here, you’ll find more information about Rust in embedded systems.

Conclusion

Rust’s key to success is allowing for concurrent programming while providing great performance without compromising on memory safety. Another reason for Rust’s growing popularity is the fact that it has a rapidly growing ecosystem of libraries, packages, frameworks, and tools.

If you are looking for a modern, fast, and safe programming language, Rust is definitely worth considering — many companies have already used it in their projects with success.

About the author

Karolina Rusinowicz is a content writer at CodiLime with extensive experience collaborating with developers to create insightful and informative articles. Her passion for technology and software development has led her to work closely with Rust developers, producing articles that provide readers with valuable insights into the language and its applications. With a keen eye for detail and a commitment to accuracy, Karolina is dedicated to helping readers stay informed and up-to-date on the latest developments in the tech world.

--

--