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
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]
- 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
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" }
- 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
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!
-------------------
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
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
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
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:
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")
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:
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!
-------------------
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:
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 ..."
9.0 Generics:
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);
}
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, }
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" }
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 |