As ever, the terse rust notes are revision and cribsheet notes taken from the other sources. None of it is mine, but its a quick reminder.

They are slanted towards an experienced dev getting into Rust.

Last updated Feb 2024.

resources

Google. But lmgtfy

Rust learning is a great set of links. I’ll include some of my own at the bottom of this page.

Why Rust?

Compile time memory safety. Just like type safety it adds to productivity and reduces silly bugs.

And its small and fast.

What is a System language?

I started work in C and then C++. The Rust docs all talk like its a special ‘system’ language. It isn’t really, its just fast and small, does everything you need, and in the world of Lambdas and Docker images will save the planet (or your repo/network) by having tiny images, and using less CPU.

It has no runtime - JVM, CLM, Node etc. It compiles for the browser or the host and runs. It does do memory safety but without a garbage collector, which is a really fine idea.

What about rich library suport?

Crates exist for everything. Rust is old and established. Almost anything you want for a framework has been written. Want green threads - libraries (sorry crates)!

What it isn’t

Well, its not OO - no inheritance. This is a good summary.

On to the syntax

Rust is not a functional language, but it does have map and pattern matching and Loads of familiar stuff from scala, or java or whatever. I’ll say it again, Rust is imperative, so all you functional folks should not try to make it functional.

Example of rust syntax

You have been streaming in Java, or you have mapped in Scala. Could Rust even look a little like this. Well I found this snippet in the Rust docs, and it demonstrates how rich the language is.

fn main() {
    use std::str::FromStr;
    let mut results = vec![];
    let mut errs = vec![];
    let nums: Vec<_> = ["17", "not a number", "99", "-27", "768"]
       .into_iter()
       .map(u8::from_str)
       // Save clones of the raw `Result` values to inspect
       .inspect(|x| results.push(x.clone()))
       // Challenge: explain how this captures only the `Err` values
       .inspect(|x| errs.extend(x.clone().err()))
       .flatten()
       .collect();
    assert_eq!(errs.len(), 3);
    assert_eq!(nums, [17, 99]);
    println!("results {results:?}");
    println!("errs {errs:?}");
    println!("nums {nums:?}");
}

Resulting in output of

results [Ok(17), Err(ParseIntError { kind: InvalidDigit }), Ok(99), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: PosOverflow })]
errs [ParseIntError { kind: InvalidDigit }, ParseIntError { kind: InvalidDigit }, ParseIntError { kind: PosOverflow }]      
nums [17, 99]

Cargo quick Notes

rustup update             // update to latest rust
rustup self uninstall     // to remove it all
rustup component add rustfmt // to allow cargo fmt
rustc --version
cargo new my_proj         // to build an application
cargo new my_lib --lib    // to build a library
cd my_proj
cargo fmt                 // format the code
cargo check               // faster than build
cargo build               // slower debug binaries
cargo build --release     // faster optimised binaries
cargo test                // runs tests, AND runs documentation tests - see below.
cargo test -p crlib1      // run tests in one crate within a workspace.
cargo run
cargo run -p cr1          // For workspace projects, run the binary in crate cr1
cargo doc                 // runs the rustdoc tool producting html at target/doc
cargo doc --open          // as above and opens a browser
cargo publish             // to publish to crates.io, which you can google on your own.
cargo yank --vers 1.0.1   // deprecates 1.0.1 for new projects, not for existing ones.
cargo install             // downloads binaries from crate.io into $HOME/.cargo/bin
// If you are into wasm, then:
cargo generate --git https://github.com/rustwasm/wasm-pack-template

Cargo has two default profiles, one for dev and one for release. You can configure them in the Cargo.toml file.

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

Dependency versions

Cargo.toml lists dependencies with versions and you maintain it. Cargo.lock is auto maintained with detailed information, don’t edit it.

Documenting your crate for crates.io

You can add a description and license in cargo.toml.

[package]
name = "some_opensource"
version = "0.1.0"
edition = "2021"
description = "I'm wasting my 20's by writing open source."
license = "MIT OR Apache-2.0"

Documenting how to use your APIs

Rust has double backslash for normal comments, and triple backslash for documentation comments supporting markdown format. Place them just ahead of the function or struct you are documenting.

cargo doc will run rustdoc to generate html in target/doc

Common sections are below, but you don’t have to provide all of them:

/// #Panics
/// Situations it could panic
///
/// #Errors
/// If the function returns a Result and when it could err
///
/// #Safety
/// if the function is unsafe
///
/// # Arguments
///
/// * `name` - A string slice that holds the name of the person
///
/// #Examples
/// Examples of how to use it, AND cargo test will run the code too!
/// ```
/// assert_eq(6, call_fn(5));
/// ```

Documenting the crate or module

Rather than documenting the thing that follows /// we can document the containing thing with //!

Project structure background

A package is one or more crates with a Cargo.toml file. A crate can be a binary you run with a main.rs, or a library with lib.rs.

Under src is main.rs, which should be tiny and call functions in src\lib.rs. Libs can have tests, main cannot. In fact lib.rs should really just define all the modules you will use, and that will pull in the rust code for that module. (A module is just a source code file)

eg lib.rs

mod todo_stuff;

Then have a file called todo_stuff.rs containing that code.

For modules, you have a choice, src/my_module1.rs works, or if the older form of src/mymodule1/mod.rs (people went off this as you end up with loads of files called mod.rs). A mdule can be used by another module or crate using the ‘use’ keyword. Modules can be nested, so you could use my_mod1::sub_mod2; which could have a structure of src/my_mod1/sub_mod2.rs (or the older format of src/my_mod1/sub_mod2/mod.rs).

Modules are private by default, so ‘pub mod’ rather than ‘mod’.

Use paths either are absolute and start ‘crate::’ or can be relative to me ‘self::’ or my parent ‘super::’, or an identifier in this module - for instance having two modules in the same file, you could just use the module name.

mod keyword

Using the ‘mod’ keyword once in any file tells rust to pull the file into the project. By doing this once, no where else has to do it, and they can pull in pub functions and structs using ‘use’.

Showing the rust project structure

From a java or scala point of view its wierd, the file name implies the module, and the act of saying mod xxx; somewhere tells rust to add file xxx.rs to the build.

The cargo.toml says this is the modules1 crate.

[package]
name = "modules1"
version = "0.1.0"
edition = "2021"

src/main.rs

// To pull in the crate modules1 (ie this crate), from the lib.rs
// ie refering to the pub lib.rs function
use modules1::nesting;

fn main() {
    nesting(); // no need to have modules1:: on the font because of the above use
}

src/lib.rs
Note: anything in here can be brought in scope using the crate name in cargo.toml. see main.rs above, ie use modeules1::nesting.

mod mod_eg1;  // This means there will be a file called mod_eg1.rs

pub fn nesting() {
    mod_eg1::bar();
}

src/mod_eg1.rs

// Because this file is called mod_eg1.rs it is automatically the implementation
// of mod_eg1 and in lib.rs we said mod mod_eg1; which meant the crate knew about it.
mod sub_eg2; // this will be nested, ie mod_eg1::sub_eg2 see next file below.

pub fn bar() {
    mod_eg2::nested::foo();
}

mod mod_eg2 {
    pub mod nested {
        use crate::mod_eg1::sub_eg2;

        pub fn foo() {
            sub_eg2::do_stuff();
        }
    }
}

src/mod_eg1/sub_eg2.rs

pub fn do_stuff() {
    println!("Hello, world!");
}

Which means this contrived example has the structure of

src\
  lib.rs
  main.rs
  mod_eg1.rs
src\mod_eg1\
    sub_eg1.rs  

pub use to re-export at a higher level

If you have some deeply nested stuff but for users of your crate it should be an obvious part of the API, then republish using pub use. Cargo doc will also respect this when it generates the documentation.

Integration Tests

Unit tests are in the same file as code. Integration tests can be placed in a test directory at the same level as src.

Workspaces

Multi project crates are supported by workspace - eg one binary and two libraries.

mkdir my_multi_proj
cd my_multi-proj

Cargo.toml

[workspace]

members = [
    "cr1",
    "crlib1",
    "crlib2",
]

Then create the crates

cargo new cr1
cargo new crlib1 --lib
cargo new crlib2 --lib

In the top level, you can run the binary crate as below, this is good as it has a single target dir which is shared by all projects in the workspace.

cargo run -p cr1

Memory

There is no gc. So, you create objects of fixed size on the stack, when they go out of scope they are dropped. When you call another function you can either move the object into it, which means when the function ends it is dropped, or the function can borrow it, which means you keep ownership.

Additionally, structs can be mutable or immutable, you are allowed multiple immutable borrows, or a single mutable borrow.

Why? Well, there is no GC, so this is the mechanism for memory safety in rust.

Compile time memory safety. Just like type safety it adds to productivity and reduces silly bugs.

Just to bring this to ‘top of mind’ early on. If a struct implements copy, then foo=bar will take a copy of the struct. If it doesn’t then foo=bar will move bar into foo, and you cannot use bar anymore. If something implements clone then foo=bar.clone() will create a clone and you can use both. When you are starting and the borrow checker is causing pain, just clone everything.

References are always valid

The rust compiler will find and error on almost all memory issues. Unlike a pointer in c++ etc the Rust reference will always be valid.

The opposite of reference is de-reference - eg *s1. Referencing is dealt with in more detail later.

let s1 = &s2; // s1 is an immutable reference to s2.

The rust compiler cannot help with race, deadlock or business logic errors.

Multiple mutable borrows in brief

I want multiple mutable borrows because I am multithreaded! OK, well use ARC (Atomic Reference Count) and Mutex. ie its all build in, and if you are familiar with Java atomic, and also the idea of mutex then its all pretty much what you would expect. The incantation for this is in the worked threadpool example from the book:

How does ARC work, well, guess what, it counts references, and as they go out of scope it decrements them down to 0 and then drops it.

let (sender, receiver) = mpsc::channel();  // multi producer, single consumer

let receiver = Arc::new(Mutex::new(receiver));

let mut workers = Vec::with_capacity(size);

But, crates are out there for whatever you want, crossbeam was mentioned for all your concurrency needs.

Drop function

Every structure can implement drop to deal with the structure going out of scope. You don’t have to, but you can. The is how smart pointers work - ie allowing you to share Struct data in multiple parts of your code because the Struct can implement reference counters and implement them in Drop etc.

When you read the learning rust witrh too many linked lists blog, you will see they implement an efficient drop for a linked list, its a good example.

Stack and heap in brief

Stack is faster and needs things placed on it to be fixed size.

Heap has to find space to hold the new item, and lookups are via ‘smart’ ptrs, so its slower. Heap holds variable length items, and you can use Box in Rust to ensure things are heap allocated. So, if you are doing a linked list where there is a recursive type of structure (firstNode->nextNode->nextNode->Nil) then stick it on Heap, and just keep a reference to it on stack.

In all cases, when the item goes out of scope in the current owner then the memory is reclaimed (well, Drop is invoked and then your smart pointer does its reference counting cleverness).

Pointers and returning structs.

println!("The address in main {:p}", &foo); // See :p to print the address.

https://doc.rust-lang.org/std/fmt/trait.Pointer.html

https://stackoverflow.com/questions/63905350/how-can-the-memory-address-of-a-struct-be-the-same-inside-a-function-and-after-t

https://stackoverflow.com/questions/38302270/why-does-the-address-of-an-object-change-across-methods

So they are saying, depending on what the compiler does, it may allocate the struct in the calling stack frame when you return an owned object, even though all the docs say its a copy. Also if you do a println! of the reference that could change the compiler behaviour.

Anyway returning an owned struct is fine, because it probably won’t use copy, even without a Box or Rc etc. (see smart pointers).

What?

Rust tracks the owner of a struct. So if you have a function which creates a struct then it is the owner, and it will go out of scope when the function ends. So returning a reference to it seems sensible to stop it doing a copy, but actually you cannot return a reference because that is a borrow, you have to return the owned object, otherwise when the function which owns the struct ends, the struct goes out of scope, so a reference is not allowed. But returning an owned struct in the book implies it will use a copy - which is fine if all your fields implement copy trait, which they do because all primitives do. But in reality there is compiler optimisation (which seems to always be true, but may not be) where it will actually allocate it in the calling stack frame anyway.

Why do I care? Well I have a function which sets up loads of data in a struct, and want to return it - without doing a big copy operation. The data is all known size, so I don’t need a smart pointer.

String first notes

For instance, the string can be variable length and is stored on heap. But its more complex, because when you think you have a string, you actually have a ptr, length and capacity to the place on the heap where the string is.

Not only that, Rust devs might create a string, but then they pass about &str references - the str type in Rust is a slice. Almost all APIs will work with Slices rather than Strings. An APi that can take a &str will also be able to be called with &String.

Basically if your struct(or String in this case) implements the correct trait then you can have a function sig which accepts a reference, and call it with Structs which on the face of it should not be accepted. The compiler invokes the deref to coerce the Struct into the correct reference. Which is how &String gets converted to &str. The compiler can even chain deref calls so you can accept references in your function but call it with other Structs which can be deref coerced into the correct type.

Immutable

Most languages now accept that some functional programming ideas like immutability are just brillaint. So Rust does too. Since you are a scala or java dev I won’t say more.

Actually, I will. If you have data that has to be mutable, keep it on its own, do not mix it with fields which can be immutable. ie rather than having one struct with both immutable and mutable data, have two. It will ease your borrow pain.

Lifetimes in brief … tic a…

<'a>  // pronounced tick a, this is the lifetime annotation.

So, if you have a structure and you do a lend to a function, and that returns another structure containing elements refering to the initial structure etc… How does the Rust compiler at compile time enforce the ownership of memory, and not drop something too soon.

Well, when the compiler cannot infer it, the programmer must tell the compiler what the lifetime of the items it returns are. Often the lifetime of the returned items will be related to the lifetime of the input items. So as in Scala and Java generics when you add [T] (scala) or <T> (java) to the function signature, you add a lifetime to the function sig, so you can say the lifetime of the returned thing is the same as one of the input things. ie it cannot be dropped before the thing you based it upon, and visa versa.

// How do you say the line below out loud?
// function first_word tic a accepts a param of s of type borrow tic a str)
// and returns borrow tic a str
fn first_word<'a>(s: &'a str) -> &'a str {
}

Lifetime Elision

We all love syntactic sugar to reduce boiler plate code. Well in Rust, quite often lifetime can be inferred, so rather than have the programmer type it all, you can leave it out.

The compiler will error if it needs your programming help. There are 3 elision rules it tries to use:

Rule 1: assign a lifetime to each input parameter.
Rule 2: if there is only 1 input lifetime, then all outputs get that Lifetime
Rule 3: if >1 input, and one of them is &self or &mut self, then all outputs get this structs lifetime.

// Without elision this would need to be
// fn first_word<'a>(s: &'a str) -> &'a str {
fn first_word(s: &str) -> &str {
}

static Lifetime

// variable s is bound to a reference to a slice of string with static lifetime
let s: &'static str = "I never get dropped";

The static lifetime is the lifetime of the program.

Lifetimes again

“lifetimes ensure that references are valid as long as we need them to be… is the scope for which that reference is valid…aim of lifetimes is to prevent dangling references”

Where to put the lifetime? In the same place you would put a generic type. So

// Function
fn foo<'a>(foo:String)->String { ...

// a reference to a lifetime
foo:&'a i32
bar:&'a mut i32

// Function with Params
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {...

// A struct that holds a reference, so its definition needs a lifetime annotation
struct ImportantExcerpt<'a> {
    part: &'a str,
}

//Lifetime names for struct fields always need to be declared after the impl keyword and
//then used after the struct’s name, because those lifetimes are part of the struct’s type.
// ie in the line below, ImportantExcerpt<'a> is the structs type.
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

// Lifetime and a generic Type
fn longest_with_an_announcement<'a, T>(....

PascalCase and snake_case - no camelCase

// snake is all lower with underscores, 
// Used for modules (file names too), functions, methods, macros and local variables
pub fn a_snake_case_function() {
  let my_name = "Jonathan".to_owned();
}
// Each word starts with a capital, also called UpperCamelCase
// Used for Types, Traits, EnumVarients, Structs
pub struct PascalCaseStruct {

}
// Consts are all caps
const MAX_VALUE:u32 = 100;

Const

A const is constant, cannot ever be mut (mutable), the type must be included, can be declared in any scope, including global scope.

const SERVICE_DELAY:u32=3*60; // calculated at compile time.

Constants are inlines when used - so references to them may not refer to the same memory address. They can implement drop which is called when they go out of scope.

Static

A rust static is similar to a const but refers to a static memory location. They have the static lifecycle and drop is never called on them. A static must be of a type which has the Sync trait bound to allow thread safe access.

Shadowing

It is entirely legal to use the name variable name again, and it will replace the previous one in the same scope.

No nulls

You can declare a variable with no value in some outer scope, but the compiler will not let you use it unless it is assigned a value.

Understore infront of variable names

Adding _ to the front means the compiler will not warn if the variable is unused.

let _ = some_fn(); // _ is a pattern, so no stack space allocated, ie call drop on it right away.
let _foo = some_fn(); // not using, but drop called when _foo out of scope.

// Maybe you change an implementation but want to keep the API the same.
fn some_api(f: i32, _old_api: i32) {

}

Using annotation to remove unused warnings

As well as _ you could just disable warnings about unused using one of the many Rust annotations.

#![allow(unused_variables)]

#[allow(dead_code)]
fn foo_bar() {

}

Rust multiple variable declaration on one line

let (mut j,mut k,mut l,mut maxgr):(u64, u64,u64,u64); // Note not intialising here.

Statements and Expressions

A statement doesn’t return a value, an expression does. A function can end with an expression - simply miss off ; and that will be the return value.

fn main() {
  let foo = {
    let c = true;
    let bar = if c {3*60} else {4*60};
    bar/30  // No ; so this is the return value.
  };
}

loops

loop forever

loop means while(true), and you can skip the rest of the block with a continue.

loop {
    let guess: i32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };
}

Not shown below but you can have labels on loops, and break to a certain nesting of labels.

let mut cnt =3;
let v = loop { // loop forever
    println!("cnt={}", cnt);
    if cnt<1 {break cnt}; // break and return a value
    cnt=cnt-1;
};
println!("v={}", v);

while loop

while has a condition, and repeats the block while true. There is also while let

while let

This destructures and needs no handling of the missing partial match.

let mut o = Some(0);
while let Some(i) = o { // destructure o into i if its Some, and then execute the expression block.
    if (i>9) {
      println!("I count, do you?");
      o = None;  // since None will not match the while let it will exit.
    } else {
      o = Some(i+1);
    }
  }

for loop

The most common loop is a for loop, examples:

let a = [1,2,3];
for el in a {
  println!("el={}", el);
}
for rnge in (1..4).rev() { // a range from 1 to 3, reversed
    println!("rnge={}", rnge);
}

Match is like a switch or case

// the file gender.rs is in the same directory, and contains an enum Gender
use super::gender::Gender;                   
// Rather than using Gender::Female I choose to just use Female below.
use super::gender::Gender::{Female, Male};
use std::fmt;

// String doesn't do copy, so I cannot derive copy because String does not
#[derive(Debug, Clone)]
pub struct Person {
    // I am choosing to fully own the string, rather than a slice Which
    // would then need a lifetime <'a>
    name: String,
    age: u32,
    gender: Gender,
}

// I want to print the Person with my own format
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // you can use expressions to assign variable values.
        // And you can see nested match expressions with guard clauses
        let desc :&str =  match self.age {
            n if n<=12 => match self.gender  {
                Male => "boy",
                Female => "girl",
            },
            12..=17 => match self.gender  { // range inclusive, ie including 17
                Male => "teenage boy",
                Female => "teenage girl",
            },
            _ => match self.gender  {
                Male => "man",
                Female => "woman",
            },
        };
        write!(f, "{} {} {}", self.name, desc, self.age)
    }
}

impl Person {
    // Note Self refers to my type, so I could instead do ->Person
    pub fn new(name: String, gender: Gender, age: u32) -> Self {
        // Field init shorthand - the param names match the field names.  Sugar.
        Person{name, age, gender}
    }
}

Struct

Structs hold data, or references to data, and you then implement methods on them with a self parameter.

pub struct Dog {
    pub name: String,
}

impl Dog {
    fn speak(&self) -> String {
        String::from("Woof")
    }
}

You will see below you can impl a trait function for a struct like this:

// Implement the trait Speaker for the Struct Dog, and then a block holding
// the functions.
impl Speaker for Dog {
  ...

Self

In rust the capital S in Self is different from self. Self means the type of self. So, if you have a constructor you could retrun Self {fld1:’A’} rather than MyType{fld1:’A’}

Tuple struct

Do not name fields, more later, but for now:

struct Color(i32, i32, i32);

Traits

You can define function signatures. You cannot implement external traits on external types - part of the coherence and Orphan rules.

Traits do not have fields, but you could provide a getter method and then have implementations provide the data.

&self means the function belongs to a trait or struct impl. Trait functions can provide default implementations, which can be overridden.

pub trait Speaker {
    fn exclaim(&self) -> String {
        String::from("!")   // default implementation
    }
    fn speak(&self) -> String;
  }

  pub struct Dog {
    pub name:String,
  }

  // I will use the default function for exclaim
  // and I will 'implement the trait on this type' for speak.
  impl Speaker for Dog {
    fn speak(&self)->String {
      // If you miss off the ; and its the last line then its an expression
      // - ie return statement
      String::from("Woof")
    }
  }

Traits as Parameters

Since we have traits, we want to have functions which accept them…

pub fn conversation(sp1: &impl Speaker, sp2: &impl Speaker) {
  println!("Speaker 1 says {}", sp1.speak());
  println!("Speaker 2 exclaims {}", sp2.exclaim());
}

Traits bound on generic type Parameters

You have a struct with generic types which should be of a certain trait. eg from the book.

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Trait objects and dyn

If you want to take something that implements a trait, then you don’t know its size, which means it won’t be on stack, it will be on heap. ‘Trait objects’ is the name given to the paramater which is one of the structs which implements a trait.

Copied from rust reference

trait Printable {
    fn stringify(&self) -> String;
}

impl Printable for i32 {
    fn stringify(&self) -> String { self.to_string() }
}

fn print(a: Box<dyn Printable>) {
    println!("{}", a.stringify());
}

fn main() {
    print(Box::new(10) as Box<dyn Printable>);
}

A trait object is prefixed with dyn. All references to it are via pointers (because it is a DST - dynamically sized type), and calls to its methods go via a vtable (virtual table). It allows late binding - the actual type is not known, so we don’t know its size, so it has to be on heap - ie Box, and dyn.

A Trait object must obey some rules such as all traits other than the first one must be auto traits.

You add a lifetime useing +, for instance:

trait Foo { }
type T2 = Box<dyn Foo + 'static>;

actually if the trait has no lifetime bounds, then the lifetime is inferred in expressions and is ‘static outside of expressions. which means these two below are the same:

impl dyn Foo {}
impl dyn Foo + 'static {}

Idiomatic traits

If you have a generic type which must impl two different traits you could write:

pub fn do_stuff<T : Animal + LandAnimal>(item:T) {
  // ...  
}

But, because this can end up looking unreadable, it is prefered to use:

pub fn do_stuff<T>(item:T)
  where:  T : Animal + LandAnimal
{
  // ...  
}

Returning traits

pub fn do_stuff<T>(item:T) -> impl Animal
  where:  T : Animal + LandAnimal
{
  item  
}

Not implement a trait

Sometimes, you will see a declaration such as Unpin!. The ! means “does not implement” the trait Unpin.

Attributes

These are meta data applied to module, crate, function etc.

Dead code attribute

The linter will warn about unused functions, you can disable this using

#[allow(dead_code)]

Derive

The derive attribute allows new items to be automatically generated for data structures.

// We can derive a `Copy` implementation. `Clone` is also required, as it's
// a supertrait of `Copy`.
#[derive(Debug, Copy, Clone)]
struct Foo;

Debug annotation

When using std::fmt such as in println!, you can use {:?} which means you will provide param(s) which can be debug output, although you won’t control how it looks. If a struct is Debug, then all its fields should be Debug too.

#[derive(Debug)]
pub struct something {}

Controlling the println display

If you implement display then you can control how it looks

use std::fmt;

#[derive(Debug)]
struct Point2D {
    x: f64,
    y: f64,
}
// ie implement a trait for my struct.
impl fmt::Display for Point2D {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "x: {}, y: {}", self.x, self.y)
    }
}

pub fn example_output() {
    let pt = Point2D{x:0.0, y:14.0};

    println!("Display: {}", pt); // Display: x: 0, y: 14
    println!("Debug: {:?}", pt); // Debug: Point2D { x: 0.0, y: 14.0 }
    println!("Pretty print Debug: {:#?}", pt);
    //Pretty print Debug: Point2D {
    //  x: 0.0,
    //  y: 14.0,
    //}
}

Clone

Primitives are copied - ie a u32 is actually smaller than a pointer, so it makes sense. But, if you have issues with ownership then clone can fix things. For instance, putting keys and values into maps. But, its costly, so smart pointers are more likely your friend. A smart pointer is a Struct which does things like increment reference count usages on every clone, and decrement on every drop, down to 0 when it frees the memory.

String implements clone (ie a clone of the heap space and ptrs), but does not implement copy, which is why the 2nd line is actually a move.

let s1 = String::from("Hi");
let s2 = s1;  // At this point s1 cannot be used any more!  it has moved to s2
let s3 = s2.clone(); // s3 and s2 are both valid here.

Copy

Traits which do not implement drop can implement copy. Why? Drop implies some complexity being implemented for reference counters or something, a copy would possibly lead to shared responsibility for memory management, or the double free bug.

If you implement copy then it is called on assignment.

// Normally this is a move, but if you implement copy then it will create a copy.
// ie with copy attribute or implementation then you can still use y after this line.
// Without it then it is a move and y is no longer usable.
let x = y;

Clone is a super trait of copy.

Copy will be called implicitly, while clone you have to call Explicitly

let g = foo.clone();

Copy can be an attribute if all a structs fields are also copy.

Types

()  // Unit, ie void or unit if you are java or scala.
let s1 = String::from("Hello");  // s1 is ptr, len, cap to heap holding Hello.

Function pointers

You can take parameters which are function pointers, or define types. Also read about closures - which I also cover later.

/// Take a 'callback', ie a function I can then call with no params which returns an i32
fn i_take_a_parameter(func: fn() -> i32) {

}

Common macros

A marco ends with a !, and is expanded into code.

println!{"Hi {}, its {} to see you", name, sentiment}; // implement Display to format
println!("Some Debug struct {:?}", s);
println!("Pretty print some Debug struct {:#?}", s);
eprintln!("Print to stderr: {}", err);
concat!("Hi","Dude"); // concatinate 2 strings
panic!("Exit due to missing file");
vec!(1,2,3); // Used rather than Vec::new(); and then push.
format!("Hello {}", name); // same as print! but returns a string
format!("{:?}", (3,4)); // debug format so don't have a place holder for each items
unimplemented!(); // panics if a function with this in is called.
write! // and so on
dbg!(x+1) // outputs and returns any expression.

dbg! is very handy.

Main can return Error

In rust the main() normally returns unit - ie (). But it can return Error which means you can just pass errors back.

use std::error::Error;
use std::fs::File;

// Exits with 0 if Ok or non 0 if Err
// Box<dyn Error> type is a trait object
fn main() -> Result<(), Box<dyn Error>> {
    // the ? means return the error or continue with the Ok value.
    let f = File::open("hello.txt")?;

    Ok(())
}

Casting numerics

The std lib contains operations for bit shifting, and for extracting as_bytes with endian params and so on. So you can do alot at a low level.

Or you can say ‘as’.

// Taken from https://stackoverflow.com/questions/26593387/how-can-i-get-the-current-time-in-milliseconds
// Probably better to simply use the Chrono crate
pub fn ms_since_epoch() -> u128 {
    let start = SystemTime::now();
    let since_the_epoch = start
        .duration_since(UNIX_EPOCH)
        .expect("Time went backwards");
    since_the_epoch.as_millis()
}
// --- snip ---
let now = jpg_timeutil::ms_since_epoch();
let smallNow = now as u64;  // ie casts it and discards the more significant bits.

usize

Just a note, as well as u32 (unsigned 32 bit) and so on, there is usize for ptr size. usize is used to index into Vector. Just mentioning it.

String

A string is stored on Heap, but the string is actually a ptr to the heap, a len and a capacity.

A string slice will just have a ptr and a len, no capacity, as it pts into the heap part of the string.

nm: String::from("Jonathan"), // eg in a struct
...
nm.as_str(); // convert back to a slice
let myNm = "Jonathan".to_owned(); // alternative version.
...
let n:u32 = "42".parse().expect("Not a number");

let s = String.from("Hi");
s.clear();
let mut s = String.from("Hello World");
let hello = &s[0..5]; // a string slice.
let s2 = String.from("!!");
let s3 = [s,s2].join("\n"); // Concatinate strings with a delimiter. See SliceContactExt trait.
let s4 = concat!(s,s2); // Concatination
s.push_str(s2); // reuses s, but changes it.  You can also use + to concatinate two strings and get a new one
let formatted = format!("{}{}", s, s4); // string interpolation in rust
s.push('.'); // to append a char onto an owned mutable string

A string literal embedded in your code is part of the binary. The type of s below is &str. ie a string slice.

let s = "Hello world";

String slice

let s = String.from("Hello World");
let hello = &s[0..5]; // a string slice.
let hello = &s[..5]; // a string slice. starting at 0 doesnt need to say 0
let world = &s[6..]; // start at 6 until the end
let helloWorld = &s[..]; // the entire string

Text Blocks - multiline strings

The \ means do not put a newline character.

let contents = "\
Rust:
safe, fast, productive.
Pick three.";

Raw string literals

Prefix the string with r and no need to escape chars. Add ## and it will need to terminate with “##. Note you could add any number of # after the r and terminate it with “ then the same number. eg r###”stuff “## still in string “###

Into and From

Part of the std::convert is being able to convert from one type to another using into.

For instance this snippet of web assembly:

extern crate web_sys;

web_sys::console::log_1(&"Hello, world!".into());

Into, as and to

Naming docs have a guide

And stolen from an answer on stack overflow:

  • if it consumes the data, use into_*, which is variable in cost, eg String::into_bytes

  • if it returns another “view” of the data, use as_*, which is free, eg String::as_bytes

  • otherwise use to_*, which is expensive, eg String::to_lowercase

Array

Every item has the same type, fixed length, allocated on stack and not heap.

You can get panic out of bounds if you try to access beyond the array size. Use arr.get(i) and you will get an option back.

let a:[i32;5] = [1,2,3,4,5]; // [] array, i32 type, 5 length
let a = [1,2,3,4,5]; // in short form
let a = [1;4];  // an array of 4 ones.
let first = a[0]; // accessing element 0
let mut b:[i32; 5] = [0; 5];
b[0] = 5; // modifying an element
for e in b {
    println!("{e}");
}
for e in v.iter().enumerate() {
    let (index, x) = e;
    println!("Index{index} = {x}");
}

The array also supports filter, sort, sort_by. You could use get() as below, to avoid panic on out of bounds.

let a = [1,2,3,4,5];
let b = a.get(0).unwrap();
let c = a.get(0..2).unwrap();
if let Some(d) = a.get(4) {
    println!("{d}");
}

Vector

You can create it using macros, and index it using a usize - which is a type used for memory pointers. So if you want to get it from a u32 you have to case as usize.

let v = vec![1,2,3];
let idx:u32 = 2;
let l = v.len(); // the length
let c = v[idx as usize]; // otherwise "the type ... cannot be indexed by `u32`"
// filter, map and so on all all supported, call collect to actually execute it
let double_evens:Vec<i32> = v.iter()
                             .filter(|num| *num%2==0)
                             .map(|even| even*2)
                             .collect();

Iter

There are three methods to go from a collection (or Option) to create iterators:

iter() // which iterates over &T
iter_mut() // which iterates over &mut T.
into_iter() // which iterates over T

Once you have an iterator you can use filter, map, collect and so on.

Vector and iterator

Imagine you have a loop to build up new values in a Vec and you then want to add those items to a Vec with a bigger scope. The borrow checker will say NO WAY YOU STUPID HUMAN. Or words to that effect.

Option

Yes Rust has the option, Some or None, and you can match them.

use std::Option;

pub fn match_eg1(maybe_digit : Option<i32>) {

    let message = match maybe_digit {
        Some(x) if x > 10 => process_digit(x),  // Notice the pattern guard - the if
        Some(x @ 1) | Some(x @ 2) => process_onetwo(x), // Alias to x and or expression
        Some(z @ 3) | Some(z) if z%2==0 => process_wierd(z), // guard could execute many times.
        Some(x) => process_other(x),
        None => panic!(),
    };
}

It also has a bit of sugar in the form of ? which can return the None condition up the stack with a single character and no match.

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

There are loads of functions on an option, such as:

expect, unwrap, unwrap_or, unwrap_or_default, unwrap_or_else
ok_or, ok_or_else, transpose
filter, flatten, map, map_or, map_or_else
zip, zip_with
and, or, xor
and_then, or_else
into_iter, iter, iter_mut
...etc

Enum

enum Suit {
    Clubs,
    Diamonds,
    Hearts,
    Spades,
}

enum Card {
    NumCard(u32, Suit),
    Jack(Suit),
    Queen(Suit),
    King(Suit),
    Ace(Suit),
}
impl Card {
    pub fn score(&self) -> u32 {
        match self {
            Card::NumCard(v, _s) => *v, // match with bind variables
            Card::Jack(_) => 12,
            Card::Queen(_) => 13,
            Card::King(_) => 14,
            _ => 0, // catch all, can have any name, or _ if we dont use the variable
        }
    }
}
// --- snip ---
let c = Card::Jack(Suit::Clubs);
println!("Card score = {}", c.score());

If you want the enum as a u32 for instance, then you can simply use as u32 and it will give the position. You can actually specify the discriminant - if you don’t for the first element then it defaults to 0.

enum Positions {
  Zeroth, // no value so starts at 0
  First,
  Second,
  Fourth = 4,
}
// and later
let i = Positions::First as u32;

The values of an enum can have different types, and the enum will be the size of the biggest - eg u32 or u64 or whatever. It will also be byte aligned to the longest. If you want to dig a bit then look at repr.

Tuple type

A tuple has members of different type, and the values are accessed from .0, .1 etc, rather than using field names.

Tuple structs

have no field names, but are named types. So you can differentiate on function signatures and so on.

struct Color(i32, i32, i32);

Vec

The most commonly used collection in Rust.

let v1 = vec!(1,2,3);
let v2 = Vec::from(1,2,3);

let knownElem = v2[0]; // will panic if out of bounds
let optionalElem = v2[5]; // ie there is no 5th element

The never type

Which is !, can only be a return type and can be coerced into any type. Its not stable yet, so I’m saying no more.

Closures

Below you see the function taking a single param to another function. If stuff used v then the closure would be allowed to make use of v, it infers that the closure should borrow them.

let v = 1;
fn(|param| {/*stuff*/})

If our closure captured variables (ie used them inside the closure) then you can use move to tell the compiler to move the ownership to the closure rather than inferring that it should borrow them.

let v = 1;
fn(move |param| {/*stuff*/})

Any closure in Rust is either a Fn or FnOnce or FnMut, which is better explained in many places, such as the rust book.

Function pointers and closures

Its also important to know that the fn type is a function pointer, ie a normal function, rather than a closure. Function pointers implement all three of the closure traits, again read the book about functions and closures.

Function pointer types

You can have a type which is a function pointer, this snippet is copied from the rust docs.

fn add(x: i32, y: i32) -> i32 {
    x + y
}

let mut x = add(5,7);

type Binop = fn(i32, i32) -> i32;
let bo: Binop = add;
x = bo(5,7);

Error Handling

// unwrap will return the Ok value or panic! with the error msg
let f = File::open("hello.txt").unwrap();

// || is a closure, which can capture variables - each closure is actually its
// own type because of this, which leads to Fn, FnOnce an FnMut...read up on it.
// unwrap_or_else will take the contents of Err and pass to the closure.
let config = Config::new(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

// expect will return the Ok or Panic! with a message
let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

Returning Ok or Err as a Result

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

And we love sugar, (syntactic sugar = remove boilerplate with incantations), so, to propogate errors just use ?

Also note you can chain them together as below:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

and more sugar

// if let is a sugar version of matching on a Result for the Err condition
if let Err(e) = run(config) {
    println!("Application error: {}", e);

    process::exit(1);
}

Don’t forget stderr, using eprintln!

eprintln!("Problem parsing arguments: {}", err);

Mutliple error types?

If you have a function that returns an io.Error and another which returns a different type of error, how can you have a function signature that works?

use std::io;
use std::io::Write;
use std::error::Error;


// Version below fails with
// error[E0782]: trait objects must include the `dyn` keyword
// pub fn askUser(question:&str) -> Result<String, Error> {
// And the version below fails with
// doesn't have a size known at compile-time
// pub fn askUser(question:&str) -> Result<String, dyn Error> {
//
// So the correct version is below.  Read up on Box and dyn!
pub fn askUser(question:&str) -> Result<String, Box<dyn Error>> {
    print!("{}", question);
    io::stdout().flush()?; // ? means pass any error to the caller

    let mut buffer = String::new();
    io::stdin().read_line(&mut buffer)?; // ? means pass any error to the caller
    Ok(buffer)
}

Multiple error types take 2.

There are commonly used crates to avoid the Box incantation. Such as

[dependencies]
anyhow = "1.0.75"

Stack trace on Panics

If you want to see stackframes from panic you can:

set RUST_BACKTRACE=1

Common libs and crates, and code

Cargo.toml holds the dependencies, so

rand = "0.8.5"

Crates have to be used, so:

use std::io;   // for file io etc
use rand::Rng; // for random numbers
use std::process; // process::exit(1);
use std::error::Error; // For Err and Ok and returning Error

Tests

Main.rs cannot have tests in, so keep it small. Move code into lib.rs and or other modules. Note that crates do not usually contain a main.rs and a lib.rs. If you want a crate with both then it seems common to have a bin directory and place in it there, google it if you must.

These files can have tests in the same file as the code, for example:

//this actually means if the configuration test is being used then include the module.
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

Quick tricks, sugar and syntax in Rust

Struct init with Expressions

let _rectangle = Rectangle {
    // struct instantiation is an expression too
    top_left: Point { x: left_edge, y: top_edge },
    bottom_right: bottom_right,
};

Field init shorthand

If the parameter and struct field name are the same you don’t need to repeat it.

pub struct Fighter {
    name: String,
    injured: bool,
}
impl Fighter {
    // Missing ; on last the line means return the value, and field init is used to avoid boilerplate.
    // Normal field initialsation would be name:name, injured: injured.
    pub fn new(name: String,injured: bool) -> Fighter {
        Fighter{name, injured}
    }
}

Struct update syntax - Copy remaining Fields

let fighter2 = Fighter {
    name: String::from("Jonathan"),
    ..fighter1                     // this has to come last
};

i.e. you can use field init shorthand in a function to set one field value, and copy the remainder using ..

Watch out though, because ..fighter1 uses move, lucky for us bool implements copy and so will be copied. But if there was a String in fighter1 which we used update syntax for then fighter1 would not be usable afterwards.

Comparison done using PartialOrd

ie std::cmp::PartialOrd

Generics

Uses <T> form (like Java) rather than [T] form (like Scala). Each one you use in your code is then created as a monomophism actual implementation. So there is no runtime slowdown. i.e. it basically generate the code for each type you actually use in your program.

All type parameters have an implicit bound type called Sized. If you want to remove this you can use the special ?Sized syntax.

// R is a type parameter which has the Rnd trait but not the Sized trait bound to it.
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Point {

Sized

This is a good read: sizedness-in-rust. Or the source info in the rust book.

Key points. Types are bound using colon.

use std::fmt::Debug;
fn debug<T: Debug> {}  // T is bound to Debug

Every type is auto bound to Sized, but for pointers you don’t want this to avoid compile errors, so you widen, or relax the bound by using ?Sized can be pronounced “optionally sized” or “maybe sized”. This is known as an opt-out bound.

fn dbg<T: Debug + ?Sized>(t: &T) {

Smart pointers

It has data on the heap and manages it someway - thats basically a smart pointer. For example there are reference counters, atomic reference counters (threadsafe), Box - which moves something to the heap and so on.

They are usually structs, and implements Drop and Deref traits. Which means your code can treat them like references, even though they are more complex.

Box lets you store data on the heap, while leaving only a pointer on the stack. For instance a linked list of nodes has to be on heap as the total size of the linked list is unkown.

Box is a smart ptr because it implements the Deref trait, so it can be treated like References. When it goes out of scope the heap is tidied up because of the Drop trait implementation.

enum List {
  Cons(i32, Box<List>), // Indirect via Box to avoid "recursive type has infinite size"
  Nil,
}
let list = Cons(1,Box::new(Cons(2, Box::new(Cons(3, Nil)))));

The Deref operatror *

You write your code as if it gets a reference, and smart pointers implement the Deref trait which means it will convert from the smart pointer through multiple deref’s until it gets the type it wants.

let x = 5;
let y = &x;
let z = Box::new(x);

assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, *z); // Box implements deref, so this works.

How to implement deref I will leave to googling. But rust substitutes * with a call to deref.

Deref coercion calls deref, and deref can return another type. The rust compiler chains the deref calls together until it gets the type it needs.

DerefMut does similar for mutable references.

The Drop trait

Critical to smart pointers, is that when they go out of scope they tidy up. This trait is in the prelude so no use needed. Again, I’ll let you google how to implement it.

But, given 14 years of rust there is probably a crate already there to do what you need, so are you sure you need to implement a library?

You can get drop invoked early, before something is out of scope, by calling std::mem::drop - which is in the prelude, so you call it as

drop(thingToDrop);

Box - compile time immutable or mutable borrows

Single owner, multiple borrows. Compile time checked.

Rc - compile time immutable

Multiple owners - ie you call clone and it just incs a ref counter. Compile time checked.

It is a smart point, rc means reference counter. You can use Rc::clone to very quickly clone large data items on heap so you can share the data in multiple parts of your code without doing big data copies.

RefCell - Interior mutability pattern

Single owner, multiple borrows. Runtime checked.

You are now into runtime Panic rather than compile time checks. It uses unsafe memory operations internally, to allow you share data and mutate it but only on a single thread.

You can start to see that Rust has lots of utility classes out of the box which let you work with data and memory safely, without a runtime.

Cow - or clone on write

This is a smart pointer with deref - so for borrow you just call the type functions. But if you need to then mutate the data to_mut will do that, cloning if needed. It doesn’t do counting, so you could use Rc::make_mut, or Arc::make_mut if you want that.

Borrowing again

There is a great tutorial on implementing linked lists. Here are some highlights.

use std::mem;
...
 next: mem::replace(&mut self.head, Link::Empty),

mem::replace is a steal a value out of a borrow and replace it with another.

ie if you have something like a list where the head ptr goes into a new node, and then self.head becomes the new node, then use this trick. In one replace call we can move the current value and replace it with something else.

Logging

A debate about logging or tracing all over the internet. But the simplest I found is the standard macro log levels supported by

Threads

use std::thread;
use std::time::Duration;
fn main() {
    let handle = thread::spawn(move || {   // Pass in the closure, return the handle
      thread::sleep(Duration::from_millis(1)); // eg of sleep
    });
    handle.join().unwrap(); // wait for the thread to finish.

Note the use of move - if our closure captured variables (ie used them inside the closure) then you can use move to tell the compiler to move the ownership to the closure rather than infering that it should borrow them.

Threads comms via messaging

let (tx, rx) = mpsc::channel(); // mpsc: multi producer, single consimer.

async

Concurrency in rust is safe, but if you are used to Java or Scala or whatever its strange.

The async keyword results in code generation of structs which implement a poll function and a state machine. You then need an async runtime such as async_std or tokio who will schedule and call your async blocks and functions. Rust has no std runtime - so its up to you to choose one which implements concurrency typically on top of a thread pool that it manages. To quote rust docs “Async runtimes are libraries used for executing async applications.”

We are not talking about native threads here, we are talking about a scheduler for async blocks/functions.

Putting it another way, an async function will return a future, which you can only call await on from an async block or function..which you can only call await on from an async block or function, etc. So on startup your main will be annotated with the async_std or tokio main annotation, as will any tests you write. And this annotation will bootstrap the concurrency framework.

Async blocks are never executed unless you await or poll them.

To repeat, Rust needs an async runtime that you choose in order for any of the async stuff to work.

#[async_std::main]
async fn main() {
  some_fn().await;
}

Unpin and Pin

Others have explained it, such as

But here is a hint…

Pinning is for structs which reference themselves (and similar situations), and Pin (Unpin! - not unpin, means doesn’t implement Unpin) will tell Rust that it cannot move data about when it wants to. Some part of the struct is pointing to another struct instance - like a linked list node say. So should Rust need to expand memory or move it about it would be really bad to move this node and leave ptrs in other nodes out of date. So you pin them.

If something is safe to move then it will implement Unpin - so Rust is free to move them about. Everything is already Unpin by default, ie u8 is Unpin.

See pin_project for a crate which makes this safe.

// all guarantee that T won't be moved if T: !Unpin
Pin<&mut T>
Pin<&T>
Pin<Box<T>>

Low level async IO - mio!

This is the only one I have used.

A little on WASM

Actually, this is one of the main reasons for me learning Rust. I have done react hooks with typescript and written a production system in the last 2 years. Its great compared to the rest of those frameworks, but still rubbish compared to how we used to do UIs.

Perhaps Rust will give me a decent language for the full stack.

In brief

You will have a pkg directory holding the output wasm and some javascript and typescript glue code.

You will have a www directory holding the npm project for bundling the app, and to use as a dev webservier. This will reference the pkg dir as a dependency.

Summary of the wasm set up notes

Anyway, lmgtfy: https://rustwasm.github.io/docs/book/game-of-life/setup.html

Section 4.1 on July 24 2022 didn’t include this:

rustup target add wasm32-unknown-unknown

And to confirm, we use wasm-pack to reflect code changes into the UI while npm run start is active in another window.

wasm-pack build

And it sticks the build into the pkg dir.

To create an npm project to bundle up the webapp.

npm init wasm-app www
cd www
npm install

When you update package.json, don’t copy the bit below, as its not valid

// Add this three lines block!

If you write tests, then you can run them is wasm mode using.

// support --chrome --firefox, --safari, and --node
wasm-pack test --chrome --headless

and you can specify the tests using

#[wasm_bindgen_test]
pub fn test_tick() {
  // ....

You can also run normal tests, but add this to the Cargo.toml to avoid linker errors.

[lib]
crate-type ["cdylib", "rlib"]

cdy means C compatible dynamic library which helps cargo pass the correct flags to the Rust compiler when targeting wasm32.

Repr

#[wasm_bindgen]
#[repr(u8)]

Given the different memory models of wasm and javascript, it is worth reading about byte alignment and padding on the repr page.

When to wasm_bindgen

You place this on every impl to expose the structs public methods.

// Placed on the struct itself
#[wasm_bindgen]
pub struct Foo {
}

// Public methods, exported to JavaScript.
// Have a seperate impl block if you want, just for these functions.
#[wasm_bindgen]
impl Foo {

Memory

Javascript can access wasm linear memory. So we pass ptrs back to javascript to avoid javascript having to copy data.

From the tutorial

/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
    // ...

    pub fn width(&self) -> u32 {
        self.width
    }

    pub fn height(&self) -> u32 {
        self.height
    }

    pub fn cells(&self) -> *const Cell {
        self.cells.as_ptr()
    }
}

Look at fn_cells! as_ptr(). This is critical, as is *const. A direct ptr to the memory so javascript can mine it quickly.

Logging and panic

The generated template has the panic hook already. You just need it on a common code path as

utils::set_panic_hook();

For logging, in Cargo.toml

[dependencies.web-sys]
version = "0.3"
features = [
  "console",
]

Logging to console

extern crate web_sys;

web_sys::console::log_1(&"Hello, world!".into());

Or, you could create a util for logging :

extern crate web_sys;

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

and call it using

log!("Stuff {}, {}, {:?}",row,col,cell,live_neighbors);

Also, to use this you need to edit your toml file, and expose the console as below:

Cargo.toml

# Has to after the console panic and wee allocator
web-sys = { version="0.3.59", features=[ "console" ] }

Other wasm Notes

Rust-generated WebAssembly functions cannot return borrowed references. Test may need borrowed references, so have a separate impl block and functions to allow local rust testing.

Read debug notes about preserving function names.

And the rest

This is work in progress, more later.

https://cheats.rs/

https://doc.rust-lang.org/book/

https://doc.rust-lang.org/reference/

Design patterns: https://rust-unofficial.github.io/patterns/intro.html

https://doc.rust-lang.org/nomicon/intro.html

https://doc.rust-lang.org/std/index.html

https://doc.rust-lang.org/cargo/index.html

https://rust-lang-nursery.github.io/rust-cookbook/intro.html

https://crates.io/

cargo generate

https://rustwasm.github.io/docs/book/game-of-life/setup.html

embedded rust, tiny size and so on

Api guidelines

Cookbook examples