theme | class | highlighter | lineNumbers | info | drawings | fonts | layout | title | |||
---|---|---|---|---|---|---|---|---|---|---|---|
default |
text-center |
shiki |
true |
Rust - B: Application programming |
|
|
cover |
Rust - B: Application programming |
Module B: Application programming
::left::
- Ferris
- I Love Rust
- [TODO: add last module's topics]
Any questions?
layout: iframe url: http://your-quiz-url-here
Learn how to use Rust for writing high quality applications
- 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)
What do you know already about this subject?
layout: iframe url: http://your-interactive-mindmap-url-here
Application programming
- Project structure
- API guidelines
- Testing and benchmarking
- Crate: A package containing Rust source code. Library or binary.
- Module: Logical part of crate, containing items.
- Workspace: Set of related crates.
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
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"
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}")
}
- 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>" }
- Logical part of a crate
- Public or private visibility
- Defined in blocks or files
Mod structure !=
file structure
// 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,
}
}
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
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
- 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
- Use multiple binaries if you are creating
- multiple similar executables
- that share code
- Create examples to show users how to use your library
- Defined by Rust project
- Checklist available (Link in exercises)
Read the checklist, use it!
Make your API
- Unsurprising
- Flexible
- Obvious
Next up: Some low-hanging fruits
Make your API
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
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
Make your API
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
}
- 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!()
}
Make your API
- 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
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)
}
$ cargo clippy
$ cargo fmt
- Correctness
- Unit tests
- Integration tests
- Performance
- Benchmarks
- 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
[..]
/// 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);
}
}
- 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
- 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
- Set up your own Rust application and library
- Using
cargo new
- Using
- 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