Skip to content

Latest commit

 

History

History
877 lines (687 loc) · 21.4 KB

B-application-programming.md

File metadata and controls

877 lines (687 loc) · 21.4 KB
theme class highlighter lineNumbers info drawings fonts layout title
default
text-center
shiki
true
Rust - B: Application programming
persist
mono
Fira Mono
cover
Rust - B: Application programming

Rust programming

Module B: Application programming


layout: three-slots

Who am i?

::left::

  • Ferris
  • I Love Rust

::right:: Photo Ferris


layout: default

Last time...

  • [TODO: add last module's topics]

Any questions?


layout: section

Recap Quiz

[Link to quiz here]


layout: iframe url: http://your-quiz-url-here


layout: default

In this module

Learn how to use Rust for writing high quality applications


layout: default

Learning objectives

  • Set up your own Rust application and library
  • Divide your code into logical parts with modules
  • Create a nice API
  • Test and benchmark your code
  • Use common crates (tutorial)

layout: section

Mindmap

What do you know already about this subject?

[Mindmap access code here]



layout: cover

Module B

Application programming


layout: default

Content overview

  • Project structure
  • API guidelines
  • Testing and benchmarking

layout: section

Rust Project structure


layout: default

Terminology

  • Crate: A package containing Rust source code. Library or binary.
  • Module: Logical part of crate, containing items.
  • Workspace: Set of related crates.

layout: default

Setting up a crate

Setting up a new crate is easy:

$ cd /path/to/your/projects
$ cargo new my-first-app --bin
$ tree my-first-app
.
├── Cargo.toml
└── src
    └── main.rs

Pass --lib instead of --bin to create a library


layout: default

Adding a crate as dependency

To add a dependency from crates.io:

$ cargo add tracing tracing-subscriber
[...]
$ cat Cargo.toml
[package]
name = "my-first-app"
version = "0.1.0"
edition = "2021"

# -snip-

[dependencies]
tracing = "0.1.37"
tracing-subscriber = "0.3.16"

layout: default

Using dependencies

Dependencies from Cargo.toml can be:

  • imported with a use
  • qualified directly using path separator ::
// Import an item from this crate, called `my_first_app`
use my_first_app::add;
// Import an item from the `tracing` dependency
use tracing::info;

fn main() {
    // Use qualified path
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::DEBUG)
        .init();

    let x = 4;
    let y = 6;

    // Use imported items
    let z = add(x, y);
    info!("Let me just add {x} and {y}: {z}")
}

layout: default

Other dependency sources

  • Local
  • Git
$ cat Cargo.toml
# -snip-
[dependencies]
my_local_dependency = { path = "../path/to/my_local_dependency" }
my_git_dependency = { git = "<Git SSH or HTTPS url>", rev="<commit hash or tag>", branch = "<branch>" }

layout: default

Modules

  • Logical part of a crate
  • Public or private visibility
  • Defined in blocks or files

Mod structure != file structure


layout: default

Module block

// Public module
// Accessible from outside
pub mod my_pub_mod {
    // Private module
    // Only accessible from parent module
    mod private_mod {

        // Public struct
        // Accessible wherever `private_mod` is
        pub struct PubStruct {
            field: u32,
        }
    }

    // Private struct
    // Only accessible from current and child modules
    struct PrivStruct {
        field: private_mod::PubStruct,
    }
}

layout: default

Module files

Content specified in

  • Either some_mod.rs
  • Or another_mod/mod.rs
$ tree src
.
├── another_mod
│   └── mod.rs
├── lib.rs
├── main.rs
└── some_mod.rs

layout: default

Module files

Mod structure defined in other modules:

lib.rs

// Points to ./some_mod.rs
mod some_mod;
// Points to ./another_mod/mod.rs
mod another_mod;
// Imports an item defined in ./another_mod/mod.rs
use another_mod::Item;
$ tree src
.
├── another_mod
│   └── mod.rs
├── lib.rs
├── main.rs
└── some_mod.rs

layout: default

Module files vs blocks

  • Use blocks for small (private) modules
  • Use files for larger (public) modules
  • Group related module files in folder

If your file gets unwieldy, move code to new module file


layout: default

Binaries and examples

  • Use multiple binaries if you are creating
    • multiple similar executables
    • that share code
  • Create examples to show users how to use your library

layout: section

Creating a nice API


layout: two-cols

Rust API guidelines

::right::

Read the checklist, use it!


layout: default

General recommendations

Make your API

  • Unsurprising
  • Flexible
  • Obvious

Next up: Some low-hanging fruits


layout: section

Make your API

Unsurprising


layout: default

Naming your methods

pub struct S {
    first: First,
    second: Second,
}

impl S {
    // Not get_first.
    pub fn first(&self) -> &First {
        &self.first
    }

    // Not get_first_mut, get_mut_first, or mut_first.
    pub fn first_mut(&mut self) -> &mut First {
        &mut self.first
    }
}

Other example: conversion methods as_, to_, into_, name depends on:

  • Runtime cost
  • Owned ↔ borrowed

layout: two-cols

Implement/derive common traits

As long as it makes sense public types should implement:

  • Copy
  • Clone
  • Eq
  • PartialEq
  • Ord
  • PartialOrd

::right::

  • Hash
  • Debug
  • Display
  • Default
  • serde::Serialize
  • serde::Deserialize

layout: section

Make your API

Flexible


layout: default

Use generics

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

/// Adds two values that implement the `Add` trait,
/// returning the specified output
pub fn add_generic<O, T: std::ops::Add<Output = O>>(x: T, y: T) -> O {
    x + y
}

layout: default

Accept borrowed data if possible

  • User decides whether calling function should own the data
  • Avoids unnecessary moves
  • Exception: non-big array Copy types
/// Some very large struct
pub struct LargeStruct {
    data: [u8; 4096],
}

/// Takes owned [LargeStruct] and returns it when done
pub fn manipulate_large_struct(mut large: LargeStruct) -> LargeStruct {
    todo!()
}

/// Just borrows [LargeStruct]
pub fn manipulate_large_struct_borrowed(large: &mut LargeStruct) {
    todo!()
}

layout: section

Make your API

Obvious


layout: two-cols

Write Rustdoc

  • Use 3 forward-slashes to start a doc comment
  • You can add code examples, too
/// A well-documented struct.
/// ```rust
/// # // lines starting with a `#` are hidden
/// # use ex_b::MyDocumentedStruct;
/// let my_struct = MyDocumentedStruct {
///     field: 1,
/// };
/// println!("{:?}", my_struct.field);
/// ```
pub struct MyDocumentedStruct {
    /// A field with data
    pub field: u32,
}

To open docs in your browser:

$ cargo doc --open

::right::


layout: default

Use semantic typing

Make the type system work for you!

fn enable_led(enabled: bool) {
    todo!("Enable it")
}

enum LedState {
    Enabled,
    Disabled
}

fn set_led_state(state: LedState) {
    todo!("Enable it")
}

fn do_stuff_with_led() {
    enable_led(true);
    set_led_state(LedState::Enabled)
}

layout: quote

Use Clippy and Rustfmt for all your projects!

$ cargo clippy
$ cargo fmt

layout: section

Testing your crate


layout: default

Testing methods

  • Correctness
    • Unit tests
    • Integration tests
  • Performance
    • Benchmarks

layout: default

Unit tests

  • Tests a single function or method
  • Live in child module
  • Can test private code

To run:

$ cargo test
[...]
running 2 tests
test tests::test_swap_items ... ok
test tests::test_swap_oob - should panic ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[..]

layout: full

/// Swaps two values at the `first` and `second` indices of the slice
fn slice_swap_items(slice: &mut [u32], first: usize, second: usize) {
    let tmp = slice[second];
    slice[second] = slice[first];
    slice[first] = tmp;
}

/// This module is only compiled in `test` configuration
#[cfg(test)]
mod tests {
    use crate::slice_swap_items;

    // Mark function as test
    #[test] 
    fn test_swap_items() {
        let mut array = [0, 1, 2, 3, 4, 5];
        slice_swap_items(&mut array[..], 1, 4);
        assert_eq!(array, [0, 4, 2, 3, 1, 5]);
    }

    #[test]
    // This should panic
    #[should_panic] 
    fn test_swap_oob() {
        let mut array = [0, 1, 2, 3, 4, 5];
        slice_swap_items(&mut array[..], 1, 6);
    }
}

layout: default

Integration tests

  • Tests crate public API
  • Run with cargo test
  • Defined in tests folder:
$ tree
.
├── Cargo.toml
├── examples
│   └── my_example.rs
├── src
│   ├── another_mod
│   │   └── mod.rs
│   ├── bin
│   │   └── my_app.rs
│   ├── lib.rs
│   ├── main.rs
│   └── some_mod.rs
└── tests
    └── integration_test.rs

layout: default

Benchmarks

  • Test performance of code (vs. correctness)
  • Runs a tests many times, yield average execution time

Good benchmarking is Hard

  • Beware of optimizations
  • Beware of initialization overhead
  • Be sure your benchmark is representative

More in exercises


layout: default

Summary

  • Set up your own Rust application and library
    • Using cargo new
  • Divide your code into logical parts with modules
    • Modules
    • Workspaces
  • Create a nice API
    • Unsurprising, Flexible, Obvious
    • API guidelines
  • Test and benchmark your code
    • Unit tests, integration tests, benchmarks

layout: end