about
RustStory Models
6/8/2022
Chapter 1. - Rust Models
Code Structure, Builds, Ownership, User-Defined Types and Traits, Generics
1.0  Prologue
Hello World in Rust
- 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.
- mutation of data with active aliases
- memory access after buffer overflow
- integer overflow
- use after free
- double free
- race conditions
-
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) - In debug builds signed and unsigned intergers are checked for overflow.
- All access to heap resources occurs through Box<T>, a smart pointer that releases resources when it goes out of scope.
-
Rust provides data race safety using two concurrency models:
Model #1 - Thread communication through messaging passing that transfers ownership of messages These models arePrevents simultaneous aliasing and mutation Model #2 - Guarded locks bound to mutated dataprevent mutation without ordering compiler checked.
-
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"); -
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 movecompile errors! S3 owns acopy 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.
- Data that can be copied with a block transfer, e.g., using memcpy.
- let x = String::from("will be dropped"); drop(x);
-
If a type is blittable1, e.g., primitives, arrays, ... it is copyable:
-
All
resource management is scope based . When an item, holding resources, goes out of scope, itsdrop function (similar to a C++ destructor) is called. -
All
heap allocations are referenced with a smart that drops its reference when it goes out of scope, similar to the C++Box<T> pointerstd::unique_ptr<T> . -
Rust has a
module system , useful for refactoring local code, and packages that need not be local. -
It has a relatively complete
std library with collections, threads, tcp socket wrappers, file system library, ... -
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. -
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. -
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 areusually 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. -
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#). -
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.Manymost 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. - 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.
-
Rust does not implement function overloading . -
Rust does not have exceptions - uses panics (controlled shutdown) -
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.
1.1 Comparing to C++
- Scope-based resource management via constructors and destructors
- Smart pointers: std::unique_ptr<T>, std::shared_ptr<T>
- Range-based for loops and begin and end iterators nearly eliminate out-of-bound indexing.
- Encapsulation of state in instances of classes
- move constructors and assignment
- exception handling, STL container at(i) method
- ...
-
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. -
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.
1.2 Features Unique to Rust
-
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.
-
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.
-
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. -
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.
-
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.
1.2 Things to Read First
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 |
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
-
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.cargo build command creates a Target subdirectory, if it doesn't already exist, and runs rustc to create an executable or library inTarget/debug . -
Crate:
The source form of a crate has:
- 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.
-
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 . -
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 thecargo.toml file. - 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.
demo_lib = { path = "../demo_lib"}
demo_lib = { path = "../demo_lib"}
serde = "1.0.104"
Structure Demo
- Open a Developer's command prompt
-
Create a directory temp with
mkdir temp -
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. TheCargo tool is in your path after you install Rust. -
main.rs is a demonstration "Hello World" program. -
Build with the command
cargo build and run withcargo run . -
Clean the crate with
cargo clean .
2.1 Display Library Example:
-
show_type<T>(_value: &T) - Displays type and size of input -
log<T>(value: &T) - Displays type and size of input, followed by its value. -
main_title(msg: String) andsub_title(msg: String - Displays msg followed by an underline. -
putline() - Emits a newline. -
putlinen(n: usize) - Emits n newlines. - ...
display library
3.0 Build Process:
-
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]" }
-
loads and builds each local library identified by:
-
Loads the crate root source file, e.g.,
main.rs (binary) orlib.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.
-
Loads and compiles each local module, identified by the keyword
4.0 Execution:
-
Execution of binaries: If the crate root file is a binary, main.rs , the command "cargo run " will run its main function. -
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 that2 + 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. -
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 thers 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.
5.0 Ownership
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 |
6.0 Rust Types
- 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
Type Declarations:
7.0 Rust Object Model
-
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 andnon-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 keywordA typical struct will be declared public withpub . Some of its methods will also be declared public, but we usually don't make the struct's member data public.
User Defined Type with Traits:
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 |
- 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.
- 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.
- Stucts with all blittable fields can implement the Copy trait. Those with non-blittable fields cannot.
8.0 User-Defined Types:
Example User-Defined Type:
9.0 Generics:
Generics Examples:
10.0 Epilogue:
10.1 Exercises:
- 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.
- 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.
- 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.
- Repeat the previous exercise using tuples instead of structs.
11.0 References:
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 |