Overview
So you’ve heard about Rust, a shiny new tool taking people by storm, and now you want to know what the fuss is all about. Rust is a programming language that promises to be powerful, fast, and bring modern conveniences to everything from bare metal to high level programs all while having memory safety at its core.
It is statically typed, and a core principle is that everything can be checked at compile time. In the following sections, we will walk you through some rust basics and even walk through some examples you can try yourself!

Getting Started
Windows:
Follow the instructions found at https://www.rust-lang.org/tools/install
Linux/Mac:
Ensure a C Compiler is installed by running one of the following commands.- Mac: $ xcode-select –install
- Debian/Ubuntu: $ sudo apt update
- $ sudo apt install build-essential
- Fedora: $ sudo dnf install gcc-c++
- Arch: $ sudo pacman -S base-devel
- $ curl –proto ‘=https’ –tlsv1.2 https://sh.rustup.rs -sSf | sh
You may check installation succeeded by running the command $ rustc –version
Syntax
The following syntax is a surface level list which aims to get you started. More in depth guides are linked down in the Resources section. Rust is a strongly typed language and data types may be automatically assigned to variables based on their initialization value. Variable data types may also be explicitly specified as necessary.
Primitives- Signed Integers: i8, i16, i32, i64, i128, isize(size of pointer)
- Unsigned Integers: u8, u16, u32, u64, u128, usize(size of pointer)
- Floats: f32, f64
- Boolean: bool
- Character: char
- Tuples, Arrays, Strings: (), [], “…”
- let x = 0; //declares an immutable variable x with a value of zero. By default all variables are immutable and auto typed.
- let mut x = 0; //declares a mutable variable x with a value of zero
- let mut x = 0i32; //declares a mutable variable x with a value of zero of type i32
- if / else if / else
- Match: similar to switch/case in other languages but more powerful as it works on integers, ranges, bools, enums, tuples, arrays, and structs.
- while [condition]
- for [variable] in [range]
- loop: Infinite loop that may return a value passed into a break statement.
- fn [function_name]([param_name:param_type]) -> [return_type]{}
- struct: Define an Object or store data in some custom format.
- enum: Store data in an easily matchable format. Enums are more similar to C/C++ Unions than traditional enums.
- impl: Implement functionality for custom data types
Create/Build/Run/Debug
For the sake of simplicity, this guide will be using Cargo (a built-in project/package manager) and VSCode with some Rust plugins to assist with development. Cargo will handle creating projects, package imports, building code, and running compiled projects. This means that while you need a C compiler installed, you no longer need to use CMake. The Cargo commands used to accomplish those tasks are as follows.
- cargo new [project_name]: Creates a new executable project in the current directory
- cargo build: Builds project at current directory
- cargo run: Runs executable generated by cargo build
When it comes time to debug a program, the VSCode plugins Microsoft C++ for Windows or CodeLLDB on Mac/Linux are necessary for allowing VSCode to manage a debug session. After installing one of these plugins, you will need to enable breakpoints by searching “everywhere” in the VSCode settings editor, and checking the box that says “Allow setting breakpoints in any file”. You may find a more detailed guide here: Enable Breakpoints [Using Rust Analyzer: Debug]. After that, running or debugging is as simple as clicking next to a line of code and pressing Run from the main.rs file.
Examples
Hello World
As with many other language introductions, we are going to start with a simple Hello World example. It introduces the main function present in any Rust program, as well as how to print some text to the console.
One thing to note is that the println command seen below is actually a macro and not a function! The way you can tell is after the println you see an ‘!’ letting you know this is not a regular function. We won’t get into the reasoning why this is the case for this guide, but it is a good thing to notice and be aware of as you learn more about Rust.
// This is the main function.
fn main() {
// Statements here are executed when the compiled binary is called.
// Print text to the console.
// Note: println is a macro and not a function as
// denoted by the '!' after 'println'
println!("Hello World!");
}
Simple Function
The syntax for defining functions and return types can be a little bit confusing at first, so we have provided very basic code which would allow you to sum two unsigned 32 bit numbers together and return the result as the same datatype. Functions definitions follow the format
fn function_name(parameter_name:type) -> return_type as seen below.
An interesting note, however, is that returns may be handled implicitly without the use of the return keyword. However, in times when functions start getting complex, it may make sense to use return explicitly for more clear readability.
// This function takes in two u32 int arguments, adds them together, and
// returns the sum as a u32 int.
// Note: function parameters are listed in the form [variable name]:[type]
fn sum(x:u32,y:u32) ->u32 {
// return x + y here.
x + y
// Rust has the ability to implicitly return data as seen above.
// However, if we want to explicitly return our data we may use
// return x + y;
// It is generally more clear when returns are explicitly stated,
// but for simple functions it may still be perfectly readable to
// return data implicitly like we do above.
}
fn main() {
// print results of sum call. '{}' is used to signify a variable will be
// printed, and the variable is included after the print string using
// commas to seperate variable data.
println!("{}",sum(10,20));
// if you wanted to print more than one variable you could do the following
// println!("var1:{}, var2:{}",someVar,someOtherVar)
}
Memory Safety
Memory safety is a key feature many will point to when the question of “Why should I learn Rust?” comes up. What is memory safety? How could that possibly work in a language that has the ability to create and use pointers? The easy answer is that the Rust compiler is extremely strict in terms of what is allowed with a pointer. In general, this means that no undefined behavior is allowed (use of an address after free, index out of bounds). It also means that ownership over data or objects is closely monitored and the value of some item is dropped when its owner goes out of scope.
Lastly, data may be borrowed, but it can only be accessed by one borrower at a time removing race conditions. It should be noted that the compiler is not 100% fool proof, and while it tends to catch quite a bit, undefined behavior could still appear during run time. In these cases, the program will “panic” halting any further work to prevent harm stemming from continuing on running.
Well how does this all work out in practice? There are many times where the Rust compiler will intervene, but we will keep things relatively simple for now. Below, you will see a simple function that is able to increment the value of a variable at a given address, as well as some code that would not be allowed by the compiler. The compiler sees that an address to a mutable sized type is passed into our function and therefore will allow the value at the address to be read or written to. It will not, however, allow any pointer arithmetic on that pointer itself. For comparison sake, there is some C/C++ code that functions similarly, but also includes some pointer manipulation that Rust would consider “illegal”. The C code shows a simple mistake where the address of some pointer is incremented instead of the value being pointed to which would have been caught at compile time in Rust.
//*****************RUST***********************
fn increment(n: &mut isize) {
//allowed
*n += 1;
//would not compile if uncommented. Incrementing the pointer would step it
//out of bounds for a single isize type value and therefore would cause
//undefined behavior since the address would be invalid.
//n += 1;
}
fn main() {
let mut someVal = 10;
increment(&mut someVal);
println!("{}", someVal);
}
//***************END RUST*********************
//*****************C/C++**********************
void increment(int ** n){
//increment value that pointer is pointing to
(**n) ++;
//uh oh, our parentheses were in the wrong spot
//so we also incremented the pointer itself!
*(*n) ++;
}
int main()
{
//create two integars next to each other in stack memory
int n = 10;
int SomeInt = 10;
//create a pointer to n
int *np = &n;
//increment n, and also shift where np is pointing to
increment(&np);
//add 100 to whatever np is now pointing at
(*np)+=100;
//print n and SomeInt to see that n is now 11, and someInt is now 110 even
//though we never explicitly created a pointer to SomeInt nor did we
//increment it directly. This behavior would not be allowed in Rust
printf("%d\n",n);
printf("%u\n",SomeInt);
return 0;
}
//***************END C/C++********************
Objects
Popular opinion seems to indicate Rust can be considered an object-oriented language, however there is some debate among the community about how true this actually is. This disagreement comes about as Objects in Rust lack inheritance which is common in many other object-oriented languages.
In truth, Rust is a multi-paradigmed language and has many other features found in a typical object-oriented language. As you can see, using a combination of structs and implement (impl) statements lets us create something that is very much like an object as it has a definition, implementation, and can produce unique instances.
For example, below is a simple (x,y) Point that may store its current coordinates and includes a default constructor that will initialize the Point to (0,0) if no other coordinates are provided. This default behavior is caused by the tag #[derive(Default)], also known as the “derive macro”, which signifies that it should contain default values unless otherwise specified in the initialization process. The “derive macro” is also user extensible allowing for default values for custom types!
// Define a Point Struct
// The derive default tag allows the compiler to automatically
// assign some default value in a struct matching its data type.
// for our Point the default value for x,y will be zero.
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
// Implement Point functionality. The impl keyword signifies the
// creation of a custom data type which will contain traits found in our
// struct of the same name and functions implemented below.
impl Point {
// Default constructor
fn new() -> Self {
Default::default()
}
}
fn main() {
// create Point instance using default constructor
let mut p1 = Point::new();
// print x parameter from Point instance
println!("{},{}",p1.x,p1.y);
// update coords and re-print
p1.x = 4;
p1.y = 7;
println!("{},{}",p1.x,p1.y);
}
Inheritance (Kind Of)
While Rust does not have any kind of official built-in inheritance, that doesn’t mean we can’t implement similar functionality ourselves. In general if you want to have inheritance there is usually one struct (the child) containing an instance of some other struct (the parent) which will allow you to incorporate a parents’ features into your own child object.
In this example, you will see how a Point object is incorporated into a Line object demonstrating how we may implement inheritance ourselves.
// Define a Point struct
#[derive(Default)]
struct Point {
x: i32,
y: i32,
}
//Implement Point functionality
impl Point {
// Default constructor
fn new() -> Self {
Default::default()
}
}
// Define a Line struct
#[derive(Default)]
struct Line {
start: Point,
end: Point
}
//Implement Line functionality
impl Line {
// Default constructor
fn new() -> Self {
Self {
start: Point::new(),
end: Point::new(),
}
}
// Function to set Line Coords
fn set_coords(&mut self, x:i32, y:i32, x2:i32, y2:i32){
self.start.x = x;
self.start.y = y;
self.end.x = x2;
self.end.y = y2;
}
// Function to get slope of Line
fn get_slope(&mut self) -> i32{
return (self.end.y - self.start.y)/(self.end.x - self.start.x);
}
}
fn main() {
// Create a Line instance, set its coords, and print it's slope.
let mut l1 = Line::new();
l1.set_coords(0,0,10,20);
println!("{}",l1.get_slope());
}
Error Handling
Another interesting feature of Rust is that it does not contain the traditional Try/Catch blocks found in many other languages. Instead, any code that can fail should be written in such a way that it will return either a failure or an expected value. This is known as either exception-based or decentralized error handling. This allows you to run a match against the result of the function that was run and program logic may be forked based on a return value or error string.
The example below modifies some code from the Line implementation we created earlier as it had an unprotected section that could divide by zero. It will now check if the divisor is zero and either return the result or raise an error to be handled later.
You will likely notice that the return type looks pretty odd, especially if you are not familiar with Rust. So we will break it down before jumping into the example. First, it is returning data that is of type Result<expected_type, error_type>. This means on a good run we will be returning an i32 as expected. On a bad run, a string with the error type will be returned. But what about that funky looking string? Well, it’s a three part type definition. First, we are denoting a pointer being returned with the use of &. The following static section states that the address will exist for the entire runtime of this process. Lastly, the address will be pointing to a string (str) which will contain the error message from some failed state. Now to the example:
...
//return either result or error string
fn get_slope(&mut self) -> Result<i32, &'static str>{
let rise = self.end.y - self.start.y;
let run = self.end.x - self.start.x;
if run == 0 {
//raise and return error condition
Err("Divide by zero error!")
} else {
// return valid result
Ok(rise / run)
}
}
...
fn main() {
let mut l1 = Line::new();
//set line to invalid slope
l1.set_coords(0,0,0,20);
//try to get slope for invalid line
let result = l1.get_slope();
// Match on result to check for good/bad return
match result {
// If division is successful, print the result
Ok(value) => println!("slope: {}", value),
// handle error
Err(error) => println!("Error: {}", error),
}
}
OUTPUT: "Error: Divide by zero error!"
Conclusion
Rust’s syntax may seem esoteric coming from C or C++, but learning the language can pay off big time in terms of memory safety, concurrency, and creating rich embedded applications. Hopefully, this guide is enough to get you started, and if you need any additional help with Rust, Dojo Five is more than happy to lend a hand.
Additionally, if your current or future projects could benefit from some extra manpower, you can book a call with us to get the conversation started. We look forward to hearing from you!
Resources
Additional learning about Rust


