Getting Started

Welcome to the "Rust Language Examples" documentation. This is supporting material for the Rust Tech Talk conducted for the Dayton Dynamic Languages SIG.

Setup

The best way to set up and maintain a Rust installation is to use the rustup tool.

If you're running Linux, it's a one-line command. Run this, and follow the prompts:

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

If you are installing in Windows, you may also need to install the Visual Studio Build Tools. The rustup CLI will prompt you if this is the case.

After you've installed Rust, you can update to the latest version at any time by running rustup update.

After rustup finishes, test your installation by running rustc --version. You should see something like this:

rustc 1.63.0 (4b91a6ea7 2022-08-08)

If you'd like to actually test the compiler, create a file named main.rs, with the following contents:

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

Then, run this:

rustc main.rs

This will produce a binary file called main. When you run it, you should see this:

Hello, world!

Learn more: https://www.rust-lang.org/learn/get-started

Cargo

Cargo is the Rust build tool and package manager. It allows you to do things like this:

CommandDescription
cargo newCreate a new Rust project
cargo buildBuild a project
cargo runRun a project
cargo testExecute unit tests in a project
cargo docBuild documentation for a project
cargo publishPublish a project to crates.io

For example, to create new Rust binary project, run the following:

cargo new [project_name]

This will scaffold a new project with the following structure:

[project_name]
.
├── Cargo.toml
└── src
    └── main.rs

Learn more: https://doc.rust-lang.org/stable/cargo/

Crates Registry

The Crates Registry (located at https://crates.io/) is a catalog of thousands of Rust libraries that you can add to your Rust projects. It is comparable to the library and package catalogs for other languages:

LanguageCatalog
.NETNuget
Node.jsnpm Registry
PerlCPAN
PythonPyPI
RubyRubyGems

If, for example, you'd like to add the rand library to your project, you can execute Cargo in your project directory like this:

cargo add rand

This will add an entry for the library to your project's Cargo.toml file. You could also add the entry to Cargo.toml manually:

[dependencies]
rand = "0.8.5"

Data and Object Types

Declarations

Statically Typed

Rust is statically typed. Variables must be declared. If a type is not specified, Rust will try to infer it.

fn main() {
	let my_variable1: i32 = 5;  // Explicit i32
	let my_variable2 = 5;		// Inferred i32

	println!("The value of my_variable1 is {}", my_variable1);
	println!("The value of my_variable2 is {}", my_variable2);
}

Type Conversion

There is no implicit type conversion in Rust:

fn main() {
	// This won't work, because the 5.0 will be treated as a float:
	let my_variable: i32 = 5.0;

	println!("The value of my_variable is {}", my_variable);
}

But, you can use casting for explicit conversion:

fn main() {
	let my_variable: i32 = 5.0 as i32;

	println!("The value of my_variable is {}", my_variable);
}

Mutability

Variables are immutable by default:

fn main() {
	let my_variable: i32 = 5;

	println!("The value of my_variable is {}", my_variable);

	let my_variable = 10;	// This works, because 'let' creates a new instance of
							// the variable.

	println!("The value of my_variable is {}", my_variable);

	my_variable = 15;  	// This does NOT work, because it attempts to assign a value
						// to the existing instance of the immutable variable.

	println!("The value of my_variable is {}", my_variable);
}

If you want variables to be mutable, use the mut keyword:

fn main() {
	let mut my_variable: i32 = 5;

	println!("The value of my_variable is {}", my_variable);

	my_variable = 10;

	println!("The value of my_variable is {}", my_variable);
}

Scalar Types

The scalar types in Rust are integers, floating point, chars, and booleans.

Learn more: https://doc.rust-lang.org/book/ch03-02-data-types.html#scalar-types

Integer

Integer types:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Integer literals:

fn main() {
	let decimal_value:i32 = 98_222;
	println!("Decimal 98_222 is integer {}", decimal_value);

	let hex_value: i32 = 0xff;
	println!("Hex 0xff is integer {}", hex_value);

	let octal_value: i32 = 0o77;
	println!("Octal 0o77 is integer {}", octal_value);

	let binary_value: i32 = 0b0111;
	println!("Binary 0b0111 is integer {}", binary_value);

	let byte_value: u8 = b'A';
	println!("Byte b'A' is integer {}", byte_value);
}

Floating Point

Rust supports two floating point types: f32 (32 bits) and f64 (64 bits).

fn main() {
	let float_var1: f32 = 10.0;

	let float_var2 = 20.0;  // defaults to f64
}

Character

The char type is Rust's most primitive alphabetic type.

fn main() {
	let my_char = 'a';

	println!("The value of my_char is {}", my_char);
}

In Rust, char type assignments are enclosed in single quotes, and strings are enclosed in double quotes:

fn main() {
	let mut my_char = 'a';
	let my_string = "a";

	my_char = "b";  // This will fail!
}

Boolean

Boolean types in Rust have two possible values: true and false. They are one byte in size.

fn main() {
	let my_bool1 = true;			// inferred type
	let my_bool2: bool = false;		// explicit type annotation
}

Compound Types

The two primitive compound types in Rust are tuples and arrays.

Tuple

A tuple provides a means of grouping several values with a variety of types into one compound type.

fn main() {
	let my_tuple1 = (20, 27.4);
	let my_tuple2: (i32, f64) = (10, 12.22);

	println!("{}", my_tuple1);  // This will fail: The Display trait used by println!
								// doesn't know how to handle tuples.
}

Unsupported display types can be handled like this:

fn main() {
	let my_tuple1 = (20, 27.4);					// Inferred type
	let my_tuple2: (i32, f64) = (10, 12.22);	// Explicit type

	println!("Flexible formatting print: {:?}", my_tuple1);		// Specify ':?' as
																// the format
	println!("Pretty print: {:#?}", my_tuple1);					// Specify ':#?' for
																// pretty-print
}

Learn more: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type

Array

Arrays in Rust are similar to tuples, except that all elements must have the same type. They are fixed length.

fn main() {
	let my_array1 = [1,2,3,4,5];			// Inferred type and length
	let my_array2: [i32; 5] = [1,2,3,4,5];	// Explicit type and length

	println!("The contents of my_array1 are {:?}", my_array1);
	println!("The contents of my_array2 are {:?}", my_array2);
}

Learn more: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-array-type

Custom Types

Structures

Rust supports three types of structures:

  • Named tuples
  • Classic C structs
  • Unit structs (useful for generics)

Tuple example:

fn main() {
	struct TupleExample(i32, f32);

	// Instanciate:
	let tuple_example = TupleExample(1, 2.2);

	// Access the fields by position:
	println!("tuple_example contains {:?} and {:?}", tuple_example.0, tuple_example.1);

    // Destructure:
	let TupleExample(integer_field, decimal_field) = tuple_example;
    
	println!("tuple_example contains {:?} and {:?}", integer_field, decimal_field);
}

Classic example:

fn main() {
	struct ClassicExample {
		string_field: String,
		integer_field: u8,
	}

	let classic_example: ClassicExample = 
	 ClassicExample { string_field: String::from("A String value"), integer_field: 10 };

	println!("classic_example values: ({}, {})",
	 classic_example.string_field, classic_example.integer_field);
}

Unit example:

fn main() {
	struct UnitExample;

	let _unit_example = UnitExample;
}

Unit structs have no fields, and are useful in generics.

Note the use of the underscore in _unit_example, indicating to the compiler that the variable is intentionally unused.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/custom_types/structs.html

Enums

There are a few variants of the enum type. Here's the simplest:

fn main() {
	enum EventStatus {
		OK,
		Failed
	}

	let my_result = EventStatus::OK;

	match my_result {
		EventStatus::OK     => println!("Finished successfully!"),
		EventStatus::Failed => println!("Failed..."),
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/custom_types/enum.html

Standard Library Types

The Rust std library expands on primitives with a large number of custom types.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/std.html

Strings

Rust supports two string types: String and &str. A String is a vector of bytes (Vec<u8>), and &str is a slice (&[u8]) pointing to a UTF-8 sequence.

fn main() {
	// Create a reference to a string in read-only memory:
	let string_slice: &'static str = "This is a string reference";
	println!("The value of string_slice is {}", string_slice);

	let mut vector_string: String = "This is a string vector".to_string();
	println!("The value of vector_string is {}", vector_string);
}

[Exercise] What happens if you try to assign a new value to string_slice?

Learn more: https://doc.rust-lang.org/stable/rust-by-example/std/str.html

Standard Library - Misc

The standard library provides additional complex types to support things like:

  • Threads
  • Channels
  • File I/O
  • Program Arguments
  • Foreign Function Interface

You can find enhanced alternatives for many of these by visiting crates.io. For example, clap is a popular crate for working with program arguments.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/std_misc.html

File I/O - Open

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // Create a path to the desired file
    let path = Path::new("hello.txt");
    let display = path.display();

    // Open the path in read-only mode, returns `io::Result<File>`
    let mut file = match File::open(&path) {
        Err(why) => panic!("couldn't open {}: {}", display, why),
        Ok(file) => file,
    };

    // Read the file contents into a string, returns `io::Result<usize>`
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("couldn't read {}: {}", display, why),
        Ok(_) => print!("{} contains:\n{}", display, s),
    }

    // `file` goes out of scope, and the "hello.txt" file gets closed
}

(Code example is copied directly from here.)

Flow Control

Rust provides a comprehensive set of keywords for managing flow of control.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control.html

if/else

Note that the boolean condition does not have to be surrounded by parentheses.

fn main() {
	let conditional: i32 = 10;

	if conditional == 10 {
		println!("The value is 10");
	} else {
		println!("The value is not 10");
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control/if_else.html

loop

The loop keyword provides an infinite loop.

fn main() {
	let mut count:i32 = 0;

	loop {
		count += 1;

		if count == 5 {
			break;
		}

		if count == 3 {
			continue;
		}

		println!("Iteration: {}", count);
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control/loop.html

while

Loops while a condition is true.

fn main() {
	let mut count:i32 = 1;

	while count < 5 {
		println!("Iteration: {}", count);

		count += 1;
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control/while.html

for and range

The for in construct can be used to iterate through an Iterator. For a simple Iterator, a range can be used.

The upper value of the range is not inclusive.

fn main() {
	for item in 1..5 {
		println!("Iteration: {}", item);
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control/for.html

match

The match keyword provides pattern matching similar to a C switch.

fn main() {
	let number:i32 = 10;

	match number {
		1 => println!("The value is one!"),
		2 | 3 | 4 =>
		 println!("The value is two, three, or four (actual value: {})", number),
		5..=8 =>
		 println!("The value is five, six, seven, or eight (actual value: {})", number),
		_ => println!("No special handling for {}", number),
	}
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/flow_control/match.html

Functions

Functions are declared with the fn keyword. Arguments are type-annotated. If the function returns a value, it must be specified after an arrow.

The order of function definitions are not dependent on main().

Let's start with some code that needs refactoring:

fn main() {
	println!("Hello, Jim!");
	println!("Hello, John!");

	let square_of_two:i32 = 2 * 2;
	let square_of_four:i32 = 4 * 4;

	println!("2 squared is {}", square_of_two);
	println!("4 squared is {}", square_of_four);
}

We have two instances of code duplication that can be refactored into functions:

  1. The "Hello" greeting, and
  2. The calculation of squares.

Let's start with "Hello":

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

fn main() {
	say_hello("Jim".to_string());
	say_hello("John".to_string());

	let square_of_two:i32 = 2 * 2;
	let square_of_four:i32 = 4 * 4;

	println!("2 squared is {}", square_of_two);
	println!("4 squared is {}", square_of_four);
}

Next, we'll add a function to handle calculating square values. We need a return value for this.

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

fn squared(input: i32) -> i32 {
	return input * input;
}

fn main() {
	say_hello("Jim".to_string());
	say_hello("John".to_string());

	let square_of_two:i32 = 2 * 2;
	let square_of_four:i32 = 4 * 4;

	println!("2 squared is {}", squared(2));
	println!("4 squared is {}", squared(4));
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/fn.html

Methods

Functions can be connected to a type. They can be defined on the type ("associated functions") or called on an instance of a type ("methods").

Associated functions are generally used like constructors:

struct Data {
	x: f64,
	y: f64,
}

impl Data {
	fn new(x: f64, y: f64) -> Data {
		Data {x: x, y: y}
	}
}

fn main() {
	let data = Data::new(2.0, 2.0);

	println!("The value of data.x is {}", data.x);
	println!("The value of data.y is {}", data.y);
}

Methods operate on an instance of a type:

struct Data {
	x: f64,
	y: f64,
}

impl Data {
	fn new(x: f64, y: f64) -> Data {
		Data {x: x, y: y}
	}

	// Methods use the built-in argument &self, which provides a reference to
	// an instance of the type.
	fn product(&self) -> f64 {
		return self.x * self.y;
	}
}

fn main() {
	let data = Data::new(2.0, 2.0);

	println!("The value of data.x is {}", data.x);
	println!("The value of data.y is {}", data.y);

	println!("The product of x and y is {}", data.product());
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/fn/methods.html

Traits

A trait is a collection of methods defined for an unknown type: Self. They define functionality meant to be shared with multiple types, similar to interfaces and abstract classes.

trait DataTrait {
	fn new(x: f64, y: f64) -> Self;
	fn product(&self) -> f64;
}

struct Data {
	x: f64,
	y: f64,
}

impl DataTrait for Data {
	fn new(x: f64, y: f64) -> Data {
		Data {x: x, y: y}
	}

	fn product(&self) -> f64 {
		return self.x * self.y;
	}
}

fn main() {
	let mut data: Data = DataTrait::new(2.0, 2.0);

	println!("The value of data.x is {}", data.x);
	println!("The value of data.y is {}", data.y);

	println!("The product of x and y is {}", data.product());
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/trait.html

Ownership

Ownership refers to the rules that govern how a Rust program manages memory. These rules are checked at compile time, and if any of the rules are being violated, then the program won't compile. This provides two benefits:

  1. The programmer isn't required to explicitly allocate and free memory, and
  2. No garbage collector is needed at run time.

Learn more: https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html

RAII

Rust enforces RAII (Resource Acquisition Is Initialization), so whenever an object goes out of scope, its destructor is called and its owned resources are freed.

Values in Rust are stack allocated by default. A Box is a smart pointer to a heap allocated value of type T.

Unsafe Operation

fn main() {
    let my_string = bad_reference();
    
	// -> The compiler will evaluate the safety of this operation.
}

fn bad_reference() -> &String {
    let s = String::from("hello");

	println!("{}", my_string);
    
	return &s;	// This tries to return a reference,
				// but the reference is dropped.
}

Safe Operation

fn main() {
	let my_string = good_value();

	println!("{}", my_string);
    
	// ->	The compiler allows this, because ownership is safely
	//		transferred from good_value() to main()
}

fn good_value() -> String {
    let s = String::from("hello");
    
	return s;
}

Scope Examples

fn create_box() {
    // Allocate an integer on the heap
    let _box1 = Box::new(3i32);

    // `_box1` is destroyed here, and memory gets freed
}

fn main() {
    // Allocate an integer on the heap
    let _box2 = Box::new(5i32);

    // A nested scope:
    {
        // Allocate an integer on the heap
        let _box3 = Box::new(4i32);

        // `_box3` is destroyed here, and memory gets freed
    }

    // Creating lots of boxes just for fun
    // There's no need to manually free memory!
    for _ in 0u32..1_000 {
        create_box();
    }

    // `_box2` is destroyed here, and memory gets freed
}

Details here: https://doc.rust-lang.org/stable/rust-by-example/scope/raii.html.

Ownership and Moves

Resources can have only one owner. When ownership is transferred to a new owner, the previous owner can no longer be used.

Simple stack-allocated types

fn main() {
	// 'x' is a stack allocated integer.
	let x = 5u32;

	// Assignment of a simple type like 'x' implements a copy, so the data is
	// copied to 'y' and no resources are moved.
	let y = x;

	// Both values can be independently used
	println!("x is {}, and y is {}", x, y);
}

Complex heap-allocated types

fn main() {
	// 'a' is a pointer to a heap allocated integer
	let a = Box::new(5i32);

	println!("a contains: {}", a);

	// 'b' takes ownership
	let b = a;

	println!("b contains: {}", b);

	// println!("a contains {}", a);  // What happens if you uncomment this line?
}
// This function takes ownership of the heap allocated memory
fn destroy_box(c: Box<i32>) {
	println!("Destroying a box that contains {}", c);

	// 'c' is destroyed and the memory freed
}

fn main() {
	let a = Box::new(5i32);

	println!("a contains: {}", a);

	destroy_box(a);

	// println!("a contains: {}", a);  // What happens if you uncomment this line?
}

Details here: https://doc.rust-lang.org/stable/rust-by-example/scope/move.html

Borrowing

To access data without taking ownership of it, borrowing can be used.

Passing by reference indicates that we want to borrow:

// This function borrows an i32
fn borrow_i32(borrowed_i32: &i32) {
	println!("This int is: {}", borrowed_i32);
}

fn main() {
	// Create a boxed i32, and a stacked i32
	let boxed_i32 = Box::new(5_i32);
	let stacked_i32 = 6_i32;

	// Borrow the contents of the box. Ownership is not taken,
	// so the contents can be borrowed again.
	println!("Start value: {}", boxed_i32);
	borrow_i32(&boxed_i32);
	println!("Return value: {}", boxed_i32);

	// Same result with a stacked i32.
	println!("Start value: {}", stacked_i32);
	borrow_i32(&stacked_i32);
	println!("Return value: {}", stacked_i32);
}

You can't destroy an object while it's borrowed:

// This function takes ownership of a box and destroys it
fn destroy_box_i32(boxed_i32: Box<i32>) {
    println!("Destroying box that contains {}", boxed_i32);
}

fn main() {
    // Create a boxed i32, and a stacked i32
    let boxed_i32 = Box::new(5_i32);

	{
        // Take a reference to the data contained inside the box
        let _ref_to_i32: &i32 = &boxed_i32;

        // Can't destroy `boxed_i32` here.  Why?
        // destroy_box_i32(boxed_i32);
        // ^ What happens if you uncomment this line?
    }

    // `boxed_i32` can now give up ownership to `eat_box` and be destroyed
    destroy_box_i32(boxed_i32);
}

Details here: https://doc.rust-lang.org/stable/rust-by-example/scope/borrow.html

Lifetimes

A variable's lifetime is a construct used by the compiler's borrow checker to track scopes, and ensure all borrows are valid.

// Lifetimes are annotated below with lines denoting the creation
// and destruction of each variable.
// `i` has the longest lifetime because its scope entirely encloses 
// both `borrow1` and `borrow2`. The duration of `borrow1` compared 
// to `borrow2` is irrelevant since they are disjoint.
fn main() {
    let i = 3; // Lifetime for `i` starts. ────────────────┐
    //                                                     │
    { //                                                   │
        let borrow1 = &i; // `borrow1` lifetime starts. ──┐│
        //                                                ││
        println!("borrow1: {}", borrow1); //              ││
    } // `borrow1 ends. ──────────────────────────────────┘│
    //                                                     │
    //                                                     │
    { //                                                   │
        let borrow2 = &i; // `borrow2` lifetime starts. ──┐│
        //                                                ││
        println!("borrow2: {}", borrow2); //              ││
    } // `borrow2` ends. ─────────────────────────────────┘│
    //                                                     │
}   // Lifetime ends. ─────────────────────────────────────┘

Details here: https://doc.rust-lang.org/stable/rust-by-example/scope/lifetime.html

Modules

Modules provide a means for splitting code into logical units. They are similar to namespaces.

Code without modules:

fn say_hello() {
	println!("Hello!");
}

fn say_goodbye() {
	println!("Goodbye!");
}

fn main() {
	say_hello();
	say_goodbye();
}

Code organized as a module:

mod greetings {
	pub fn say_hello() {
		println!("Hello!");
	}

	pub fn say_goodbye() {
		println!("Goodbye!");
	}
}

fn main() {
	greetings::say_hello();
	greetings::say_goodbye();
}

Functions in a module are private by default. You use the pub keyword to make them accessible.

A use declaration can be used to bind a full function path to a new name. This is particularly useful for deeply nested modules:

use greetings::say_hello as hello;

mod greetings {
	pub fn say_hello() {
		println!("Hello!");
	}
}

fn main() {
	hello();
}

Modules can be mapped to file hierarchies, allowing you to break up functional units into files.

Assuming you have the following functions in a file named greetings.rs:

pub fn say_hello() {
	println!("Hello!");
}

pub fn say_goodbye() {
	println!("Goodbye!");
}

You can then access them as follows:

mod greetings;

fn main() {
	greetings::say_hello();
	greetings::say_goodbye();
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/mod.html

Crates

A crate is a compilation unit. Crates can be compiled as binaries or libraries.

In an empty directory, create two subdirectories: lib and bin. Then, in the lib directory, create a library crate by creating a file named greeting.rs, with the following contents:

pub fn say_hello() {
	say_greeting("Hello".to_string());
}

pub fn say_goodbye() {
	say_greeting("Goodbye".to_string());
}

fn say_greeting(prefix: String) {
	println!("{}!", prefix);
}

Build the library with the following command:

rustc --crate-type=lib greeting.rs

This will create a file named libgreeting.rlib.

Next, in the bin directory, create a file named client.rs, with the following contents:

fn main() {
    greeting::say_hello();

    greeting::say_goodbye();
}

Build the client (and link it to the library) with this:

rustc client.rs --extern greeting=../lib/libgreeting.rlib --edition=2018

This will create an executable named client. When you run it, you'll see this:

Hello!
Goodbye!

Generics

Rust provides support for generics: abstractions of concrete types. Generics provide a way to reduce code duplication.

Duplicate Code

We'll start with a simpler concept: extraction of functions. Consider the following:

fn main() {
	println!("Hello!");
	println!("I'm printing a number: 1");

	println!("Hello!");
	println!("I'm printing a string: text");
}

Functions

In the code block above, we have two very similar sets of code lines. It makes sense to refactor that duplication into a function. But, in order to provide the flexibility of printing numbers and strings, we have to create two functions:

fn print_a_number(input_number: i32) {
	println!("Hello!");
	println!("I'm printing a number: {}", input_number);
}

fn print_a_string(input_string: String) {
	println!("Hello!");
	println!("I'm printing a string: {}", input_string);
}

fn main() {
	print_a_number(1);

	print_a_string("text".to_string());
}

A Generic

To accomplish our task in a single function, we can pass the value to printed as a generic representation <T>:

I cheated a little bit in order to keep this example simple: My argument type is being restricted to values that implement std::fmt::Display.

fn print_a_value<T>(input_value: T) where T: std::fmt::Display {
	println!("I'm printing a value: {}", input_value);
}

fn main() {
	print_a_value(1);

	print_a_value("text".to_string());
}

Learn more: https://doc.rust-lang.org/stable/book/ch10-00-generics.html

Macros

You can use Rust's macro system to implement metaprogramming.

A macro looks and acts very much like a function, except that a macro name ends with a bang !, and instead of generating a function call, macros are expanded into source code that is compiled along with the rest of the program.

Here's simple greeting functionality implemented as a function:

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

fn main() {
	say_hello("John".to_string());
}

And here's the same functionality implemented as a macro:

macro_rules! say_hello {
	($name:expr) => {
		println!("Hello, {}!", $name);
	};
}

fn main() {
	say_hello!("John".to_string());
}

The $name expression in the example above is one of many kinds of designators supported by Rust macros. This provides a level of abstraction somewhat similar to generics, with more flexibility than concrete functions.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/macros.html

Error Handling

Rust provides a variety of ways to deal with error conditions. At a high level, simplified:

ConstructTypically Used For
panicUnrecoverable errors.
Option typeWhen a value is optional, and the lack of an option may or may not be an error condition.
ResultThere is a problem, and it should be dealt with.

Learn more: https://doc.rust-lang.org/stable/rust-by-example/error.html

panic

fn say_hello(name: String) {
	if name == "Todd" {
		panic!("You can't trust a Todd!");
	} else {
		println!("Hello, {}!", name);
	}
}

fn main() {
	say_hello("Jim".to_string());
	
	say_hello("Todd".to_string());

	say_hello("Sam".to_string()); // This will never be reached.
}

Learn more: https://doc.rust-lang.org/stable/rust-by-example/error/panic.html

Option

Option gives us more control over multiple conditions.

fn say_hello(name: Option<&str>) {
	match name {
		Some("Jim") => println!("Jim is a great guy!"),
		Some("Todd") => panic!("You can't trust a Todd!"),
		Some(inner) => println!("Hello {}!", inner),
		None => println!("I don't know who you are..."),
	}
}

fn main() {
	let jim = Some("Jim");
	let john = Some("John");
	let todd = Some("Todd");
	let sam = Some("Sam");

	say_hello(jim);
	say_hello(john);
	say_hello(todd);
	say_hello(sam);  // This will never be reached.
}

What happens if we don't cover None?

Result

Result provides an enhancement over Option in that it describes a possible error instead of a possible absence.

use std::num::ParseIntError;

fn parse_integer(input_value: &str) -> Result<i32, ParseIntError> {
	match input_value.parse::<i32>() {
		Ok(n) => return Ok(n),
		Err(e) => return Err(e),
	};
}

fn main() {
	let result = parse_integer("1");
	println!("{:?}", result);

	let result = parse_integer("text");
	println!("{:?}", result);
}

Testing

Rust supports unit testing, documentation testing, and integration testing.

Unit Testing

main.rs

fn main() {
    println!("10 + 5 is {}", add(10, 5));
}

fn add(first_number: i32, second_number: i32) -> i32 {
    return first_number + second_number;
}

#[cfg(test)]
mod tests;

tests.rs

use super::*;

#[test]
fn test_add() {
    assert_eq!(add(10, 5), 15);
}

Running cargo test in the project root directory will invoke all of the test functions, and display the results.

Unsafe Code

The default behavior of Rust is to provide as many safety checks-and-balances as possible, to protect you from unsafe code. For example, if you try to derefence a raw pointer, the Rust compiler disallows it:

fn main() {
    let raw_p: *const u32 = &10;

	assert!(*raw_p == 10);
}

But, sometimes it's necessary to bypass these protections, not only for raw pointers, but also for operations like calling functions over FFI, and accessing hardware addresses directly (for IoT/embedded programming).

To write unsafe code, you simply wrap the code in an unsafe block, like this:

fn main() {
    let raw_p: *const u32 = &10;

    unsafe {
        assert!(*raw_p == 10);
    }
}

An unsafe wrapper informs the Rust compiler that you know what you're doing, and to turn off its safety checks.

Learn more:

Language Interoperability

Rust supports language interoperability in a couple of ways:

  • Through the built-in Foreign Function Interface (FFI), for calling C functions, and
  • With 3rd-party library support, e.g., PyO3 for calling Python functions.

Learn more about FFI: https://doc.rust-lang.org/stable/rust-by-example/std_misc/ffi.html

C Interop

C code interoperability is accomplished via the Foreign Function Interface.

Let's start with a simple C source file, along with its header file:

simplemath.c

#include "simplemath.h"

float square(float input) { return input * input; }

simplemath.h

float square(float input);

To use this from Rust, we need to first compile the C source, to create simplemath.o (the object file):

gcc -c simplemath.c

Then, create an .ar archive file (libsimplemath.a) from simplemath.o:

ar rcs libsimplemath.a simplemath.o

Next, we'll create a Rust file that will call the C library:

main.rs

/* This 'extern' block describes the C library function we want to call. */
extern "C" {
    fn square(x: f32) -> f32;
}

fn main() {
	/* Our input to the external function */
    let input_value: f32 = 10.0;

	/* Since we're calling an external function, it must be marked as unsafe */
    unsafe {
        println!("The square of {} is {}", input_value, square(input_value));
    }
}

Finally, we compile our Rust crate, and link the external library:

rustc -l static=simplemath -L . main.rs

This produces a binary named main, and when we run it, we get this result:

./main

The square of 10 is 100

PyO3

The PyO3 project provides support for creating Python extension modules in Rust, and also for running Python code from a Rust binary.

PyO3 on GitHub: https://github.com/PyO3/PyO3

PyO3 User Guide: https://pyo3.rs

Rust from Python

To create our Rust-based Python module, we'll be making use of maturin, a build/publish tool.

First, we'll create a directory for our project, and cd to it:

mkdir string_sum

cd string_sum

Next, create a Python virtual environment, and activate it:

python3 -m venv .env

source .env/bin/activate

Install maturin into the Python environment:

pip3 install maturin

Scaffold your project:

maturin init

Select pyo3 for your bindings:

? 🤷 Which kind of bindings to use? ›
❯ pyo3
  rust-cpython
  cffi
  bin

A starter project will be generated, with sample code for Cargo.toml and src/lib.rs. The code provides a simple function accepting two numbers as input, adding them together, and returns the result as a string:

use pyo3::prelude::*;

/// Formats the sum of two numbers as string.
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

/// A Python module implemented in Rust.
#[pymodule]
fn string_sum(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
    Ok(())
}

Finally, build your package and install it into the virtual environment:

maturin develop

Now we're ready to test our new Rust-based package from Python. Start a Python interactive environment:

python3

Then, import the new package and run the function:

>>> import string_sum
>>> string_sum.sum_as_string(1,1)
2

Learn more: https://pyo3.rs/v0.17.1/#using-rust-from-python

Python from Rust

If you want to embed Python into a Rust binary, your Python installation must contain a shared library. For a Debian-based Linux environment, you install this as follows:

sudo apt install python3-dev

Running Python code from Rust is very straightforward. First, in an empty directory, create a new Rust project:

cargo new demo

cd demo

I named my project demo, but that's just an example.

Add a pyo3 dependency to Cargo.toml:

[dependencies.pyo3]
version = "0.17.1"
features = ["auto-initialize"]

Update src/main.rs to call some methods from the Python sys library:

use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

fn main() -> PyResult<()> {
    Python::with_gil(|py| {
        let sys = py.import("sys")?;
        let version: String = sys.getattr("version")?.extract()?;

        let locals = [("os", py.import("os")?)].into_py_dict(py);
        let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'";
        let user: String = py.eval(code, None, Some(&locals))?.extract()?;

        println!("Hello {}, I'm Python {}", user, version);
        Ok(())
    })
}

Compile and run the program:

cargo run

You'll see something like this:

Hello jimc, I'm Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]

Learn more: https://pyo3.rs/v0.17.1/#using-python-from-rust

Many usage examples: https://pyo3.rs/v0.17.1/python_from_rust.html

ASCII Table

Image

Utility Functions

Display Variable Type

#![allow(unused)]
fn main() {
fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}
}