RustStory Repo

RustStory: Models

Code Structure, Builds, Ownership, User-Defined Types and Traits, Generics

"If you always do what interests you, at least one person is pleased."
- Katharine Hepburn

1.0  Prologue

Hello World in Rust
Uses only Cargo, after installing Rust, and the x64 Native Tools Command Prompt for Visual Studio Code.
Create Project
main.rs
C:\github\JimFawcett\RustStory\Chap_1_Models
> cargo new hello_world
Creating binary (application) `hello_world` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
C:\github\JimFawcett\RustStory\Chap_1_Models
> cd hello_world
C:\github\JimFawcett\RustStory\Chap_1_Models\hello_world
> dir

Directory: C:\github\JimFawcett\RustStory\Chap_1_Models\hello_world

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         8/22/2024  12:13 PM                src
-a----         8/22/2024  12:13 PM             82 Cargo.toml

C:\github\JimFawcett\RustStory\Chap_1_Models\hello_world
>
fn main() {
  println!("Hello, world!");
}
Build and Run
Cargo.toml Contents
> cargo run
Compiling hello_world v0.1.0 (C:\github\JimFawcett\RustStory\Chap_1_Models\hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target\debug\hello_world.exe`
Hello, world!
[package]
name = "hello"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]


Rust resembles C++, but differs in important ways. To understand those differences, start with a couple of definitions:
Type Safety
  • A program is well defined if no execution can exhibit undefined behavior.
  • A language is type safe if its type system ensures that every program is well defined.
A non-type safe language may introduce undefined behavior with:
  • mutation of data with active aliases
  • memory access after buffer overflow
  • integer overflow
  • use after free
  • double free
  • race conditions
Rust's type system prevents these behaviors.
Rust Principles:
  1. Rust provides memory safety by: Compiler checked Ownership + Borrowing rules,
    compiler checked indexing where possible, else run-time panic (no reads/writes of unowned memory)
    These prevent simultaneous aliasing + mutation and avoids accessing out-of-bounds indexed memory.
  2. In debug builds signed and unsigned integers are checked for overflow.
  3. All access to heap resources occurs through Box<T>, a smart pointer that releases resources when it goes out of scope.
  4. Rust provides data race safety using two concurrency models: Model #1 - Thread communication through message passing that transfers ownership of messages Prevents simultaneous aliasing and mutation Model #2 - Guarded locks bound to mutated data prevent mutation without ordering These models are compiler checked.
  1. Rust's types are, by default, immutable. You have to opt-in to mutability with the mut keyword: let mut s = String::from("a string");
  2. Ownership:
    In Rust there is only one owner of a value:
    • If a type is blittable1, e.g., primitives, arrays, ... it is copyable: let a1 = [1, 2, 3];
      let a2 = a1;   // a copy
    • If not blittable, e.g., strings, a collection type, ... then it gets moved (transfers ownership): let s1 = "a string".to_string();
      let s2 = s1;   // a move
      let s3 = s2.clone();   // an explicit copy so no move
      In this example, s1 will not be usable after the move. Attempts to use a moved item are compile errors! s3 owns a copy of s2's resource, so s2 is usable after the clone operation.
    • References support borrowing of ownership: let mut s = String::from("another string);   // s owns string
      let rs = &mut s;   // rs borrows ownership from s
    • References must satisfy compiler-checked rules:
      • Only one mutable reference is allowed
      • Any number of un-mutable references are allowed
      • Original owner must not mutate while references are active. References become inactive when they go out of scope or are dropped2.

    1. Data that can be copied with a block transfer, e.g., using memcpy.
    2. let x = String::from("will be dropped"); drop(x);
  3. All resource management is scope based. When an item, holding resources, goes out of scope, its drop function (similar to a C++ destructor) is called.
  4. All heap allocations are referenced with a smart Box<T> pointer that drops its reference when it goes out of scope, similar to the C++ std::unique_ptr<T>.
  5. Rust has a module system, useful for refactoring local code, and packages that need not be local.
  6. It has a relatively complete std library with collections, threads, tcp socket wrappers, file system library, ...
  7. The good news is, in Rust, it is relatively simple to interface to C code. The bad news is it is relatively simple to interface to C code.
  8. Rust has a very useful tool, Cargo, which is a package manager, builder, and verifier, and will execute code, all with a simple command syntax.
  9. The Rust compiler analyzes code deeply and refuses to build code that has memory and reference problems. Because of the structure of its types and its caching strategies, this analysis is faster than you might expect. Compiler error messages are usually clear, simple, and provide advice for fixing the error. If the error is grievous enough, the messages can get confusing, even complaining about correct code.
  10. Rust code layout is more flexible than that of C++, where definitions must come before use. In Rust, an identifier may be used at any place in the code provided only that it has been defined somewhere in the translation unit (similar to Java and C#).
  11. As of now, there is no debugger as part of the Rust tool chain. That isn't as big an issue as you might think. Many most errors are compile-time and the compiler helps with those. Since Rust eliminates many errors by construction, logging works for the small remainder. The Visual Studio Code IDE does have Rust pluggins which support graphical debugging, although the data visualizers aren't as complete as in IDEs like Visual Studio.
  12. Rust uses structs for creating types. Structs can only inherit from traits, a form of abstract type similar to an interface. Traits can, but often do not, implement the methods they declare. This is not full inheritance of implementation because traits do not have data members.
  13. Rust does not implement function overloading.
  14. Rust does not have exceptions - uses panics (controlled shutdown)
  15. Rust generics are like Java and C# generics rather than C++ Templates.
    • C++ template classes can be specialized and template functions can be overloaded. That enlarges the set of types that the template function or class can be successfully instantiated with. Any types that don't work correctly with the generic template can be specialized with code that works for that type.
    • Rust generics can be constrained by the use of trait specifications. That diminishes the set of types that will compile for the generic function or struct. Any type that doesn't implement the trait will not compile when used to instantiate the generic function or struct.
    • Rust generics do not support variadic functions or variadic type lists for generic structs. To create a variadic function you need to use macros. Rust macros are well engineered, so this is less inconvenient than it might seem.
This is a demanding list, but the Rust compiler makes learning it more manageable. Every time code violates a rule, the compiler refuses to build and emits clear error messages that guide you to a fix.

1.1  Comparing to C++

Modern C++ provides lots of tools to promote safe design:
  1. Scope-based resource management via constructors and destructors
  2. Smart pointers: std::unique_ptr<T>, std::shared_ptr<T>
  3. Range-based for loops and begin and end iterators nearly eliminate out-of-bound indexing.
  4. Encapsulation of state in instances of classes
  5. move constructors and assignment
  6. exception handling, STL container at(i) method
  7. ...
Experienced C++ developers use these tools effectively to write safe code. Not all developers have the experience to apply them consistently. C++ is safe by convention - but conventions must be followed. Rust identifies safety vulnerabilities and eliminates them with compiler-enforced rules. Modern C++ Safe by convention Rust Safe by construction This isn't as one-sided as it seems. Rust has two back doors that could re-introduce vulnerabilities:
  1. unsafe blocks:
    There are a few places in library code where the rules may need to be suspended. The application of the compiler's rules may not be precise enough to allow a library designer to build what is needed.
  2. Rust provides an interface to C code through its std::ffi (foreign function interface) crate. This can be used to bind to existing code resources written in C or C++. The Rust interface can only talk directly to C style code, but that code can call into C++ code that uses more advanced features. That imports any vulnerabilities suffered by the foreign code.
When limited to unsafe blocks, they are easy to locate in code and, if small, straightforward to wrap safely. The Rust community does debate the promiscuous use of unsafe code by some open-source developers.

1.2  Features Unique to Rust

  1. Memory and Concurrency safety
    Ownership rules, combined with panics, provide memory safety. Ownership rules, combined with required Mutex and Convar locking and signaling, provide safe concurrency.
  2. Ownership Rules
    Data in Rust can have only one owner. That ownership can be borrowed or transferred, allowing any number of readers if there are no writers, but only one writer with no readers.
  3. Error Handling
    Rust provides Result<T, E> types that are returned by any function that can fail. Code has to explicitly opt out of handling these, and usually should not.
  4. Value behavior without need for copy and move constructors
    Rust will implicitly copy only blittable types, e.g, i32, f64, ...  Non blittable types, e.g., Strings, Vecs, ... are implicitly moved, and may implement the Clone Trait, but clone() must be called explicitly.
  5. Extraordinarily effective tool chain
    Rust provides the cargo package manager that wraps rustc, rustfmt, rustdoc, and rust-clippy to handle most of the common developer tasks other than writing code. Cargo is simple to use, quick, and effective.
There is a lot to like about Rust, but strong use cases for C++ and other languages remain. I expect to write a lot of Rust code, but I will continue using C++ frequently for things I build.

1.2  Things to Read First

Important references for this chapter:
Link Description
Considering Rust - Feb 2020 - Jon Gjengset Great what-is-it video, with code snippets and a lot of mind setting conversation by a very knowledgeable presenter.
RustModels.pdf Discussion of principle ideas on which Rust is based.
Rust std Very clear documentation for the Rust std facilities, e.g., types, modules, macros, and keywords.
A half-hour to learn Rust Code fragments with commentary that cover most of the Rust ideas.
Rust Cheat Sheet Very broad reference to the Rust language, its libraries, and idioms.
Rust Containers Container diagrams
Diving into Rust Contains an excellent video you should watch. Discusses ownership, with copies and moves, and thread models.
Rust Basic Code Demos Small code examples that probe how Rust functions
Places to visit later
Places to visit later Description
Shared reference &T and exclusive reference &mut T More accurate description than immutable reference and mutable reference
Elegant APIs in Rust Things to consider when writing Rust functions and packages
Soundness Pledge Commentary about Rust programming goals and ideals - how not to subvert safety goals.

2.0  Code Structure

Figure 1. Demo_Crate Package in VS Code
Rust code structure is based on crates and packages:
  • Package:
    A Package is a directory containing a cargo.toml file and a /src folder containing a collection of source files. Packages are built with cargo: cargo init [--bin | --lib] [package_name]
    Default option is --bin and default package_name is the current directory name.
    The cargo build command creates a Target subdirectory, if it doesn't already exist, and runs rustc to create an executable or library in Target/debug.
  • Crate:
    The source form of a crate has:
    1. A crate root file with the name main.rs or lib.rs, residing in the /src folder. main.rs has a main function. lib.rs has a test configuration section with one or more tests.
    2. Zero or more supporting source files, *.rs, in /src, called modules, that are imported into the crate root with the keyword mod followed by the module name without extension. Each module may import other lower-level module files that reside in /src.
    3. The crate may depend on other libraries in some local folder, in crates.io, or in a github repository. These are all identified in the [dependencies] section of the cargo.toml file.
    4. Crates compile into a single file. A binary crate has a main.rs root and compiles to a single executable file, [package_name].exe on windows. A library crate has a lib.rs root and compiles to a single library file, lib[package_name].rlib.
All this is illustrated in the "Structure Demo", below. The Structure Demo main.rs loads a module, helper.rs, from its own src directory with the declaration mod helper and then declares use of a public function, say, from helper. The package for main.rs also imports a library demo_lib by including the path to demo_lib in its Cargo.toml file's [dependencies] section. [dependencies]
demo_lib = { path = "../demo_lib"}
If the Cargo.toml file contains an identifier without a path in its [dependencies] section then Cargo looks for a library in https://Crates.io. [dependencies]
demo_lib = { path = "../demo_lib"}
serde = "1.0.104"
The serde crate provides serialization and deserialization of Rust types.
The main.rs file may then simply use demo_lib::say() or declare use helper_lib::{ say } at the top and then use say() in its subsequent code.
Structure Demo
This Structure Demo illustrates how to build a binary crate root, helper module, and a demo library. It has almost no functionality; just serves to illustrate how code is structured from modules and libraries. The main function in demo/src/main.rs uses code from demo/src/helper.rs and demo_lib/src/lib.rs. This shows how to factor code into maintainable pieces.
demo/src/main.rs
demo/src/helper.rs
mod helper; // declares helper module

use helper::{ say };
use demo_lib::{ libsay };

fn main() {
helper::say();
say();
demo_lib::libsay();
libsay();
print!("\n Hello, world!\n\n");
}
pub fn say() {
print!("\n  hello from helper");
}
demo_lib/src/lib.rs
Output
pub fn libsay() {
print!("\n  hello from demo_lib");
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
cargo -q run

hello from helper
hello from helper
hello from demo_lib
hello from demo_lib
Hello, world!
Cargo.toml
[package]
name = "demo"
version = "0.1.0"
authors = ["James W. Fawcett <jfawcett@twcny.rr.com>"]
edition = "2018"

[dependencies]
demo_lib = { path = "../demo_lib" }

Most Rust code starts with a package structure built using Cargo. Cargo is a package manager that builds, runs, and cleans a Rust Package, creates new packages, and downloads crates from https://crates.io.
Figure 1., above, shows a newly created Package. The process is simple.
  1. Open a Developer's command prompt
  2. Create a directory temp with mkdir temp
  3. Issue the command: cargo new demo_crate. That creates everything shown in Figure 1. The image is a screen shot of Visual Studo Code with opened folder demo_crate. The Cargo tool is in your path after you install Rust.
  4. main.rs is a demonstration "Hello World" program.
  5. Build with the command cargo build and run with cargo run.
  6. Clean the crate with cargo clean.
Using Visual Studio Code makes this a fairly painless process. If, using the command window, you navigate to the temp directory and type code ., assuming you've installed VS Code, it will open showing the temp directory and its files and subdirectories, e.g., Cargo.toml and temp/src. You can open a teminal in VS Code, e.g., top menu > Terminal > New Terminal. I usually move the terminal to the right side. You can do that with top menu > View > Appearance > Move Panel Right. You can make your favored panel position the default in top menu > View > Pallet [searh: user settings] > [search panel] . Now, you can edit the package files, save, then, in the terminal type cargo check to test the build without running, and cargo run to build and run. You will see the output in the terminal. When you are done you can cargo clean to remove the target items including the target folder itself. A typical Rust program consists of a binary crate (one with a main.rs) and perhaps several library crates that implement all the functionality used by main. You create a library crate with the command cargo new [library name] --lib. That builds the same structure as shown here except that in the folder src you find a file lib.rs that has functions that implement the library's operations and a test fixture for running a sequence of tests on the library. The binary crate must specify each of the libraries it uses with dependencies in its Cargo.toml file. All of this is illustrated by the code in the two details dropdowns, below.

2.1  Display Library Example:

This display library provides a set of functions to help generate demonstration and debug io for Rust programs. It contains functions:
  1. show_type<T>(_value: &T) - Displays type and size of input
  2. log<T>(value: &T) - Displays type and size of input, followed by its value.
  3. main_title(msg: String) and sub_title(msg: String - Displays msg followed by an underline.
  4. putline() - Emits a newline.
  5. putlinen(n: usize) - Emits n newlines.
  6. ...
The code in display library shows the structure of a lib crate. Note how the library imports Rust std library facilities with a series of use std::xxxx statements. These do essentially the same thing that #include <xxxx> declarations do in C++.
display library
display::lib.rs
examples/demo_display.rs
/////////////////////////////////////////////////////////////
// display::lib.rs - Demonstrate display types             //
//                                                         //
// Jim Fawcett, https://JimFawcett.github.io, 25 Mar 2020  //
/////////////////////////////////////////////////////////////
/*
log and do_work are derived from:
https://doc.rust-lang.org/beta/std/any/index.html

*/
use std::any::type_name;
use std::any::Any;
use std::fmt::Debug;
use std::mem::size_of;

/*-----------------------------------------------
Accepts either String or str
- no automatic newline
*/
pub fn shows<S: Into<String>>(s: S) {
print!("{}", s.into());
}
/*-------------------------------------------------------------
Display message and value
- no automatic newline
*/
pub fn show<T: Debug>(msg: &str, t: &T) {
print!("{}{:?}", msg, t);
}
pub fn str_show<T: Debug>(msg: &str, t: &T) -> String {
format!("{}{:?}", msg, t)
}
/*-------------------------------------------------------------
show value
- expects T to implement Debug
*/
pub fn show_value<T: Debug>(value: &T) {
print!("\n  value: {:?}", value);
}
pub fn str_show_value<T: Debug>(value: &T) -> String {
format!("\n  value: {:?}", value)
}
/*-------------------------------------------------------------
show type name
*/
pub fn show_type<T>(_value: &T) {
let name = std::any::type_name::<T>();
print!("\n  TypeId: {}, size: {}", name, size_of::<T>());
}
pub fn str_show_type<T>(_value: &T) -> String {
let name = std::any::type_name::<T>();
format!("\n  TypeId: {}, size: {}", name, size_of::<T>())
}
/*-------------------------------------------------------------
show type name and value
- expects T to implement Debug
- see #[define(Debug)] attributes, above
*/
pub fn log<T: Debug>(value: &T) {
let name = type_name::<T>();
print!("\n  TypeId: {}, size: {}", name, size_of::<T>());
print!("\n  value:  {:?}", value);
}
pub fn str_log<T: Debug>(value: &T) -> String {
let name = type_name::<T>();
let mut st = format!("\n  TypeId: {}, size: {}", name, size_of::<T>());
let st1 = format!("\n  value:  {:?}", value);
st.push_str(&st1);
st.clone()
}

/*-------------------------------------------------------------
log type name and value
- expects T to implement Debug

This function is deprecated.  Its here to avoid breaking
older code.
*/
pub fn slog<T: Any + Debug>(value: &T) {
let value_any = value as &dyn Any;
let name = type_name::<T>();
print!("\n  TypeId: {}, size: {}", name, size_of::<T>());

// Try to convert our value to a `String`. If successful, we want to
// output the String`'s length as well as its value. If not, it's a
// different type: just print it out unadorned.
match value_any.downcast_ref::<String>() {
    Some(as_string) => {
        print!("\n  value:  String ({}): {}", as_string.len(), as_string);
    }
    None => {
        print!("\n  value:  {:?}", value);
    }
}
}
/*-------------------------------------------------------------
Display underlined main title on console
*/
pub fn main_title(msg: &str) {
print!("\n  {}", msg);
let s = "=".repeat(msg.len() + 2);
print!("\n {}", s);
}
/*-------------------------------------------------------------
Display underlined sub title on console
*/
pub fn sub_title(msg: &str) {
print!("\n  {}", msg);
let s = "-".repeat(msg.len() + 2);
print!("\n {}", s);
}
/*-------------------------------------------------------------
show line with len hyphens
*/
pub fn separator(len: u8) {
let mut s = String::new();
for _i in 1..len + 2 {
    s.push('-');
}
print!("\n {}", s);
}
/*-------------------------------------------------------------
push a single newline to console
*/
pub fn putline() {
println!();
}
/*-------------------------------------------------------------
pust n newlines to console
*/
pub fn putlinen(n: usize) {
let s = "\n".repeat(n);
print!("{}", s);
}

///////////////////////////////////////////////////////////////
// display::main.rs tests - Demonstrate display types        //
//                                                           //
// Jim Fawcett, https://JimFawcett.github.io, 25 Mar 2020    //
///////////////////////////////////////////////////////////////

#[cfg(test)]
mod tests {
use super::*;

#[derive(Debug)]
#[allow(dead_code)]
struct Point {
    x: f64,
    y: f64,
    z: f64,
}
#[test]
/*
   Library doesn't write to console, so all this tests is that
   no panic occurred.  See test_display for useful tests.
*/
fn test_show_type() {
    let mut str = String::new();
    str.push_str("a string");
    assert_eq!(str_show_type(&str).contains("TypeId:"), true);
}
#[test]
fn test_show_value() {
    let mut str = String::new();
    str.push_str("a string");
    assert_eq!(str_show_value(&str).contains("value:"), true);
}
#[test]
fn test_log() {
    let an_i8: i8 = 100;
    assert_eq!(str_log(&an_i8).contains("100"), true);
    let mut vi: Vec<i32> = Vec::new();
    vi.push(-1);
    vi.push(0);
    vi.push(1);
    assert_eq!(str_log(&vi).contains("1]"), true);
    #[derive(Debug)]
    enum Test {
        Test1,
        Test2,
    }
    log(&Test::Test1);
    log(&Test::Test2);
    let point = Point {
        x: 1.0,
        y: 1.5,
        z: 2.0,
    };
    log(&point);
    assert_eq!(str_log(&point).contains("2.0 }"), true);
    sub_title("that's all folks!");
}
}

/* run using: cargo --example demo_displays */

extern crate display;
use display::*;

#[allow(dead_code)] /*-- fields are not explicitly read -*/
#[derive(Debug)]
pub struct Point {
x: f64,
y: f64,
z: f64,
}

fn test_displays() {
main_title("demo display");
sub_title("--  shows  --");
putline();

sub_title("--  show_type and show_value  --");
let mut str = String::new();
str.push_str("a string");
show_type(&str);
show_value(&str);
putline();

sub_title("--  log  --");
let an_i8: i8 = 100;
log(&an_i8);
let mut vi: Vec<i32> = Vec::new();
vi.push(-1);
vi.push(0);
vi.push(1);
log(&vi);
#[derive(Debug)]
enum Test {
    Test1,
    Test2,
}
log(&Test::Test1);
log(&Test::Test2);
let point = Point {
    x: 1.0,
    y: 1.5,
    z: 2.0,
};
log(&point);
print!("\n  {:?}", point);
putline();

sub_title("--  show --");
show("\n  this is a Point structure\n  ", &point);
putline();
sub_title("that's all folks!");
putline();
}

fn main() {
test_displays();
}
Output
  demo display
==============
--  shows  --
---------------

--  show_type and show_value  --
----------------------------------
TypeId: alloc::string::String, size: 24
value: "a string"

--  log  --
-------------
TypeId: i8, size: 1
value:  100
TypeId: alloc::vec::Vec<i32>, size: 24
value:  [-1, 0, 1]
TypeId: demo_display::test_displays::Test, size: 1
value:  Test1
TypeId: demo_display::test_displays::Test, size: 1
value:  Test2
TypeId: demo_display::Point, size: 24
value:  Point { x: 1.0, y: 1.5, z: 2.0 }
Point { x: 1.0, y: 1.5, z: 2.0 }

--  show --
-------------
this is a Point structure
Point { x: 1.0, y: 1.5, z: 2.0 }

that's all folks!
-------------------
Developers configure and build packages using Cargo. Cargo acts as a package manager, build manager (comparable to a makefile), checker, runner, and cleaner. A developer new to Rust must learn the naming conventions Cargo requires. It searches for src directories to locate source code, resolves dependencies declared in Cargo.toml from either a local path or crates.io, and loads modules from the local src directory.

3.0  Build Process:

Figure 2. Build Process
The Rust build process for a crate starts with code in the [crate]/src folder, provided that its [crate]/target folder does not exist or contains items that are out of date. Rust builds are transitive. The command cargo build issued in a terminal opened in the root of a crate, i.e., the directory holding the crate's cargo.toml file:
  1. Builds each dependency cited in it's cargo.toml file, if its build target is not cached or retrieved from the load, by traversing this list from the top.
    • loads and builds each local library identified by:
      [dependencies]
      lib_name = { path = "[path to lib root]" }
    • loads and builds each crate from crates.io specified by:
      [dependencies]
      crate_name = "[version number]"
    • loads and builds each crate from github.com specified by:
      [dependencies]
      crate_name = { git = "https://github.com/[crate_repo]" }
    Each of the build statements in the three items above implement all the same steps. So when a dependency is loaded and no target is available, the build process examines its cargo.toml file and builds its dependencies.
  2. Loads the crate root source file, e.g., main.rs (binary) or lib.rs (library) located in the /src folder.
    • Loads and compiles each local module, identified by the keyword mod [filename] in the crate root source file. Modules are files, *.rs, other than the root file, found in the local /src folder.
    • Then compiles the source of the crate root file.
    Each compilation checks code syntax and checks that ownership rules have not been violated.
Cargo also builds documentation. The Structures chapter covers this in more detail. Cargo-generated documentation provides a source listing for all your code and every dependency, except the std libraries, which are documented here.

4.0  Execution:

The cargo execution model consists of three modes:
  1. Execution of binaries: If the crate root file is a binary, main.rs, the command "cargo run" will run its main function.
  2. Testing libraries:
    If the crate root file is a library, lib.rs, the command "cargo test" will run any tests configured at the end of the library. When cargo builds a library it creates a single configured test at the end of the library that simply asserts that 2 + 2 == 4, to show you how to build tests. Tests succeed if and only if they do not assert, so you use assertions to check test invariants and operation results. When running tests, an assertion failure does not cause a panic shutdown. Instead, the test environment traps the panic and returns an error message.
  3. Running Examples
    For library crates, if you create an /examples folder and put source files, each with a main function, then the command "cargo run --example [file_name]", without the rs extension, will execute that main, using the library as needed. This is a great way to help users understand how to use your library. You might provide several example files for users with different levels of expertise, for example.
Finally, when cargo builds a binary, you will find the execution image in /target/debug and can execute that by navigating to that folder with the command prompt and typing the command [file_name].exe or ./[file_name].exe depending on your platform. You will find a more detailed discussion of these execution processes in the Structures chapter.

5.0 Ownership

Ownership is one of Rust's most important features. Rust fully supports value types without requiring the application designer to write copy constructors, move constructors, assignment operators, or destructors. Every name binds to a value on the stack. That value may manage additional heap structure without any prompting from the designer. String and Vec are examples. When a variable goes out of scope, it drops (deallocates) any heap resources it owns. When a new name, x, binds to an existing variable y: let x = y; The result is a copy if y's value is copyable, e.g., blittable. Otherwise, y's value moves to x, transferring ownership. After a move, y is no longer usable. Any attempt to use a moved instance produces a compile-time error.
Copy and Move
Output

let i = 3;        // i is blittable
let mut j = i;    // j gets copy of i
print!("\n  i = {}, j = {}", i, j);
// i is still usable

let s = String::from("String named s");  // s is not blitable
let mut t = s;                           // s is moved to t
t.push_str(" with more stuff");
print!("\n  t = {}", t);
// statement below will fail to compile
// print!("\n  s = {}", s);              // can't use s, been moved



i = 3, j = 3





t = String named s with more stuff
Rust has references, which bind to and borrow but do not own their values: let r = &x; A Rust program may have any number of non-mutating references to a given value. It may have at most one mutating reference, and in that case no other references. let r = &mut x; Because references do not own the values they bind to, nothing happens to those values when the references go out of scope. Drops occur only for owned values when the owner goes out of scope, or when dropped by a move or a call to drop(o). When a mutable reference r borrows from owner x, the original owner cannot mutate the value because r holds the mutation right. That restriction lasts until r goes out of scope or is released with drop(r). When a non-mutable reference to a value is declared, the value must not change until that reference is dropped. The original owner cannot mutate its value until all references go out of scope. Dropping a reference does not drop the referred value, since the reference holds no ownership. The most common use of references is passing arguments to functions by reference. When the function returns, the reference goes out of scope and the original owner regains the ability to mutate its value.
Mutable References
Output

let i = 3;
let mut j = i;    // copy of i
let r = &mut j;   // mutable ref to j
*r += 2;          // mutating j thru r

///////////////////////////////////////////////////////
// won't compile:
// print!("\n  i = {}, j = {}, r = {}", i, j, r);
// print! takes its arguments by reference
// so can't pass r, as that would make a second
// reference where the first is mutable - not allowed

let z = *r;       // copy r's referent value
print!("\n  i = {}, j = {}, z = {}", i, j, z);














i = 3, j = 5, z = 5
You will find a more detailed presentation of Rust's ownership model by following links in the table, below.
Ownership Details
RustModels.pdf
RustModels code demos
Discussion of RwLock ownership, Copies, Moves, Clones, reference borrowing, mutability with code examples
RustBasicDemos.html Large collection of demos, several for ownership
The next section covers user-defined structuring of code and data.

6.0 Rust Types

Rust's type system has:
  • primitives, i32, f64, ...
  • array [i32, 4]
  • tuple (i32, f64, String)
  • struct { i:i32, f:f64, s:String }
  • enum { BS(String), MS(String), PhD(String) }
  • std collections:
    Vec, VecDeque, LinkedList
    HashMap, HashSet, BTreeMap, BTreeSet
    BinaryHeap
The details dropdown below shows how to write type specifications for each non-collection type. Typically you do not need to supply them - the compiler's type inference engine handles that.
Type Declarations:
Type Declarations
Output
/*-- fully specified --*/
let i:i32 = 5;
let f:f64 = 3.4;
let a:[f32; 5] = [1.0, 1.5, 2.0, 1.5, 1.0];
let t:(i32, f64, String) = (1, 2.0, "three".to_string());
#[derive(Debug)]
struct S<'a>{i:i32, s:&'a str, };
let s:S = S{i:15, s:"a literal string" };
#[derive(Debug)]
enum E {BS(String), MS(String), PhD(String),};
let e:E = E::MS("Computer Engineering".to_string());

print!("\n  -- fully specified types --\n");
print!("\n  i = {:?}", i);
print!("\n  f = {:?}", f);
print!("\n  a = {:?}", a);
print!("\n  t = {:?}", t);
print!("\n  s = {:?}", s);
print!("\n  e = {:?}", e);

/*-- using type deduction --*/
let i = 5;
let f = 3.4;
let a = [1.0, 1.5, 2.0, 1.5, 1.0];
let t = (1, 2.0, "three".to_string());
let s = S{i:15, s:"a literal string" };
let e = E::MS("Computer Engineering".to_string());

print!("\n\n  -- using type deduction --\n");
print!("\n  i = {:?}", i);
print!("\n  f = {:?}", f);
print!("\n  a = {:?}", a);
print!("\n  t = {:?}", t);
print!("\n  s = {:?}", s);
print!("\n  e = {:?}", e);
-- fully specified types --

i = 5
f = 3.4
a = [1.0, 1.5, 2.0, 1.5, 1.0]
t = (1, 2.0, "three")
s = S { i: 15, s: "a literal string" }
e = MS("Computer Engineering")

-- using type deduction --

i = 5
f = 3.4
a = [1.0, 1.5, 2.0, 1.5, 1.0]
t = (1, 2.0, "three")
s = S { i: 15, s: "a literal string" }
e = MS("Computer Engineering")

The Rust compiler occasionally needs help with reference lifetimes. It must ensure that a reference does not outlive the value it is bound to. The S struct in the dropdown illustrates this - the compiler is told that a literal string member must share the lifetime of the struct. The member declaration syntax is: s:&'a str where 'a declares that the literal string has the same lifetime as the struct. struct S<'a>{i:i32, s:&'a str, }; The notation S<'a> identifies "a" as the lifetime of S.

7.0 Rust Object Model

Rust does not have classes, but structs fill a similar role to C++ classes. Structs have:
  • Composed members:
    May be instances of language or user defined types.
  • Aggregated members:
    Uses the Box<T> construct. Box<T> is composed, but points to an instance of T in the native heap. Box<T> acts like the std::unique_ptr<T> in C++. That is, it provides a reference to data on the heap, but deallocates that data when it goes out of scope.
  • Methods: both static and non-static
    A non-static method is declared with a first argument of &self, analogous to the "this" pointer in C++. When invoked the compiler generates code to pass a reference to the invoking instance so the user does not supply that argument. Static methods do not have the &self argument, and so are unable to access member data.
  • Traits:
    Traits declare method signatures that any implementing type must define. A trait may provide default method implementations but often does not. Traits serve like Java or C# interfaces and also provide type constraints for generic functions and types.
  • Access control: using the pub keyword
    A typical struct will be declared public with pub. Some of its methods will also be declared public, but we usually don't make the struct's member data public.
Traits can be applied to existing types, including language-provided types, to add new methods - similar to C# extension methods, but easier to use. Here is an example of a user defined type:
User Defined Type with Traits:
Traits Definition
Test Demo Code
/////////////////////////////////////////////////////////////
// probe_traits.rs - demo user defined traits              //
//                                                         //
// Jim Fawcett, https://JimFawcett.github.io, 22 Mar 2020  //
/////////////////////////////////////////////////////////////

use std::fmt::{Debug};
use display::{*};
/*-----------------------------------------------
  Note that all trait functions are public
*/

/*-----------------------------------------------
  Show trait needs Super trait Debug
  - provides default method implementation
*/
trait Show : Debug {
  fn show(&self) {
    print!("\n  {:?}", &self);
  }
}
/*-----------------------------------------------
Stucts that use Size must implement fn size
  - no default impl provided
*/
trait Size {
  fn size(&self) -> usize;
}

#[derive(Debug, Copy, Clone)]
pub struct Test { // public type
  x:i32, y:f64, // private data
}
/*-----------------------------------------------
  Implementing traits
*/
impl Show for Test {}  // using default impl
impl Size for Test {   // must provide impl
  fn size(&self) -> usize {
    std::mem::size_of::<Test>()
  }
}
/*-----------------------------------------------
  Implementing methods
*/
impl Test {
  pub fn new() -> Self {
    Self {
      x:42,
      y:1.5,
    }
  }
  pub fn get_x(&self) -> i32 {
    self.x
  }
  pub fn set_x(&mut self, v:i32) {
    self.x = v;
  }
  pub fn get_y(&self) -> f64 {
    self.y
  }
  pub fn set_y(&mut self, v:f64) {
    self.y = v;
  }
}

#[allow(dead_code)]
pub fn run () {
  main_title("Demonstrating probe_traits");
  putline();

  sub_title("using Show trait");
  let mut t = Test::new();
  t.show();
  putline();

  sub_title("using getters and setters");
  t.set_x(3);
  t.set_y(-3.5);
  print!("\n  x = {}, y = {}", t.get_x(), t.get_y());
  putline();

  sub_title("using Size trait");
  print!("\n  size = Test instance t = {}", t.size());

  /*-------------------------------------------
    Implementing Size for built in types!
  */
  impl Size for i32 {
    fn size(&self) -> usize {
      std::mem::size_of::<i32>() as usize
    }
  }
  impl Size for f64 {
    fn size(&self) -> usize {
      std::mem::size_of::<f64>() as usize
    }
  }

  let sx = t.x.size();
  let sy = t.y.size();
  print!("\n  size of x = {}, size of y = {}", sx, sy);
  print!("\n  remaining 4 bytes is padding for alignment");
  putline();

  sub_title("exploring struct layout with safe pointers");
  let mut t = Test::new();
  t.show();
  shows("\n  Note: Test implements traits:");
  shows("\n        Show, Size, Debug, Copy, Clone");
  shows("\n        Missing 4 bytes is padding for alignment.\n");
  let rt = &t as *const Test;
  let rx = &t.x as *const i32;
  let ry = &mut t.y as *mut f64;
  let st = std::mem::size_of::<Test>();
  let sx = std::mem::size_of::<i32>();
  let sy = std::mem::size_of::<f64>();
  print!("\n  address of t   = {:?}", rt as i32);
  print!("\n  address of t.x = {:?}", rx as i32);
  print!("\n  address of t.y = {:?}", ry as i32);
  print!("\n  size of t      = {:?}", st);
  print!("\n  size of x      = {:?}", sx);
  print!("\n  size of y      = {:?}", sy);
  print!("\n  address of t + st = {:?}", rt as i32 + st as i32);
  putline();
}
Test Demo Output
  demo display
==============
--  shows  --
---------------

--  show_type and show_value  --
----------------------------------
TypeId: alloc::string::String, size: 24
value: "a string"

--  log  --
-------------
TypeId: i8, size: 1
value:  100
TypeId: alloc::vec::Vec, size: 24
value:  [-1, 0, 1]
TypeId: demo_display::test_displays::Test, size: 1
value:  Test1
TypeId: demo_display::test_displays::Test, size: 1
value:  Test2
TypeId: demo_display::Point, size: 24
value:  Point { x: 1.0, y: 1.5, z: 2.0 }
Point { x: 1.0, y: 1.5, z: 2.0 }

--  show --
-------------
this is a Point structure
Point { x: 1.0, y: 1.5, z: 2.0 }

that's all folks!
-------------------
Figure 3. Trait Method Binding
Any type with a trait has an associated virtual function pointer table (Vtbl), as shown in Figure 3. The "User Defined Type with Traits" dropdown above demonstrates how to analyze struct layout using safe pointers1. Table 1. shows the results of that analysis. Rust traits closely resemble Java and C# interfaces. Rust does not support inheritance of implementation, but does support inheriting traits. Traits carry no member data.
Table 1. Test Struct Memory Layout
Component Address Size - bytes
Test Struct 8190584 16
y:f64 8190584 8
x:i32 8190592 4
padding 8190596 4
The table shows that each struct field lies within the struct's memory footprint. If the struct implements any traits2, the footprint also includes an embedded pointer to a Vtbl. Each type has exactly one Vtbl, so that pointer address is invariant. This layout means a copy3 is simply a memcopy of the entire struct to the destination. Structs containing non-blittable fields support only moves, which work identically - a memcopy to the destination. The difference is that after a copy the source remains valid, while after a move it does not.
Figure 4. Trait Virtual Dispatching
Rust structs support polymorphism through virtual dispatching, as illustrated in Figure 4. Suppose we implement a function:
Function accepting trait object fn size_is(o:&dyn Size) ->usize { o.size() // Size::size() }
Any type that implements the Size trait can be passed to size_is. Each type that implements traits has an associated virtual method table (vtbl). Rust uses type inference to associate the input instance o in size_is(o:&dyn Size) with its vtbl and dispatches to the correct size method.
One function like size_is works for an open-ended set of types - they only need to implement the Size trait. The function implementation needs no knowledge of the incoming types; it operates entirely through trait methods. That is a significant capability.

  1. You can create pointers and display their addresses in safe code, which is what we have done for this analysis. You can only dereference pointers in unsafe blocks, but we did not need to do that.
  2. Almost every struct has traits, at least Debug, and if copyable, Copy and Clone. Adding custom traits can be very useful, as shown in this and the next example.
  3. Stucts with all blittable fields can implement the Copy trait. Those with non-blittable fields cannot.

8.0 User-Defined Types:

Rust has no classes and no inheritance of implementation. It does have structs and traits. A trait is similar to an interface or abstract class. Structs have public and private members and can implement traits as well as other methods. Here's an example, used in the details dropdown, below: Salutation Trait trait Speaker { fn salutation(&self) -> String; } The trait declares a salutation function that structs implement, like this: Implementing Salutation Trait pub struct Presenter; impl Speaker for Presenter { fn salutation(&self) -> String { "Hello, today we will discuss ...".to_string() } } The code in the User-Defined Type dropdown illustrates polymorphic operations - function dispatching based on the type of value bound to a reference, not on the type of the reference itself. This works because each trait has a virtual function pointer table used for dispatch.
Example User-Defined Type:
Trait Defs and Impl : userdef.rs
Demo Code : main.rs
///////////////////////////////////////////////////////////
// user-defined traits act like interfaces               //
// userdef.rs                                            //
///////////////////////////////////////////////////////////

pub trait Speaker {
  fn salutation(&self) -> String;
}

///////////////////////////////////////////////////////////
// The following structs act like classes that implement
// a Speaker interface

#[derive(Debug,Copy,Clone)]
pub struct Presenter;
impl Speaker for Presenter {
  fn salutation(&self) -> String {
    "Presenter: Hello, today we will discuss ...".to_string()
  }
}

#[derive(Debug)]
pub struct Friend {
  pub name : String,
}
impl Speaker for Friend {
  fn salutation(&self) -> String {
    let mut s = String::from("Friend:    Hi good buddy, its me, ");
    let nm = self.name.as_str();
    s.push_str(nm);
    return s;
  }
}
impl Friend {
  #[allow(dead_code)]
  pub fn new(name : String) -> Self {
    Self {
      name,
    }  // note: no semicolon so Self is returned
  }
}
#[derive(Debug,Copy,Clone)]
pub struct TeamLead;
impl Speaker for TeamLead {
  fn salutation(&self) -> String {
    "TeamLead:  Hi, I have a task for you ...".to_string()
  }
}

///////////////////////////////////////////////////////////
// Demonstrate polymorphic operations using Rust Traits  //
// user_def::main.rs                                     //
///////////////////////////////////////////////////////////

mod userdef;
use userdef::*;

#[allow(dead_code)]
pub fn run() {
  print!(
    "\n\n  {}",
    "-- demo polymorphic struct instances --\n"
  );
  let presenter : Presenter = Presenter;
  let joe : Friend = Friend::new("Joe".to_string());
  let sue : Friend = Friend::new("Sue".to_string());
  let team_lead : TeamLead = TeamLead;

  let mut v :Vec<&dyn Speaker> = Vec::new();
  v.push(&presenter);
  v.push(&joe);
  v.push(&sue);
  v.push(&team_lead);

  /*
    Presenters, Friends, and TeamLeads are all Speakers, i.e.,
    they implement the Speaker trait, so they all can be
    treated uniformily, as speakers.
  */
  for speaker in v.iter() {
    print!("\n  {:?}",speaker.salutation());
  }
}

fn main() {
  run();
}
Demo Output
-- demo polymorphic struct instances --

"Presenter: Hello, today we will discuss ..."
"Friend:    Hi good buddy, its me, Joe"
"Friend:    Hi good buddy, its me, Sue"
"TeamLead:  Hi, I have a task for you ..."
The next section covers Rust generics - a way to defer specification of concrete types used as function arguments and struct data members.

9.0 Generics:

Rust supports generics for functions and structs:
Generic Function: fn demo_ref<T>(t:&T) where T:Debug { show_type(t); show_value(t); } Generic Struct: #[derive(Debug)] struct Point<T> { x:T, y:T, z:T, }
Rust generics resemble C# and Java generics. They do not support specialization to broaden the set of accepted types as C++ does. Instead they define constraints that narrow the set of types that compile, as C# and Java do. The function example above uses a constraint that restricts instantiation to types implementing the Debug trait. The details dropdown below adds a further example for both functions and structs, with code and output.
Generics Examples:

9.1 Generic Functions and Structs Example

Generic Function Definitions
use std::fmt::{*};
use display::{*};

/*-----------------------------------------
  Non-copy arguments will be moved
*/
fn demo<T:Debug>(t:T) {
  show_type(&t);
  show_value(&t);
}

/*-----------------------------------------
  ref so arguments will not be moved
*/
fn demo_ref<T>(t:&T) where T:Debug  {
  show_type(t);
  show_value(t);
}
Generic functions are patterns for implementing functions by substituting a specific type for each of the generic type placeholders, like T. This block defines two generic functions, demo<T> and demo_ref<T> which both simply use functions defined in the display library. The only difference is that the first passes its argument by value, i.e., t:T. That causes the argument t to be moved into the body of the function, so the caller's t is no longer valid. The second function passes its argument by reference, i.e., t:&T, so the caller's argument remains valid after the call. This illustrates the consequences of passing by value or reference, as well as illustrating how generic patterns work. The Debug constraint is required because both show_type<T> and show_value<T> will print t using debug format {:?}.
Using Function Definitions
Functions Demo Output
main_title("generics_probes");
let mut s = String::from("this is a test");

sub_title("demo_ref");
demo_ref(&s);
let pi = 3.1415927;
demo_ref(&pi);
s.push('Z');
putline();

sub_title("demo");
demo(s);
demo(pi);
// statement below won't compile - s moved
// s.push('Z');
putline();

 demo_ref
----------
TypeId: alloc::string::String, size: 24
value: "this is a test"
TypeId: f64, size: 8
value: 3.1415927

demo
------
TypeId: alloc::string::String, size: 24
value: "this is a testZ"
TypeId: f64, size: 8
value: 3.1415927

Generic Struct Definitions
use std::fmt::{*};
use display::{*};

#[allow(dead_code)]
#[derive(Debug, Clone)]
struct Point<T> { x:T, y:T, z:T, }

// Copy works because blittable, 
// provided that T is blittable
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
struct BetterPoint<T> { x:T, y:T, z:T, }
Like generic functions, generic structs are patterns for building structs, which are patterns for building concrete instances. In this case, the instances are points. Two generic point classes are defined. The only difference between them is that BetterPoint<T> is marked as satisfying the Copy trait. That prompts the compiler to generate code that copies instances instead of moving them. That only works if if T is copy-able. If we supply a move type for T like a String, then BetterPoint<String> will be a move type, e.g., the compiler ignores the Copy marker trait.
Using Struct Definitions
Structs Demo Output
sub_title("demo_ref and demo with struct");
let mut pt = Point { x:1.0, y:-1.5, z:2.3 };
demo_ref(&pt);
pt.x = 3.2;
demo(pt);
// statement below won't compile - pt moved for demo
// pt.x = 3.2;
putline();

sub_title("demo_ref and demo with copy-able struct");
let mut bpt = BetterPoint { x:1.0, y:-1.5, z:2.3 };
demo_ref(&bpt);
bpt.x = 3.2;
demo(bpt);
// statement ok - pt copied for demo
bpt.x = 3.2;
putline();

/* literal strings are type &str and are copy-able */
sub_title("demo_ref and demo with copy-able struct");
let mut bpt = BetterPoint { x:"one", y:"two", z:"three" };
demo_ref(&bpt);
bpt.x = "1";
demo(bpt);
// statement ok - pt copied for demo
bpt.x = "one";
putline();

sub_title("demo_ref and demo with non copy-able struct");
let mut bpt = BetterPoint {
x:"one".to_string(),
y:"two".to_string(),
z:"three".to_string()
};
demo_ref(&bpt);
bpt.x = "four".to_string();
demo(bpt);
// statement won't compile - pt not blittable
// bpt.x = "one".to_string();

putlinen(2);
}
demo_ref and demo with struct
-------------------------------
TypeId: generics_probes::Point, size: 24
value: Point { x: 1.0, y: -1.5, z: 2.3 }
TypeId: generics_probes::Point, size: 24
value: Point { x: 3.2, y: -1.5, z: 2.3 }

demo_ref and demo with copy-able struct
-----------------------------------------
TypeId: generics_probes::BetterPoint, size: 24
value: BetterPoint { x: 1.0, y: -1.5, z: 2.3 }
TypeId: generics_probes::BetterPoint, size: 24
value: BetterPoint { x: 3.2, y: -1.5, z: 2.3 }

demo_ref and demo with copy-able struct
-----------------------------------------
TypeId: generics_probes::BetterPoint<&str>, size: 48
value: BetterPoint { x: "one", y: "two", z: "three" }
TypeId: generics_probes::BetterPoint<&str>, size: 48
value: BetterPoint { x: "1", y: "two", z: "three" }

demo_ref and demo with non copy-able struct
---------------------------------------------
TypeId: generics_probes::BetterPoint, size: 72
value: BetterPoint { x: "one", y: "two", z: "three" }
TypeId: generics_probes::BetterPoint, size: 72
value: BetterPoint { x: "four", y: "two", z: "three" }

This chapter covers the main features and models of Rust. Succeeding chapters provide more detail on each.

10.0 Epilogue:

The models covered here - Code Structure, Build Process, Ownership, and User-Defined Types and Traits - are what make Rust distinctive. The coming chapters examine each in greater detail. Rust rewards the effort needed to gain a working knowledge of the language and its uses. The exercises below help you begin that process. I won't be providing answers, but RustBasicDemos contains code very similar to completed solutions.

10.1  Exercises:

  1. Write code that intentionally attempts to violate each of the ownership rules. Observe when they fail to compile, comment out the offending code with commentary about why the code failed to compile.
  2. Declare an array of some primitive type, and visit each element, printing out its value. Now repeat for a Vec holding the same primitive type.
  3. Declare a struct that defines a three dimensional point. Define two points with different values and print out the differences between each of the three coordinates for the two points.
  4. Repeat the previous exercise using tuples instead of structs.

11.0 References:

These references are ordered from most elementary to most detailed. You will return to the later ones frequently. There are many references here. Cover-to-cover reading is not recommended; browse, stop at points of interest, and use them as reference materials afterward.
Reference Link Description
Why Rust? - Jim Blandy Monograph on why Rust is important
Rust users forum Answers to a broad range of questions from beginner to advanced.
A half-hour to learn Rust Code fragments with commentary that cover most of the Rust ideas.
Rust by Example Rust docs - walkthrough of syntax
Rust Cookbook Rust docs - a collection of example projects using the Rust libraries and external crates
A Gentle Introduction To Rust Read early in your Rust travels.
The Rust Book Rust docs - walkthrough of syntax
Rust cheat sheet Quite extensive list of cheats and helpers.
Rust Containers Container diagrams
The Rust Reference Book Rust's approximation of a language standard. Clear and well written, with a glossary at the end. Its github site shows that changes are still being actively incorporated.
RIP Tutorial on Rust Comprehensive coverage from RIP, the stackoverflow archive
Learning Rust ebook Comprehensive coverage from RIP, stackoverflow archive
Rust - Awesome Book lots of interesting discussions from RIP, the Stackoverflow Archive
Shared reference &T and exclusive reference &mut T More accurate description than immutable reference and mutable reference
Getting Started with Rust on Windows and Visual Studio Code Install Rust, Verify, Configure Visual Studio Code, Create Hello World, Create Build Task, Configuring Unit Tests, Configure Debugging,
rust-lang.org home page Links to download and documentation
(video) Rust - Crash Course | Rustlang Code demo using basic types.
Tutorial - tutorialspoint.com Tutorials for most of the Rust parts with code examples.
Blog - Pascal's Scribbles Pascal Hertleif - Rust contributor
Blog - Barely Functional Michael Gattozzi - Rust contributor
Blog - S.Noyberg Examples of aysnc/await, tokio, lifetime, ...
Compilation details kmcallister.github.io