Introduction

Welcome to the Rustup book, created and maintained by Trevor Sullivan. In this book, I hope to share knowledge about the Rust programming language.

Objectives

  • Onboard more developers globally to the Rust language
  • Evangelize the benefits of Rust
  • Share lesser-known tips and tricks on the Rust language
  • Write Rust documentation in my own verbiage

Why Rust?

Rust is a high-performance, cross-platform language, with significant backing from large companies like Microsoft, Google, etc. Learn more about the Rust leadership team on the Rust Foundation website.

Rust can be used to build many different types of applications:

  • CLI tools
  • Terminal User Interfaces (TUI)
  • Graphical applications
  • PC games
  • Operating systems

Remember that Rust is just another tool in your software toolbox. Other programming or scripting languages have their own set of merits. Don't limit yourself to being a "Rust developer" or "Other Language developer." Instead, evaluate your needs against the strengths of Rust, and decide if Rust is the best language for your use case.

Resources

This page includes a list of useful resources for learning about the Rust programming language.

This video playlist from Trevor Sullivan includes a variety of useful videos covering the Rust language fundamentals.

Variables

Like most other programming languages, Rust allows you to declare variables with a specific data type. You use the let keyword to indicate that you are declaring a new variable.

Variable Mutability

By default, variables in Rust are immutable, meaning that once they've been initialized, the values cannot be changed.

#![allow(unused)]
fn main() {
let name = "Trevor";
name = "Trevor";       // This is NOT allowed, and results in a compiler error.
}

If you need to change the value of a variable, after it has been initialized, you can use the mut keyword before the variable name.

#![allow(unused)]
fn main() {
let mut name = "Trevor";
name = "Joe";
}

These examples only show mutating values of a primitive data type. For more complex data structures, using the mut keyword may be necessary to allow alteration of the object's internal state.

Specify Data Type

When you declare a Rust variable, you can specify its data type, directly following the variable name. For numeric types, you can specify the size of the type, such as u8 (unsigned 8-bit integer) or i128 (signed 128-bit integer).

Numeric Values

#![allow(unused)]
fn main() {
let age = 30u8;                  // Define an unsigned 8-bit integer
let number_of_ducklings = 8u128; // Define an unsigned 128-bit integer (largest possible size)

let cost: f32 = 30.28; // Define a 32-bit floating point value
let cost: f64 = 80.99; // Define a 64-bit floating point value

let item_price = 73.09f32; // Define a 32-bit floating point value
let item_price = 91.12f64; // Define a 64-bit floating point value
}

Variable Shadowing

Although some other languages don't allow you to re-declare the same variable, Rust does allow this. We call this behavior variable "shadowing." You can use the same variable name that has already been declared in the current scope to declare a new variable with the let keyword. When the shadow variable goes out of scope, the variable reverts to its previous value. Variable shadowing allows you to assign different data types to the same variable name (ie. &str and u8).

Here's a simple example on the Rust playground.

#[tokio::main]
async fn main() {
    println!("How does Rust variable shadowing work?");
    let person = "Trevor";
    
    // Let's shadow the variable in a child block / scope
    {
      println!("The person variable is: {0}", person);
      
      // This shadowed variable goes out of scope
      // at the end of the block, and reverts to its previous value
      let person = 30u8; 
      println!("Now we change person variable to: {0}", person);
      
      // Now we will nest another child block inside the parent block
      {
          let person = 1.18992152f32;
          println!("Now person has a floating point value: {0}", person);
      } // The f32 value goes out of scope here
    } // The u8 value goes out of scope here
    println!("Now we're back to: {0}", person);
}

The output of this snippet should look similar to this:

How does Rust variable shadowing work?
The person variable is: Trevor
Now we change person variable to: 30
Now person has a floating point value: 1.1899215
Now we're back to: Trevor

TODO

Rust Control Flow

Rust supports a variety of control flow statements that allow you to branch code based on certain conditions.

If..Else Statements

Let's construct a simple Rust program that checks your age, to see if you're old enough to drink (in the USA). First, we declare an integer variable to hold an age. Next, we use an if statement to execute a block of code, depending on the value of the age variable. Using the >= comparison operator, we check if age is greater than, or equal to, 21. We'll use the built-in Rust println! macro to write some text to the terminal, but only if the age variable is greater than, or equal to, 21.

#![allow(unused)]
fn main() {
let age = 21;
if age >= 21 {
  println!("You can drink beer!");
}
}

Since the age is indeed equal to 21, the comparison expression returns a boolean true value, and the if block is executed. If you change the value of the variable to something less than 21, you will see that the text is not printed out.

#![allow(unused)]
fn main() {
let age = 19;
if age >= 21 {
  println!("You can drink beer!");
}
}

You can also include an else block to execute in case the if condition does not return true. The else block will match any condition that is not satisfied by the if expression.

#![allow(unused)]
fn main() {
let age = 19;
if age >= 21 {
  println!("You can drink beer!");
}
else {
  println!("You need to wait a bit longer to drink alcoholic beverages.")
}
}

A more advanced rendition of the if block is to use the else if statement before the final else block. This allows you to check more conditions, before resorting to the else block. You can use else if blocks without having an else block as well, but it's usually a good idea to handle any edge cases with a catch-all else block.

#![allow(unused)]
fn main() {
let age = 19;
if age >= 21 {
  println!("You can drink beer!");
}
else if age == 20 {
  println!("You are only one year away from legally drinking beer!")
}
else if age == 19 {
  println!("Hold on a couple more years");
}
}

Loop Keyword

For an infinite loop, you can use the loop keyword. To exit out of the loop, you must implement your own logic to determine, under what condition, the loop should exit. At the appropriate time, simply use the break statement to exit out of the loop.

Similar to exiting a process, with a given exit code, you can also return a value from the break statement.

Let's build a simple loop example. This example simply prints out a line of text upon every iteration. The loop is infinite, so you would need to use CTRL + C to cancel execution, or kill the process from another utility. To avoid overloading the CPU, we will include a call to the std::thread::sleep() function, which delays the loop iterations.

fn main() {
  loop {
    println!("This is a loop");
    std::thread::sleep(std::time::Duration::from_millis(800));
  }
}

Now that we've create an infinite loop, let's forcibly break out of the loop, after a certain number of iterations. We'll create a mutable integer variable called loop_count, before the loop starts, and initialize it to 0. This variable will track the number of loop iterations that occur. We will also define a variable called max_loops to indicate how many loop iterations we will allow the program to execute.

On every iteration, we need to increment the loop_count variable by 1, using the += assignment operator. Finally, we will create an if block that causes the loop to break if the loop_count exceeds a certain value.

fn main() {
  let mut loop_count = 0u8;
  let max_loops = 5u8;

  loop {
    if loop_count >= max_loops {
      break;
    }
    println!("This is iteration {}", loop_count);
    loop_count += 1;
  }
}

For Loops

One of the most common constructs you'll find across different programming languages is the for loop. This type of loop construct allows you to perform an action a certain number of times, or "for each" input element.

For example, let's say you have a retail store, and need to discount each item's price by 10%. We'll use the simple tuple-based struct to create a list of items inside of a Vec type. If you don't understand the Vec type, just think of it as a dynamically-sized array / list of items for now.

#![allow(unused)]

fn main() {
}

Match Statement

Rust provides a match statement that's used to match patterns against input values. Match statements can return a value, which can be captured as a variable or be returned from a function. Each match statement has multiple "arms" which must address all potential match conditions. You can use the underscore _ as a "catch-all" arm in match statements.

fn main() {
  let age = 22u8;

  match age {
    21.. => { }
  }
}

If you don't specify all potential patterns in the match statement, you'll receive an error similar to the following. Check out the above example on the Rust Playground.

error[E0004]: non-exhaustive patterns: i32::MIN..=21_i32 and 23_i32..=i32::MAX not covered

Let's make sure we satisfy all potential values for the age input. We are using the Rust range operator to account for:

  • All valid values up to 21, and
  • All valid values 21 and greater
fn main() {
  let age = 22u8;

  match age {
    ..21 => { }
    21.. => { }
  }
}

Error: Match Against Mismatched Types

In our earlier example, the Rust compiler knows that we are comparing the match arms to an unsigned 8-bit integer, because of the age variable's data type. Therefore, it wouldn't make sense to add a match arm that compares a u8 to a &str value. Let's try adding another match arm that compares the match input to a string slice.

Try this example on the Rust playground.

fn main() {
  let age = 22u8;

  match age {
    ..21 => { }
    21.. => { }
    "25" => { }
  }
}

When you try to compile and run this example, you'll receive an error message.

error[E0308]: mismatched types this expression has type u8 expected u8, found &str

Whenever you're coding a match statement, you'll want to ensure that your match arms match the data type of the match input.

Return Value from Match Statement

As previously mentioned, you can also return values from match statements. All you need to do is use a let statement to capture the returned value into a variable. Notice that we do not put a semicolon after the return value from each match arm. However, you must put a semicolon at the end of the match statement. The allowed_to_drink variable will contain a bool value.

Check out this example on the Rust Playground.

#[tokio::main]
async fn main() {
  let age = 22; // Try changing this to a different number, to change the result.

  let allowed_to_drink = match age {
    ..21 => { false }
    21.. => { true }
  };

  println!("Are you allowed to drink? {0}", allowed_to_drink);
}

The result should look like this:

Are you allowed to drink? true

Return Match Value from Function

You can return a value from a match statement directly to a function's return value. Let's write a simple function that returns a bool value, and embed the match statement inside it. Notice that we do not put a semicolon at the end of the match statement. This allows the match statement's return value to be passed up to the function's return value.

Here's the following example on the Rust Playground.

fn main() {
    let age01 = 22;
    
    let allowed_to_drink = test_age(age01);
    
    println!("Are you allowed to drink? {0}", allowed_to_drink);
}

fn test_age(age: u8) -> bool {
    match age {
      ..21 => { false }
      21.. => { true }
    } // No semicolon here, which allows match statement to return its value to the function
}

⚠️ If you did accidentally put a semicolon after the match statement's closing curly brace, you would receive this error:

error[E0308]: mismatched types expected bool, found () implicitly returns () as its body has no tail or return expression

The Rust compiler is extremely helpful and tells us how to resolve the error!

help: remove this semicolon to return this value

Using a Catch-All Match Arm

In our earlier examples, we accounted for all possible matches by using only two match arms. But what would happen if we didn't account for all values? Let's say that from 18-20 years old, we want to print a statement that tells the user that they're close to being of drinking age, but still return false. First, we'll limit the first match arm up to age 18. Then we'll add a "catch-all" arm using the underscore character _ that matches anything not accounted for.

Now if you change the age variable to 18, 19, or 20, you should see the message printed out to the terminal.

fn main() {
  let age = 20; // Close to drinking age

  let allowed_to_drink = match age {
    ..18 => { false }
    21.. => { true }
    _ => { println!("You can't drink yet, but you're close to drinking age! 🍺"); false }
  };

  println!("Are you allowed to drink? {0}", allowed_to_drink);
}

The result from the example above will be:

You can't drink yet, but you're close to drinking age! 🍺
Are you allowed to drink? false

Ordering Match Statement Arms

In the catch-all example, we added an underscore as a wildcard that matches any input value to the match statement. It's important to note that the order of match arms affects your Rust code execution. If you put the "catch-all" match as the first match arm, inside your match block, then none of the other match arms will be executed.

In the following example, we will put the wildcard match arm first. The Rust compiler will allow this, but it will also throw a warning about the unreachable match arms. Remember that only the first matching match arm will be executed. Once a match arm has been matched by the input value, all of the other match arms are ignored.

fn main() {
  let age = 25; // Even though we are old enough, we never reach the 3rd match arm

  let allowed_to_drink = match age {
    _ => { println!("You can't drink yet, but you're close to drinking age! 🍺"); false }
    ..18 => { false }
    21.. => { true }
  };

  println!("Are you allowed to drink? {0}", allowed_to_drink);
}

The result from this program will be:

You can't drink yet, but you're close to drinking age! 🍺
Are you allowed to drink? false

Obviously the false result is unexpected, since 25 is old enough to consume alcohol. This would be considered a bug in our program. Thankfully the Rust compiler warns us about the bug.

warning: unreachable pattern

While the underscore being used as an overlapping match arm is extreme, you can also have more limited overlapping match arms. Try the following example on the Rust Playground. You'll notice that the 25 match arm will never be reached, because the 21.. match arm already accounts for the value 25.

fn main() {
  let age = 25;

  let allowed_to_drink = match age {
    ..18 => { false }
    21.. => { true }
    25 => { println!("You're 25 and allowed to drink!"); true }
    _ => { println!("You can't drink yet, but you're close to drinking age! 🍺"); false }
  };

  println!("Are you allowed to drink? {0}", allowed_to_drink);
}

Functions

Functions are one of the most fundamental concepts in Rust. You define a function by using the fn keyword.

Input Arguments

Rust functions can define zero or more input arguments. Each input argument must declare its data type.

For example, check out the input arguments declared within the parentheses of the following function.

#![allow(unused)]
fn main() {
fn get_full_name(first_name: String, last_name: String) {
  return format!("{} {}", first_name, last_name);
}
}

Return Values

Functions can optionally define a return value. By default, the return value of a function is the Rust "unit" type, displayed as a pair of empty parentheses ().

#![allow(unused)]
fn main() {
fn get_name() { } // This function returns a unit type
}

To specify an alternate return value than the "unit" type, add a "thin arrow" after your function input arguments, and specify the return type. After specifying the return type, you can use the return keyword to specify a value that you want the function to return, inside the function body.

#![allow(unused)]
fn main() {
fn get_name() -> String {
  return "Trevor".to_string();
}
}

In addition to returning primitive types, such as integers, floating-point values, and strings, you can also return enum variants or an instance of a user-defined struct. In the following example, we'll define a Dog struct with a name field, and then return a new instance of the Dog type to the function caller. Note that the function named get_dog has declared a return type of Dog.

#![allow(unused)]
fn main() {
struct Dog {
  name: String,
}

fn get_dog() -> Dog {
  return Dog{ name: "Fido".to_string() };
}
}

Calling Functions

To call a Rust function from your application or library, use the function name, followed by parentheses, containing the input arguments.

Rust does NOT support named arguments, as some other languages do. All input arguments are interpreted positionally, according to the order they're declared in. The documentation for a given function, or your development environment, should indicate which order the function arguments ought to be passed in.

Let's define a function with input arguments and a return type.

#![allow(unused)]
fn main() {
fn get_name(first: &str, last: &str) -> String {
  let mut new_string = String::from(first);
  new_string.push_str(" ");
  new_string.push_str(last);
  return new_string;
}
}

To call this function, let's try the following example:

fn main() {
  let full_name = get_name("Trevor", "Sullivan");
  println!("{full_name}");
}

Discard Function Return Value

Capturing the return value of a function call is optional. To discard the return value of a function, you can either:

  • Skip assigning the result to a variable
  • Assign the function's result to the underscore character _
fn main() {
  _ = get_name("Trevor", "Sullivan"); // Valid, explicitly discards the function return value
  get_name("Trevor", "Sullivan");     // Valid, implicitly discards the function return value
}

In some cases, the Rust compiler will issue a warning for implicitly discarding a function return value. For example, if a function returns a Result<T, E>, you might see the following warning:

unused Result that must be used this Result may be an Err variant, which should be handled #[warn(unused_must_use)] on by default

Here's an example:

fn get_name(first: &str, last: &str) -> Result<String, Box<dyn std::error::Error>> {
  let mut new_string = String::from(first);
  new_string.push_str(" ");
  new_string.push_str(last);
  return Ok(new_string);
}

fn main() {
  get_name("Trevor", "Sullivan");
}

Async Functions

Rust functions can be declared as asynchronous, which means that they will return a type implementing the Future trait, by default. You can use the async Rust keyword to denote a function as being asynchronous.

Keep in mind that a Future will not be executed unless you include an async executor in your application. You can write your own executor or you can use an off-the-shelf async executor like smol or tokio.

#![allow(unused)]
fn main() {
async fn get_age() -> i32 {
  return 34;
}
}

We will discuss asynchronous functions, in more depth, in a separate section.

Closures

In Rust, closures are akin to "anonymous functions" in other programming languages. You can define a closure and assign it to a Rust variable. After you've defined the variable pointing to a closure, you can invoke that function later on.

Just like regular functions, closures support input arguments and return types.

The syntax for defining a closure is somewhat different to defining a regular Rust function. The key differences are that:

  • Closure input arguments are surrounded by the pipe character | |, instead of parentheses
  • Closures don't require the use of curly braces to surround the function body
  • Closure return types don't need to be declared

Basic Closure

Here's a basic version of a closure. The empty pipe characters mean that there are no input arguments defined. The function body only has a single statement, and doesn't require curly braces surrounding it.

#![allow(unused)]
fn main() {
let myfn = || println!("Hello from Rust"); // Define the closure
myfn(); // Call the closure
}

Input Arguments

If you want to define input arguments on a closure, add them between the pipe characters.

#![allow(unused)]
fn main() {
let fn_full_name = | first_name: String, last_name: String | println!("{} {}", first_name, last_name);
fn_full_name("Trevor".to_string(), "Sullivan".to_string());
}

Closure Return Values

If you need to return a value from a closure, you don't need to specify the return keyword. The following example will return the result from the format! macro.

#![allow(unused)]
fn main() {
let fn_full_name = | first_name: String, last_name: String | format!("{} {}", first_name, last_name);
let full_name = fn_full_name("Trevor".to_string(), "Sullivan".to_string());
println!("{full_name}");
}

Multi-line Closure Body

While the previous closure examples only contain a single statement, Rust closures can support complex bodies as well. Simply use curly braces to denote the closure body, just like you would with functions.

#![allow(unused)]
fn main() {
let fn_full_name_lower = | first_name: String, last_name: String | {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower("Trevor".to_string(), "Sullivan".to_string());
println!("{full_name_lower}");
}

Capture Environment

Rust closures capture the variables defined in the same scope as the closure definition. This means that variables defined outside the closure can be referenced inside the closure.

Consider the following example.

#![allow(unused)]
fn main() {
let first_name = "Trevor".to_string();
let last_name = "Sullivan".to_string();
let fn_full_name_lower = || {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower();
println!("{full_name_lower}");
}

Rather than passing the first_name and last_name as input arguments, we can define them in the same scope as the closure. When we call the function, the values of the variables are accessible inside the closure body.

Using the Move Keyword

If you don't need to access a variable after the closure is defined, you can use the move keyword to move ownership of the variable into the closure itself.

If you try the following example, you'll receive an error message from the Rust compiler saying "borrow of moved value." That's because ownership of the first_name variable was moved into the closure, and after the closure's scope is dropped, the first_name variable is no longer valid.

#![allow(unused)]
fn main() {
let first_name = "Trevor".to_string();
let last_name = "Sullivan".to_string();
let fn_full_name_lower = move || {
  let fname = first_name.to_lowercase();
  let lname = last_name.to_lowercase();
  return format!("{} {}", fname, lname);
};
let full_name_lower = fn_full_name_lower();
println!("{full_name_lower}");
println!("{first_name}");
}

Structs

Using the struct keyword, you can define custom data types in Rust applications or libraries.

Structs can have zero or more "fields." Each field has a name and a data type.

Basic Struct

Let's say you want to define a custom Rust struct that represents a dog. 🐕 We will define two fields on the struct that represent:

  • The dog's breed, as a String value
  • The dog's age, as an unsigned 8-bit integer u8
#![allow(unused)]
fn main() {
struct Dog {
  breed: String,
  age: u8,
}
}

Instantiate Struct

The struct itself only defines the structure of an object, like a blueprint for a house or car. In order to create an actual dog, we have to instantiate the struct.

To create a struct instance, we use curly braces after the struct name. Inside the curly braces, we specify field names, followed by the values to assign to each field.

#![allow(unused)]
fn main() {
let dog01 = Dog{ breed: "German Shepherd".to_string(), age: 9, };
let dog02 = Dog{ breed: "Border Collie".to_string(), age: 5, };
let dog03 = Dog{ breed: "Australian Shepherd".to_string(), age: 3, };
}

Tuple Structs

A special type of struct known as a tuple-based struct can have unnamed fields, rather than named fields. To access fields from a tuple, you would use the index number of the field, starting at 0, and incrementing by 1.

For example, let's say that you want to define a custom Person type, with a few fields:

  • First name
  • Last name
  • Year Born
#![allow(unused)]
fn main() {
struct Person(String, String, i16);
}

To instantiate this tuple-based struct, simply use the following syntax.

#![allow(unused)]
fn main() {
let p1 = Person("Trevor".to_string(), "Sullivan".to_string(), 1984);
}

You can access the last name field by using the index 1, and the year born with 2. As you can see, these are still valid struct fields, but the fields don't have names, just indexes.

#![allow(unused)]
fn main() {
println!("Your last name is {} and were born in the year {}", p1.1, p1.2);
}

Public Structs

If a struct needs to be accessible outside of a Rust module, use the pub keyword to make it "publicly" visible. By default, structs are private members of a module.

#![allow(unused)]
fn main() {
pub struct Person {
  first_name: String,
}
}

NOTE: Trailing commas are supported in Rust, but are not required.

Traits

Traits are an important concept in Rust that allows you to specify "behaviors" for your data structures. A "behavior" can be an incredibly simple concept, like adding two numbers together. However, in large applications, such as web servers or games, a "behavior" might implement more complex logic.

The basic structure of a Rust trait looks like the following.

trait <TraitName> {
  // Function signatures
}

Traits can define zero or more function signatures as the shared behaviors. After defining a trait, the trait must also be implemented, for any types that you want to use it on.

Example: Animal Behavior

A variety of different animals can "run." Rather than implementing a different run() function on each type of Animal, a trait can define a common function that works for all animal types.

Let's start by definine two different animals as struct types.

#![allow(unused)]
fn main() {
struct Tiger {
  distance_ran: u32,
}

struct Rhino {
  distance_ran: u32,
}
}

Both of these animals can run, but does a tiger run at the same speed as a rhinoceros? Of course not! Even though these two types of animals share a behavior, the specific implementation of that behavior could be different.

Let's define a trait that allows both animal types to share a run() behavior. Our time input argument represents the number of seconds that the animal runs.

#![allow(unused)]
fn main() {
trait AnimalThatRuns {
  fn run(&mut self, time: u8);
}
}

After you define a trait, you also must implement the trait for each type. To accomplish this, we use the impl keyword. We specify the trait name that we want to implement, along with the type that we are implementing it for. The function implementation must match the signature of the function that the trait defines. That means your input arguments, output type, generic arguments, must match the trait's function definition.

For simplicity, let's say a Tiger runs at 60 feet per second, and a rhinoceros runs at 20 feet per second. We'll start by implementing the AnimalThatRuns trait for the Tiger type.

#![allow(unused)]
fn main() {
impl AnimalThatRuns for Tiger {
  fn run(&mut self, time: u8) {
    self.distance_ran += (time as u32)*60;
  }
}
}

Next, we'll implement the AnimalThatRuns trait for the Rhino type.

#![allow(unused)]
fn main() {
impl AnimalThatRuns for Rhino {
    fn run(&mut self, time: u8) {
      self.distance_ran += (time as u32)*20;
    }
}
}

Now that we have implemented the AnimalThatRuns trait for both the Tiger and Rhino types, we are guaranteed that we can use the run() function regardless of which object we have. Let's make both animals run for 5 seconds, and see what the result is.

fn main() {
  let mut t1 = Tiger{distance_ran: 0};
  let mut r1 = Rhino{distance_ran: 0};

  t1.run(5);
  r1.run(5);

  println!("Tiger ran distance of {} feet", t1.distance_ran);
  println!("Rhino ran distance of {} feet", r1.distance_ran);
}

You should see output like the following. The tiger ran 3x further than the rhinoceros, in the same amount of time!

Tiger ran distance of 300 feet
Rhino ran distance of 100 feet

Default Implementations

Traits can have a default implementation, which is used in the absence of a type-specific implementation. To specify a default implementation, simply include a function body in your trait definition.

#![allow(unused)]
fn main() {
trait AnimalThatRuns {
  fn run(&mut self, time: u8) {
    println!("Running for {} seconds", time);
  }
}
}

If you create a new type of animal, and implement AnimalThatRuns, without specifying a custom function definition for the run() function, then the default implementation will be used.

#![allow(unused)]
fn main() {
struct Lion {
  distance_ran: u32,
}

impl AnimalThatRuns for Lion { }
}

As you can see, we still must specify the impl block. Because there's a default implementation for the run() function in the trait block, we are not required to create a custom implementation.

fn main() {
  let mut my_lion = Lion{distance_ran: 0};
  my_lion.run(6);
}

If you run this program, you should see the following output.

Running for 6 seconds

Default Implementations Can't Access Struct Fields

Because the default function implementation doesn't know what type it could be implemented on, you will not be able to reference fields from the struct.

For example, if you try to set a value for the distance_ran field on the Lion type, in the default implementation, you'll receive an error similar to the following.

trait AnimalThatRuns {
  fn run(&mut self, time: u8) {
    println!("Running for {} seconds", time);
    self.distance_ran += 5;
  }
}

no field distance_ran on type &mut Self

Modules

Crates

In the Rust ecosystem, crates are either third-party libraries that you can import into your application, or they can be applications (binaries). The Cargo CLI tool is how you create and manage crates. Cargo is installed along with the Rust toolchain.

Crates can actually contain both libraries and binaries, however they can only contain a maximum of one library. If you choose to include binaries into your crate, then you can expose as many as you want to. Essentially, a Rust crate can contain lots of separate applications, but can only define a single library.

When you create a library or binary, you will have a metadata file called Cargo.toml. This file is customizable by you directly, and the Cargo CLI also makes modifications to it.

Create New Crate

If you want to create a new crate, open an empty folder in your terminal and run cargo init. By default, this will create a binary application. You can run cargo init --lib to create a crate that will be imported into other applications.

Add Binary to Existing Crate

Let's say you already have a Rust crate that declares a library, and you'd like to add an application entrypoint to it. In your Cargo.toml file, you can declare a new [[bin]] header to denote a new binary in your Crate. Underneath each [[bin]] header, you can define the name property to set the compiled binary (application) name. You can set the path property to indicate which Rust source code file contains the main() entrypoint function for the application.

[[bin]]
name = "mycoolapp"
path = "mycoolapp.rs"

Install Rust Tools with Cargo

You can install Rust applications from the crates.io registry. The cargo install command is used to install binaries (applications) that other developers have published to the registry.

For example, try running this command:

cargo install btop

This should compile btop and install the binary into your $HOME/.cargo/bin directory. To verify this simple list the files in that directory.

ls ~/.cargo/bin/

Publish Crate on Crates.io Registry

The crates.io registry is the central place where you can publish your libraries for other developers to consume. You can also search for Rust libraries on crates.io, that you can use in your own Rust applications or libraries.

The high-level process for publishing your own crate looks like this:

  • Login to the registry
  • Cutomize Cargo.toml metadata file
  • Run the publish command

We'll break each of these tasks down in the next few sub-sections.

Login to Registry from Cargo

Before you can publish to the crates.io registry, you'll need to login to the web interface with GitHub and generate an API token. Copy the API token to your clipboard and then run the following command to authenticate from the command line.

cargo login

You'll be prompted to paste your crates.io API token, and after hitting ENTER on your keyboard, you'll be authenticated to the registry.

Customize Cargo.toml

The Cargo.toml file contains metadata about your Rust crate. Running cargo init generates this file, but certain extra settings are required, in order to successfully publish your crate to the registry. For example, here are the settings I had to add to one of my projects, in order to publish it to the registry.

author = "Trevor Sullivan <trevor@trevorsullivan.net>"
license = "MIT"
description = "Btop CLI monitoring tool, re-imagined in Rust 🦀"

Make sure that you add these settings to your Cargo.toml file before you attempt to publish your Rust crate to the registry.

Publish Crate

Now that you've authenticated to the registry and customized your Cargo.toml file, let's publish your Rust crate. Run the following command from your project directory.

cargo publish

When attempting to publish a crate to the crates.io registry, you might receive the error below.

error: 10 files in the working directory contain changes that were not yet committed into git:

By default, the Cargo CLI is expecting your git working tree to be clean, before you publish. However, you can override this behavior by simply appending the --allow-dirty argument onto your cargo publish command.

cargo publish --allow-dirty

Async

Rust supports asynchronous execution in single-threaded and multi-threaded scenarios. With single threads, multiple tasks can run concurrently (not parallel).

In order for async functions to make progress, the poll() function must be called. When you call an async Rust function, you aren't actually executing the code inside the function, because Rust futures are "lazy." Instead, an async function returns something known as a Future, wrapping the return (Output) type of the function.

In order for a future to execute, an "executor" must periodically call the Future's poll() function. Once a Future has completed, it has no more work to do, and the executor can stop invoking the poll() function.

Implement Future Trait

You can create your own asynchronous tasks by simply implementing the std::future::Future trait on custom types.

  • Set the Output type of the future (aka. the future's "return type")
  • Define behavior of the poll() function

You might be wondering how exactly the poll() function should be implemented. For starters, you'll want to think about how to break up your "work" into smaller chunks, that can be progressively completed.

Example: Implement Future Trait

Let's explore an example of implementing the Future trait. We will build a Rust binary that represents a Horse 🐴 in a race.

In this example, we will implement a custom type representing a Horse. The Horse will run a race, and perform several laps. Each lap that the horse runs will be a single "unit of work." When we call the poll() function on the Horse, we will advance the number of completed laps by one.

Install Tokio Async Executor

In order to execute our custom future, we'll need to install an async executor.

cargo add tokio --features=full

Turn your main() function into an async function by using the following snippet.

#[tokio::main]
async fn main() {
}

Define Horse Struct

The first thing we need to do is define the Horse struct itself. We'll add three data fields to the struct, to represent the following pieces of information:

  • name: The horse's name
  • laps_total: The number of race laps that the horse is planned to completed
  • laps_current: The number of race laps that the horse has completed thus far
#![allow(unused)]
fn main() {
struct Horse {
  laps_total: u8,
  laps_current: u8,
  name: String,
}
}

Now that the Horse has been defined, we need to instantiate it in our main() function.

#[tokio::main]
async fn main() {
  let horse01 = Horse{ laps_total: 5, laps_current: 0, name: "Dakota".to_string() };
  let horse02 = Horse{ laps_total: 3, laps_current: 0, name: "Shadowfax".to_string() };
}

To execute a future, we need to await it. The await keyword is used like a function call, but without parentheses. If you try to await a struct instance that doesn't implement the Future trait, then you'll receive an error similar to the one below.

let result = horse01.await;

Horse is not a future the trait Future is not implemented for Horse Horse must be a future or must implement IntoFuture to be awaited required for Horse to implement IntoFuture

Let's proceed by taking the compiler's recommendation, and implement the std::future::Future trait on our Horse struct. We'll come back to await when we're done implementing Future.

Implement Future Trait

Defining the Output type for the Future is easy. Simply use the type keyword to denote the type that will be returned by the Future's Output. You can also think of this as the Future's "return type."

You don't always need to return an output from the Future, so you can stick with the "unit" type.

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = (); // Declare the output type resulting from the completed Future
}
}

Implementing the poll() method is the more complicated bit, that we need to address.

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = ();
  
  // Implement the function to make "progress" on the Future
  fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {    
  }
}
}

When you add the poll() function definition, without defining the function body, you'll receive a compiler error similar to the following:

mismatched types expected enum Poll<Task> found unit type ()

This message appears because your poll() function needs to return a variant of std::task::Poll. This Poll enum has only two variants:

  • Pending
  • Ready(T)

If your custom Future still has more work to do, then you need to return Poll::Pending. Otherwise, if your Future is complete, and you are ready to return the final result, use Poll::Ready(T).

If the Horse's number of completed laps is less than the target number of laps, then we need to run a lap (do some work), and then return Poll::Pending. Let's implement that now. The poll() function allows you to reference and mutate the future instance using the self argument.

Also notice that we return Ready(()) once the Horse has completed running all the laps. All code paths must return a value, so if you don't include that line, you'll receive the error below.

if may be missing an else clause ^ expected Poll<()>, found ()

#![allow(unused)]
fn main() {
impl std::future::Future for Horse {
  type Output = ();

  // Implement the function to make "progress" on the Future
  fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {    
    if self.laps_current < self.laps_total {
      println!("Running another lap!")   // Provide some debugging output, so we can see what is happening.
      self.get_mut().laps_current += 1;  // After the race lap has completed, add one to laps_current.
      return std::task::Poll::Pending;   // Return "Pending" to indicate that there's still more work to do.
                                         // By returning "Pending", we politely give control back to the executor.
                                         // The executor will (hopefully) call poll() on our Future again.
    }
    return std::task::Poll::Ready(());
  }
}
}

If you run this future as-is, then your program will hang. That's because you also have to "wake up" your poll() function, using the waker, accessed via the Context argument. If you don't wake up your Future, then it will indefinitely stay in the Pending state.

As an example, the following code snippet should hang.

#[tokio::main]
async fn main() {
  let horse01 = Horse{ laps_total: 5, laps_current: 0, name: "Dakota".to_string() };
  let result = horse01.await;
}

To wake up the Future, let's add the following line, right before the line where we return Pending.

#![allow(unused)]
fn main() {
cx.waker().wake_by_ref();
}

Now, run your application with cargo run and you should see it run to completion.

Videos