about
RustStory Models
6/8/2022
Rust Story Code

Chapter 1. - Rust Models

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

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 is an interesting language, similar to C++, but with some unique differences. To understand those, let's 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 was designed to prevent 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 intergers 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 messaging 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 creation of variadic functions and variatic type lists for generic structs. To create a variatic function you need to use macros. Rust macros seem to be well engineered, so that may not be as inconvenient as it would otherwise be.
This seems like a daunting list of things to learn. It is, but the Rust compiler makes that a lot easier than you might think. Every time your code doesn't follow the rules it fails to compile with usually very clear error messages that help you fix the error(s).

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. ...
These are used effectively by experienced C++ developers to write safe code. But not all C++ developers have enough experience to do that consistently. C++ is safe by convention, but the conventions have to be followed. Rust indentifies safety vulnerabilities and avoids 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.
Assuming we use only unsafe blocks, its easy to identify them in code and, if they are small, fairly easy to build safe wrappers around them. Note however, that there is some controversy in the Rust community about 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. Extrodinarily 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 there are still strong use-cases for C++ and other languages. I expect to write a lot of Rust code, but think I will continue to use 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 with the Rust tool Cargo. Cargo is a package manager, that can build, run, and clean a Rust Package as well as create a new Package and download 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!
 -------------------
Configuring packages and building them are traditionally done with Cargo. Cargo is a package manager, build manager (in some ways like a make file), checker, runner, and cleaner. One of the first things a developer new to Rust needs to learn is the naming conventions that Cargo depends upon to do its business. It looks for directories named src to find code to build, its dependencies, described in Cargo.toml, have to be library packages that come from a local path or from crates.io, and it expects that modules are loaded 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 will also build documentation. That is covered in the Structures chapter in more detail than we have space for here. Note that the cargo generated documentation gives you a source listing for all your code and all the code you depend upon, other than 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 the most important features of the Rust programming language. Rust fully supports value types, and manages to do that without help from the application designer. There are no copy and move constructors, assignment operators, or destructors to write. Every name is bound to a value on the stack. The value may have additional structure on the heap which it manages without prompting by the designer. The String and Vec types are examples. When a variable bound to some type goes out of scope, it drops (deallocates) any heap resources it may own. When a new name, x, binds to an existing variable y: let x = y; That results in a copy if y's value is copyable, e.g., blittable. If not, then y's value is moved to x, passing ownership. In that case, y is no longer usable. If code attempts to use a moved instance that results in 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 don't own their "borrowed" values: let r = &x; A Rust program many have any number of non-mutating references to a given value. However, it may have at most one mutating reference, and in that case, no other references. let r = &mut x; Since references don't own the values to which they bind, nothing happens to the values when they go out of scope. Drops happen only for an owned value when the owner, o, goes out of scope or are dropped with a move or call to drop(o). When a value has been borrowed by a reference, e.g., r mutably borrowing x's value, the original owner, x, can not mutate the value, because r has borrowed the ability to mutate. That restriction continues until the reference, r goes out of scope or is dropped with a call to drop(r). When a non-mutable reference to a value is declared, that creates a view of the value which should not change until the reference is dropped, so, again, the original owner can not mutate its value until all references go out of scope. Note that dropping a reference does not drop the referred value since the reference does not own the value. The most common use of references is to pass an argument to a function by reference. When the function returns, the reference goes out of scope and the original owner may 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 focuses on 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 of the non-collection types. Usually you don't need to supply the specs, but instead may rely on the compiler's strong type inference engine.
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")

Occasionally the Rust compiler needs help defining life time of references. It wants to ensure that a reference does not outlive the value to which it is bound. You can see an example of that help for the S struct, in the dropdown, where we tell the compiler that a literal string member must have the lifetime of the struct. The member declaration syntax is: s:&'a str where the 'a says that the literal string has the same lifetime as the struct. struct S<'a>{i:i32, s:&'a str, }; Where the notation S<'a> identifies "a" as the lifetime of S.

7.0  Rust Object Model

Rust does not have classes but structs are used in a way similar to the way classes are used in C++. 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, implemented by the struct, declare method signatures that must be defined for types that implement them. A trait may provide default method implementations but often does not. Traits are used 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.
It is interesting to note that traits can be applied to existing types, including those provided by the language, to add new methods for the type, much like 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. If you looked at the "User Defined Type with Traits" details dropdown, above, you saw that the layout of a struct can be analyzed using safe pointers1. The results of that analysis are shown in Table 1., below. Rust traits are very similar to Java and C# interfaces. Rust does not support any form of inheritance of implementation, but does support inheriting traits, which can have 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
We see from the table that each field associated with a struct lies within the memory footprint of the struct, and if the struct has any traits2 there will be an embedded pointer to a Vtbl as well. There is only one Vtbl for each type, so the pointer address value is invariant. Because of this layout strategy, a copy3, is simply a memcopy of the entire struct to the destination. For structs that contain no-copy (non-blittable) fields, only moves are allowed, but they work the same way, doing a mem-copy to the destination. The only difference is that for copies the source remains valid, while for moves, the source becomes invalid.
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() } We can pass any type to size_is as long as the type implements the Size trait. Each type that implements traits has an associated virtual method table (vtble). Rust uses type inference to associate an input instance, o for size_is(o:&dyn Size) with its vtble and calls the size method implemented for that type. That means that we can write one function like size_is that will work for an open-ended number of types - they just need to implement the Size trait. And, that function implementation doesn't need to know anything about the incoming types. It just uses the trait methods. That is a big deal!

  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 does not have classes or inheritance of implementation. It does, however, 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 functions. 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 Saluation 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 how this can be used to implement polymorphic operations, e.g., function dispatching based on the type of value bound to a reference, not on the type of the reference. That works because each trait has a virtual function pointer table used for function dispatching.
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 ..."
In the next section we discuss Rust generics. They provide a way of deferring definition of specific 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 are similar to C# and Java Generics. They don't support specialization to broaded the number of types accepted, like C++. They do define constraints to narrow the number of types that compile, like C# and Java. In the function example, above, we've used a constraint to allow only those types with a Debug trait to compile. In the details dropdown, below, we added a another example for both functions and structs, and show using code and output as well.
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 has focused on the main features and models of Rust. You will find more details in succeeding chapters.

10.0  Epilogue:

The models we've covered here: Code Structure, Build Process, Ownership, and User-Defined Types and Traits are what make Rust unique and interesting. We will see more details for each of these in the coming chapters. Rust is interesting, worth spending effort to gain a working knowledge of the language and its uses. To help you do that, I'm providing a set of exercises for you to begin that process. I won't be providing answers, but you will find code in RustBasicDemos very similar to code for the completed exercises.

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:

I've ordered these references with the most elementary or briefest expositions coming first. Later references you will use heavily later. There are a lot of references here. I don't recommend cover-to-cover reading; just browse, stopping at points of interest, and, after that, use them as reference materials.
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
   
  Next Prev Pages Sections About Keys