principles

Understand the difference between aliasing and mutability.

Aliasing refers to having several immutable references to T. Mutability refers to having single mutable reference to T. Rust follows Aliasing XOR Mutability rule. You’re either allowed to have multiple references to an object or have single unique mutable reference.

primitives

coercion: coercion refers to changing types on run time to a related type. for example: converting from i16 to u16. but not from sttring to int.

Question: how rust defines the relation about conversion from one type to another?

  • unwrap() : unwrap Result<T, E> into T.

    fn total_cost(input: String) -> Result<String, Error> {}
     
    let cost = total_cost(input).unwrap();
  • expect() : catches an error and panic with that

    let f = File::open("hello.txt").expect("File not found!");
  • is_ok() : returns bool whether returned val is Result Ok.

  • parse() : parses number to string

    let cost = "8".parse::<i32>();
    // OR
    let cost: i32 = "8".parse();
  • ? : when returning Result<T, E> type from a func, if want to return error as soon as you encounter it, use ? .

    // qty can be "hello", in which case it'll return error due to `?`
    let qty = item_quantity.parse::<i32>()?;
    Ok(qty * 2 * 3)
  • ::from() : convert primitive type to custom type

  • .into() : converts custom to primitive type, reverse of From

  • Every type whether primitive or custom have traits implemented. Each trait have different functions that the type uses. Example: println! utilises Display trait for types like i32 or String.

    • Thus, these types need to implement the following traits in order to use println! like functions.
    • Debug trait implements {:?} functionality. So, whenever you implement a function for a generic type,
  • usize : used as unsigned size type, basically ui32

  • const fn: const functions are evaluated at compile time, and have some limitations like for can’t be used.

    • The interpretation happens in the environment of the compilation target and not the host.

  • Deref trait is used to enforce any smart pointer to have same functionality as a normal pointer.

    • If a type implements Deref, it means it can handle shared references to inner type.
    • If a type implements DerefMut, it means it has access to mutable reference (exclusive) reference to inner type.
    use std::ops::Deref;
     
    struct MyBox<T> (T);
     
    impl<T> Deref for MyBox<T> {
        type Target = T;
     
        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }

Pointers and References

Functions and traits

fn type refers to any function type which also include closures.

fn(type) -> return_type

there is no such thing as &fn(type) -> return_type as fn() is already a pointer type. Each named function has a distinct type since Rust PR #19891 was merged.

there is a difference between function item type and function return type, any named function when referred in an object always return a function item type: fn(arg)->return_type {name} which is different than function pointer: fn(arg)->return_type.

fn temp(a: i64) -> bool {
    a != 0
}
fn another_fn<F: Fn(i64)->bool>(callback: F, b: bool) -> bool {
    if b {return (callback)(b as i64)}
    b
}
fn main() {
    another_fn(|a: i64|->bool{a==0}, false);
    another_fn(temp, true);
}

Use of traits is advised as it compiled to direct function call which is better than function pointer. How?

This means that when you call func_of_func with a function item such as funccallback will be compiled to a direct function call instead of a function pointer, which is easier for the compiler to optimize.

Function pointers have to be dereferenced to be called while a function call is directly called from stack.

When passing function items as args in function calls, it can’t accept closures as closures doesn’t implement FnMut trait.

fn item types vs fn pointer type

  • item type: function types that are assignable at compile time.
  • pointer type: assignable at run time

A big question mark in my mind: how does any language define and knows what is compile time abstractable and what is run-time?

When you refer to a function by its name, the type you get is not a function pointer (e.g. fn(u32) -> bool). Instead, you get an a zero-sized value of the function’s item type (e.g. fn(u32) -> bool {foo}).

A big unlock for understanding why closures closely emulates functions are how they are treated by the language. In rust, closures are objects that can implement Fn* traits which allows them to be callable. Rust desugars closure || a + 1 with a trait definition like this:

struct __SomeName {
        a: i32,
    }
 
    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

Trait objects and Fat pointers

rustc want everything to be done at compile time. It forces you to write things in such a way that less and less things are left to be done dynamically or at runtime.

Rust accomplishes this by performing monomorphization of the code using generics at compile time. Monomorphization is the process of turning generic code into specific code by filling in the concrete types that are used when compiled. In this process, the compiler does the opposite of the steps we used to create the generic function in Listing 10-5: the compiler looks at all the places where generic code is called and generates code for the concrete types the generic code is called with.

That’s why rust’s way of handling generics is quite simple and straightforward. Let’s say you take a function F and supplies it’s arguments with generic parameters <T>. Now, since rust doesn’t know it’s concrete types but it does know what other type implement this trait, and it just copies the function with all the concrete types so that at runtime, it just becomes a function call and no additional overhead is implied due to generics.

Now, you can do other thing as well, and that is, impl trait objects in return type of the function. In usual generic argument function fn f<T: Bar>(…)->T, caller determines the concrete type of the function, but in case of impl Trait fn f(…) -> impl Bar, callee determine what function type to return so the caller only has an object of that trait type and doesn’t know what concrete type it belongs to. But note that, impl trait only works when all return type is of one concrete type. So, in the example show below, function rands(x: bool) -> impl Bar doesn’t work because it returns two different types, Foo and Baz. So, rustc throws an error that first match expression returns a Foo and expects second to be Foo as well.

Below example doesn’t work, because of how trait objects are implemented in Rust. Every trait objects is associated with a vtable. That’s how fat pointers work in rust, they generally have extra data as compared to normal pointers. In case of trait objects, it contains a vptr to trait’s vtable. So, when the function is called, vptr references the vtable with an offset which calls the required function implemented by that concrete type.

trait Bar {fn bar();}
struct Foo {}
struct Baz {}
impl Bar for Foo {
    fn bar() {
        println!("foo");
    }
}
impl Bar for Baz {
    fn bar() {
        println!("baz");
    }
}
fn rands() -> impl Bar {Foo {}}
 
// fn rands(x: bool) -> dyn Bar { // this doesnt work
fn rands(x: bool) -> impl Bar { // this doesn't work either
    match x {
        true => Foo {},
        false => Baz {},
    }
} 
// this works
fn rands(x: bool) -> Box<dyn Bar> {
    match x {
        true => Box::new(Foo {}),
        false => Box::new(Baz {}),
    }
} 
fn main() {
	let x = rands();
	x.bar();
    let a = rands(true);
    a.bar();
}

Adding a &self to trait function makes this work, because now rust has a concrete object whose function will be determined using vtable. Rust creates a vtable containing function pointers to each implementor class of the trait. So, when you call rands(), it returns a concrete type that implements Bar, and then when you call .bar(),

trait_object.vtable.echo(trait_object.obj, "hello")

rust can look up at the vtable to determine which trait objects function call happens. Following diagram explains how rust determines which function to call depending on vtable. Pros of this: Rust only calls what is needed, so even if a type implements ton of traits, when you use a trait object, it only creates a vtable for the types implementing that trait object and not for all other traits implemented by the type.

&dyn Foo (Trait Object)
+--------+----------+
| Data * | Vtable * |
+--------+----------+
    |        |
    |        v
    |     +---------------+--------+----------+-------+
    |     | drop_in_place | size   | method_1 | ...   |
    |     +---------------+--------+----------+-------+
    v
+---------+---------+--------+
| field_1 | field_2 | ...    |
+---------+---------+--------+

memory layout of trait objects

fn rands(x: bool) -> Box<dyn Bar> {
	if x {
		return Box::new(Bar{})
	}
	
	Box::new(Foo {})
}

Lifetimes

Lifetimes are used in association with references to tell compiler how much you need a particular reference and should compiler keep it alive for that lifetime.

Very nice implementation of StrSplit by Jon Gjengset shows nice usage lifetimes, multiple lifetimes and generics.

// This is the struct with that lifetime, it has a remainder and delimiter.
// Note the usage of `'a` here, which is a lifetime used to tell the compiler
// the lifetime of a particular variable.
#[derive(Debug)]
pub struct StrSplit<'a, D> {
    remainder: Option<&'a str>,
    delimiter: D,
}
 
impl<'a, D> StrSplit<'a, D> {
    fn new(string: &'a str, delimiter: D) -> Self {
        StrSplit { remainder: Some(string), delimiter}
    }
}
 
impl<'a, D> Iterator for StrSplit<'a, D>
where
    D: Delimiter {
    type Item = &'a str;
    fn next(&mut self) -> Option<Self::Item> {
        if let Some(ref mut remainder) = self.remainder {
            if let Some((start, end)) = self.delimiter.find_next(remainder) {
                let res = &remainder[..start];
                *remainder = &remainder[end..];
                Some(res)
            } else {
                self.remainder.take()
            }
        } else {
            None
        }
    }
}
 
pub trait Delimiter {
    fn find_next(&self, s: &str) -> Option<(usize, usize)>;
}
 
impl Delimiter for &str {
    fn find_next(&self, s: &str) -> Option<(usize, usize)> {
        s.find(self).map(|start| (start, start + self.len()))
    }
}
 
impl Delimiter for char {
    fn find_next(&self, s: &str) -> Option<(usize, usize)> {
        s.char_indices().find(|(_, c)| c == self).map(|(start, _)| (start, start + self.len_utf8()))
    }
}
 
fn until_char(s: &str, c: char) -> &str {
    StrSplit::new(s, c).next().unwrap()
}
 
#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn it_works_until_char() {
        assert_eq!(until_char("hello world", 'o'), "hell");
    }
 
    #[test]
    fn it_works() {
        let haystack = " a b c d e ";
        let letters: Vec<_> = StrSplit::new(haystack, " ").collect();
        assert_eq!(letters, vec!["", "a", "b", "c", "d", "e", ""]);
    }
}

Generics

// Struct
struct GenVal<T> {
    gen_val: T,
}
 
// impl
impl<T> GenVal<T> {
    fn value(&self) -> &T {
        &self.gen_val
    }
}
 
// A trait generic over `T`.
trait DoubleDrop<T> {
    // Define a method on the caller type which takes an
    // additional single parameter `T` and does nothing with it.
    fn double_drop(self, _: T);
}
// Implement `DoubleDrop<T>` for any generic parameter `T` and
// caller `U`.
impl<T, U> DoubleDrop<T> for U {
    // This method takes ownership of both passed arguments,
    // deallocating both.
    fn double_drop(self, _: T) {}
}

Can define generic trait, then impl can be generic or concrete, and it can be further restricted with other traits.

// generic trait with T restricted in fn
pub trait Surtur<T> {
  fn sort(&self, slice: &mut [T]
  where
    T: Ord + Copy;
}
 
// unit struct
pub struct StdSort;
 
// generic trait impl for struct restricted at impl level
impl<T> Surtur<T> for StdSort
where
  T: Ord {
 
  fn sort(&self, slice: &mut [T]) {}
}
 
// Another unit struct
pub struct GenericSort;
 
// generic trait impl for struct restricted at fn level
impl<T> Surtur<T> for GenericSort {
  fn sort(&self, slice: &mut [T])
  where
    T: Ord + Copy {}
}

Macros

#[macro_export]
macro_rules! avec {
  ( $($x:expr),* ) => {
    {
      let mut _vec = Vec::with_capacity(avec![@COUNT; $($x),*]);
      $(_vec.push($x);)*
      _vec
    }
  };
  ( $($x:expr,)* ) => {
    avec![$($x),*]
  };
  ( $x:expr; $count:expr ) => {
    {
      let mut vs = Vec::new();
      vs.resize($count, $x);
      vs
    }
  };
  (@COUNT; $($x:expr),*) => {
    <[()]>::len(&[$(avec![@SUBST; $x]),*])
  };
  (@SUBST; $_:expr) => {()};
}
 
#[test]
fn it_works() {
  let x: Vec<i32> = avec![];
  assert!(x.is_empty());
  assert_eq!(avec![1, 2, 3,], vec![1, 2, 3]);
  assert_eq!(avec![1; 3], vec![1, 1, 1]);
}

Some examples to learn rust declarative macros from:

Macro for Implementing Getters

pub struct Person {
    name: String,
    age: u32,
}
macro_rules! generate_getters {
    ($type_name: ty, $($getter_name: ident, $elem_name: ident, $elem_type: ty),+) => {
        $(impl $type_name {
            pub fn $getter_name(&self) -> $elem_type {
                &self.$elem_name
            }
        })+
    };
}

Macro for implementing default trait

pub enum Help {
    Cry,
    SoS,
    Noop,
}
pub struct Elf(Help);
macro_rules! impl_default_elf {
    ($type: ty, $default_val: expr) => {
        impl Default for $type {
            fn default() -> Self {
                Self($default_val)
            }
        }
    };
}
impl_default_elf!(Elf, Help::SoS);

Macro for enum parsing

Resource

enhanced_enum!(
    Chicks {
        BBW,
        Redhead,
        Latina,
        Age(u8),
        Ass,
    },
    name_chick
);
macro_rules! enhanced_enum {
    ($type: ident {
		$($variant: tt)*
	}, $method_name: ident) => {
        #[derive(Debug)]
        pub enum $type {
            $($variant)*
        }
 
        impl $type {
	        // pub fn $method_name(&self) -> &'static str {
    		// 	another_enum_impl!(self, $($variant)*);
			// 	""
        	// }
			another_enum_match_impl!([] $($variant)*);
        }
    };
}
// using if expression
macro_rules! another_enum_impl {
    ($self:ident, $variant: ident, $($rest: tt)*) => {
        if let Self::$variant = $self {
            return stringify!($variant);
        }
    };
    ($self: ident, $variant:ident($type: ty), $($rest:tt)*) => {
        if let Self::$variant(_) = $self {
            return stringify!($variant);
        }
    };
}
 
// using tt munching: variable tt matcher expression
macro_rules! another_enum_match_impl {
    ([$($processed:tt)*] $variant:ident, $($rest: tt)*) => {
		another_enum_match_impl!{[
			$($processed)*
			Self::$variant => stringify!($variant),
		] $($rest)*}
	};
    ([$($processed:tt)*] $variant:ident($type:ty), $($rest:tt)*) => {
		another_enum_match_impl!{[
				$($processed)*
				Self::$variant(_) => stringify!($variant),
			] $($rest)*
        }
	};
	([$($processed:tt)*]) => {
		pub fn name(&self) -> &'static str {
			match self {
				$($processed)*
			}
		}
	};
}

Iterators

trait IteratorExt
where
    Self: Iterator + Sized,
{
    fn flatten_ext(self) -> Flatten<Self>
    where
        Self::Item: IntoIterator;
}
 
impl<T> IteratorExt for T
where
    T: Iterator,
{
    fn flatten_ext(self) -> Flatten<Self>
    where
        Self::Item: IntoIterator,
    {
        flatten(self)
    }
}
 
// This is the main function flatten which takes any two level nested iterator and returns Flatten struct.
// Defined on generic `O` which has to implement `IntoIterator` and it's item also has to implement `IntoIterator`
 
fn flatten<O>(iter: O) -> Flatten<O::IntoIter>
where
    O: IntoIterator,
    O::Item: IntoIterator,
{
    Flatten::new(iter.into_iter())
}
 
// Struct which houses outer and inner iterators to move on the data structure.
// Is generic on `O` such that O has to iterator and O's item has to implement `IntoIterator`
struct Flatten<O>
where
    O: Iterator,
    O::Item: IntoIterator,
{
    outer: O,
    iter_next: Option<<O::Item as IntoIterator>::IntoIter>,
    iter_back: Option<<O::Item as IntoIterator>::IntoIter>,
}
 
// Constructor fn
impl<O> Flatten<O>
where
    O: Iterator,
    O::Item: IntoIterator,
{
    fn new(iter: O) -> Self {
        Flatten {
            outer: iter,
            iter_next: None,
            iter_back: None,
        }
    }
}
 
// Implements Iterator trait's next function for `Flatten` struct
impl<O> Iterator for Flatten<O>
where
    O: Iterator,
    O::Item: IntoIterator,
{
    type Item = <O::Item as IntoIterator>::Item;
    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if let Some(ref mut inner_iter) = self.iter_next {
                if let Some(res) = inner_iter.next() {
                    return Some(res);
                }
 
                self.iter_next = None;
            }
 
            if let Some(next_inner) = self.outer.next() {
                self.iter_next = Some(next_inner.into_iter());
            } else {
                return self.iter_back.as_mut()?.next();
            }
        }
    }
}
 
// Implements DoubleEndedIterator's next_back fn for `Flatten` struct
impl<O> DoubleEndedIterator for Flatten<O>
where
    O: Iterator + DoubleEndedIterator,
    O::Item: IntoIterator,
    <O::Item as IntoIterator>::IntoIter: DoubleEndedIterator,
{
    fn next_back(&mut self) -> Option<Self::Item> {
        loop {
            if let Some(ref mut inner_iter) = self.iter_back {
                if let Some(res) = inner_iter.next_back() {
                    return Some(res);
                }
 
                self.iter_back = None
            }
 
            if let Some(next_back_inner) = self.outer.next_back() {
                self.iter_back = Some(next_back_inner.into_iter());
            } else {
                return self.iter_next.as_mut()?.next_back();
            }
        }
    }
}

Cargo

[workspace]
members = ["node", "narwhal-shim", "evm-client"]

workspace: used to add different packages together in a project with same cargo.lock.

Error Handling

Closures

closures: lambdas instantiated as:

let a = || {
// body
};

rust-multithreading

Interior mutability

async

Advanced Topics

Allocation

Memory Layout

Resources

Unstable features

Specialisation

Extends rust trait’s functionality to have overloading like feature using specialisations.

"traits today can provide static dispatch in Rust,"

what’s the meaning of static dispatch?

std crates

std

arch

alloc

Crates

Proptest

introduces property testing, a framework to determine failing inputs for certain properties of the system. can be used substitute for fuzzing + UTs.

tokio

serde

reqwest

hyper

rayon

anyhow

clap

tracing

criterion

cargo fuzz

Resources

Interesting Questions