about
RustStory Models
6/8/2022
Rust Story Repo 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 2019. Create hello_demo Package with Cargo c:\su\temp> cargo init hello_demo Created binary (application) package c:\su\temp> cd hello_demo c:\su\temp\hello_demo> dir Volume in drive C is OS Volume Serial Number is 765A-DAD5 Directory of c:\su\temp\hello_demo 02/23/2020 10:15 AM <DIR> . 02/23/2020 10:15 AM <DIR> .. 02/23/2020 10:15 AM 8 .gitignore 02/23/2020 10:15 AM 234 Cargo.toml 02/23/2020 10:13 AM <DIR> src 2 File(s) 242 bytes 3 Dir(s) 644,435,443,712 bytes free c:\su\temp\hello_demo> dir .\src Volume in drive C is OS Volume Serial Number is 765A-DAD5 Directory of c:\su\temp\hello_demo\src 02/23/2020 10:13 AM <DIR> . 02/23/2020 10:13 AM <DIR> .. 02/23/2020 10:13 AM 45 main.rs 1 File(s) 45 bytes 2 Dir(s) 644,435,443,712 bytes free main.rs c:\su\temp\hello_demo> cd src c:\su\temp\hello_demo\src> type main.rs fn main() { println!("Hello, world!"); } run main.rs c:\su\temp\hello_demo\src> cd.. c:\su\temp\hello_demo> cargo run Compiling hello_demo v0.1.0 (C:\su\temp\hello_demo) Finished dev [unoptimized + debuginfo] target(s) in 0.84s Running `target\debug\hello_demo.exe` Hello, world! Cargo.toml c:\su\temp\hello_demo> type cargo.toml [package] name = "hello_demo" version = "0.1.0" authors = ["James W. Fawcett "] edition = "2018" # See more keys and their definitions at // remainder elided [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:
LinkDescription
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 directory 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 mod helper; // declares helper module // since there is no code block // helper.rs must be in local src and // provide code for this module use helper::{ say }; use demo_lib::{ libsay }; fn main() { helper::say(); say(); demo_lib::libsay(); libsay(); print!("\n Hello, world!\n\n"); } demo/src/helper.rs pub fn say() { print!("\n hello from helper"); } // Including a main function is not a good idea // but doesn't seem to do any harm, even if pub. #[allow(dead_code)] fn main() { print!("\n hello world"); } demo/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" } demo_lib/src/lib.rs pub fn libsay() { print!("\n hello from demo_lib"); } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } Output: cargo -q run hello from helper hello from helper hello from demo_lib hello from demo_lib Hello, world!
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::Cargo.toml [package] name = "display" version = "0.1.1" authors = ["James W. Fawcett "] edition = "2018" [lib] doctest = false [dependencies] examples/demo_display.rs use display::{*}; #[derive(Debug)] struct Point { x: f64, y: f64, z: f64, } fn test_displays() { main_title("demo display"); sub_title("-- shows --"); shows("\n showing type and value:"); putline(); sub_title("-- show_type and show_value --"); let mut str = String::new(); str.push_str("a string"); 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); 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 -- --------------- showing type and value: -- show_type and show_value -- ---------------------------------- value: "a string" -- log -- ------------- TypeId: i8, size: 1 value: 100 TypeId: alloc::vec::Vec, size: 12 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 } -- show -- ------------- this is a Point structure Point { x: 1.0, y: 1.5, z: 2.0 } that's all folks! ------------------- display::lib.rs ///////////////////////////////////////////////// // lib.rs - Demonstrate display types // // // // Jim Fawcett, https://JimFawcett.github.io // ///////////////////////////////////////////////// /* log and do_work are derived from: https://doc.rust-lang.org/beta/std/any/index.html */ use std::fmt::Debug; use std::any::Any; use std::any::type_name; 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() } /*----------------------------------------------- Display underlined main title on console */ pub fn main_title(msg: &str) { print!("\n {}", msg); let s = std::iter::repeat('=') .take(msg.len() + 2) .collect::<String>(); print!("\n {}", s); } /*----------------------------------------------- Display underlined sub title on console */ pub fn sub_title(msg: &str) { print!("\n {}", msg); let s = std::iter::repeat('-') .take(msg.len() + 2) .collect::<String>(); 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() { print!("\n"); } /*----------------------------------------------- pust n newlines to console */ pub fn putlinen(n: usize) { let s = std::iter::repeat('\n') .take(n).collect::<String>(); print!("{}", s); } lib.rs tests ///////////////////////////////////////////////// // display::main.rs tests - Demo display types // // // // Jim Fawcett, https://JimFawcett.github.io // ///////////////////////////////////////////////// #[cfg(test)] mod tests { use super::*; #[derive(Debug)] struct Point { x: f64, y: f64, z: f64, } #[test] 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!"); } } lib.rs test output C:\github\JimFawcett\RustBasicDemos\display> cargo test Finished test [unoptimized + debuginfo] target(s) in 0.02s Running target\debug\deps \display-26eaf9b270500dad.exe running 3 tests test tests::test_show_type ... ok test tests::test_log ... ok test tests::test_show_value ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
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.
Example of copy and move: 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); // print!("\n s = {}", s); // can't use s, been moved Output: 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.
Example of references: 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); Output: 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: Declaring Code: /*-- 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); Output: -- 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: Type Definition: ///////////////////////////////////////////////// // probe_traits.rs - demo user defined traits 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 { use std::mem; 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; } } Using 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::() as usize } } impl Size for f64 { fn size(&self) -> usize { std::mem::size_of::() as usize } } let sx = t.x.size(); let sy = t.y.size(); print!("\n size of x = {}, size of y = {}", sx, sy); print!( "\n 4 bytes is size of pointer to trait vtable" ); 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 ptr to traits vtable.\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::(); let sx = std::mem::size_of::(); let sy = std::mem::size_of::(); 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(); } Output: Demonstrating probe_traits ============================ using Show trait ------------------ Test { x: 42, y: 1.5 } using getters and setters --------------------------- x = 3, y = -3.5 using Size trait ------------------ size = Test instance t = 16 size of x = 4, size of y = 8 4 bytes is size of pointer to trait vtable exploring struct layout with safe pointers -------------------------------------------- Test { x: 42, y: 1.5 } Note: Test implements traits: Show, Size, Debug, Copy, Clone Missing 4 bytes is ptr to traits vtable. address of t = 8190584 address of t.x = 8190592 address of t.y = 8190584 size of t = 16 size of x = 4 size of y = 8 address of t + st = 8190600
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 3. 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: Declaring Code: /////////////////////////////////////////////////////////// // user-defined traits act like interfaces 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 { "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("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 { "Hi, I have a task for you ...".to_string() } } Using Code: #[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); for speaker in v.iter() { print!("\n {:?}",speaker.salutation()); } } Output: -- demo polymorphic struct instances -- "Hello, today we will discuss ..." "Hi good buddy, its me, Joe" "Hi good buddy, its me, Sue" "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've added a another example for both functions and structs, and shown using code and output as well.
Generics Examples: Generic Functions /*----------------------------------------- No-copy arguments will be moved */ fn demo<T:Debug>(t:T) { show_type(&t); show_value(&t); } /*----------------------------------------- refs so arguments will not be moved */ fn demo_ref<T>(t:&T) where T:Debug { show_type(t); show_value(t); } Using Code: 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(); Output: demo_ref ---------- TypeId: alloc::string::String, size: 12 value: "this is a test" TypeId: f64, size: 8 value: 3.1415927 demo ------ TypeId: alloc::string::String, size: 12 value: "this is a testZ" TypeId: f64, size: 8 value: 3.1415927 Generic Structs #[derive(Debug)] struct Point<T> { x:T, y:T, z:T, } // Copy trait works because blittable, provided // that T is blittable #[derive(Debug, Copy, Clone)] struct BetterPoint<T> { x:T, y:T, z:T, } Using Code: 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(); 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); // won't compile - bpt not blittable so it was moved // bpt.x = "one".to_string(); putlinen(2); Output: 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: 24 value: BetterPoint { x: "one", y: "two", z: "three" } TypeId: generics_probes::BetterPoint<&str> , size: 24 value: BetterPoint { x: "1", y: "two", z: "three" } demo_ref and demo with non copy-able struct --------------------------------------------- TypeId: generics_probes::BetterPoint , size: 36 value: BetterPoint { x: "one", y: "two", z: "three" } TypeId: generics_probes::BetterPoint , size: 36 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