Skip to main content

Notes on Rust

· 4 min read
Hinny Tsang
Data Scientist @ Pollock Asset Management

Some notes for the rust programming language.

Borrow Checker

  • Ownership rules:

    1. Each value has one owner.
    2. There can only be one owner at a time.
    3. References will be dropped when go out of scope.
    4. The checker evaluates at compile time.
  • References and Borrowing:

    • At any given time, you can have either
    • 1 mutable reference, or
    • any number of immutable references.

Lifetimes

The three rules

  1. Compiler assigns a lifetime parameter to each parameter that is a reference.
  2. If there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters.
  3. If &self or &mut self is one of the input lifetime parameters, the lifetime of self is assigned to all output lifetime parameters.
  • 'static: Static Lifetime denotes that the reference can live for the entire duration of the program.

Tests

Should know:

  • #[cfg(test)]: module only compiled when running tests
  • #[test]: function is a test function
  • cargo test: run testsj
    • cargo test -- --test-threads=1 to run tests in single thread
    • cargo test <test_name> to run specific test
    • cargo test -- --show-output to show output of passing tests
    • cargo test -- --ignored to run ignored tests
  • assert!, assert_eq!, assert_ne!: macros to check if conditions are met
  • #[should_panic]: test should panic to pass, e.g.
    #[test]
    #[should_panic] // Or #[should_panic(expected = "divide by zero")] to check panic message
    fn test_divide_by_zero() {
    let _ = 1 / 0;
    }
  • Result<T, E>: test can return a Result type instead of using assert macros, but can't use with #[should_panic].
  • ignore attribute to ignore specific tests, use after #[test]

Closure

Closure will automically implment 1, 2, or all of the following traits:

  • Fn: closure only borrows values from the environment immutably
  • FnMut: closure borrows values from the environment mutably
  • FnOnce: closure takes ownership of values from the environment (move keyword)

Smart Pointers

  • Box<T>: for heap allocation, single ownership.
  • Rc<T>: for shared ownership, reference counting, single-threaded.
  • RefCell<T>: allows mutable borrows checked at runtime, even if RefCell<T> is immutable, single-threaded.
    • Rc<RefCell<T>>: shared ownership with interior mutability, single-threaded!
  • Weak<T>: non-owning reference to Rc<T>, prevents reference cycles, created using Rc::downgrade(&rc).

Interior Mutability

Rust enforces borrowing rules at compile time. While it may reject safe code that violates these rules, interior mutability is a way to check these rules at runtime.

Concurrency

  • Arc<T>: Atomic Reference Counted, thread-safe version of Rc<T>.
  • Mutex<T>: Mutual Exclusion, provides interior mutability with locking mechanism for thread safety.
  • RwLock<T>: Read-Write Lock, allows multiple readers or one writer at a time, useful for read-heavy scenarios.

Threads can be spawned using std::thread::spawn and can be joined using the join method on the thread handle.

Send and Sync Traits

  • Send: Transfer ownership to another thread.
  • Sync: Allow references to be shared between multiple threads.

Async tasks

The rust book introduced trpl library for async programming, below are some common uses:

  • trpl::spawn_task(future): spawn a new async task.

  • trpl::join(futures, future2): wait for two futures to complete.

  • trpl::join!(...futures): wait for multiple futures to complete.

  • trpl::race(future1, futures2): wait for the first future to complete.

  • trpl::channel: create a channel for communication between tasks (mpsc, async).

  • Streams:

    • trpl::stream_from_iter(iter): create a stream from an iterator.
    • trpl::ReceiverStream(rx): create a stream from a receiver.

Create futures

let my_future = async {
// async code here
};

let my_future_with_move = async move {
// async code here
};

my_future.await;

Pin for futures

Unlike other types, futures may be self-referential, meaning they can contain references to themselves. This can lead to issues when the future is moved in memory, as the references may become invalid.

To address this, Rust provides the Pin type, which prevents a value from being moved once it has been pinned. This is particularly useful for futures, as it ensures that the self-referential parts of the future remain valid for the duration of its execution.

Unpin and !Unpin:

By default, most types in Rust implement the Unpin trait, which means they can be safely moved in memory. However, some types, such as certain futures or self-referential structs, do not implement Unpin and are considered !Unpin. These types must be pinned to ensure their memory location remains stable.

When working with futures that are !Unpin, you typically use Pin<Box<dyn Future<Output = T>>> or Pin<&mut dyn Future<Output = T>> to pin them in place. This ensures that the future can be safely polled without the risk of invalidating any self-references.