7 September 2023
So you've learned the basics of Rust - now what? In this comprehensive guide, we'll cover key intermediate concepts that will level up your Rust skills. We'll look at managing complex projects, advanced trait usage, smart pointers, asynchronous programming, functional concepts, testing, interoperability, web development, and more through practical examples.
This article is part of our JBI Training course in Rust training
By the end, you'll feel empowered to build robust, fast, and reliable applications in Rust. So let's dive in!
As your Rust projects grow in scope, you'll want to organize code into multiple crates and libraries. Cargo provides workspaces to manage multiple related packages under one umbrella.
A workspace allows you to:
Let's walk through a simple workspace setup:
Create a new cargo project:
cargo new my-workspace --lib
Add a members
section in Cargo.toml
:
toml
[workspace] members = ["my-lib"]
Create a Rust library crate:
cargo new my-lib
[dependencies] my-lib = { path = "./my-lib" }
Now both crates are part of the workspace. You can build them together:
cargo build --workspace
This structure lets you easily share code while still keeping crates decoupled.
Use workspaces when:
They provide organization for complex Rust projects.
Traits abstract over behavior in Rust. You've likely used basic traits like Debug
and Display
. But traits have even more powerful capabilities.
Some advanced trait patterns include:
Traits can declare associated types for abstraction:
trait Graph { type Node; type Edge; fn add_node(&mut self, Node); fn add_edge(&mut self, Edge); }
Implementors specify the concrete types:
struct MyGraph { nodes: Vec<MyNode>, edges: Vec<MyEdge>, } impl Graph for MyGraph { type Node = MyNode; type Edge = MyEdge; // ...implement methods... }
This allows generic graph handling code.
You can specify default behavior for traits:
trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } }
Implementors can override or use the default.
Trait bounds let you restrict generics to types that implement a trait:
fn notify<T: Summary>(item: T) { println!("Breaking news! {}", item.summarize()); }
T
must implement Summary
.
You can use trait objects for dynamic dispatch:
fn print_summary(item: &dyn Summary) { println!("{}", item.summarize()); }
item
can be any type that implements Summary
.
Mastering advanced trait techniques unlocks generic, flexible code in Rust.
Rust's ownership model means you don't need to manually allocate and free memory - but sometimes you still want more control over allocation.
Smart pointers provide custom ownership behavior while preventing leaks or dangling pointers.
Some useful smart pointers include:
Box<T>
allocates a value on the heap instead of the stack:
let b = Box::new(5); // Stored on the heap
Useful for recursive types.
Rc<T>
provides reference counting for shared ownership:
let a = Rc::new(42); let b = a.clone(); // Increments ref count
a
and b
both own the data. Freed when count reaches 0.
Arc<T>
is an atomically reference counted pointer for concurrent code:
use std::sync::Arc; let a = Arc::new(Data); // Share across threads
Safely shares ownership between threads.
Smart pointers give you more tools to manage memory and ownership!
Writing asynchronous code efficiently is crucial for performance. Rust provides first-class support for async programming with async
/await
.
Here is an async HTTP server:
async fn handle_request(req: Request) -> Response { // ... } async fn listen() { let listener = TcpListener::bind("0.0.0.0:8080").await?; loop { let (stream, _) = listener.accept().await?; let response = handle_request(stream).await?; stream.send(response).await? } }
Some key async features:
async fn
defines an asynchronous function.await
pauses execution until completedAsync Rust enables efficient network services, web apps, CLI tools, and more without compromising safety.
Rust isn't strictly a functional language, but it does provide useful functional constructs.
Some examples:
Closures are anonymous functions you can save in variables:
let sqrt = |num| num.powf(0.5); println!("{}", sqrt(4.0)); // Prints 2
Very useful with iterators.
Iterators abstract collection processing:
let v = vec![1, 2, 3]; let sum = v.iter().map(|x| x * 2).fold(0, |acc, x| acc + x); // 12
Chaining iterator adapters promotes a functional style.
Option
and Result
enums can represent the outcomes of computations:
fn inverse(x: f64) -> Result<f64, String> { if x == 0.0 { Err("Cannot divide by zero".to_string()) } else { Ok(1.0 / x) } }
Models errors in a functional way.
Rust isn't Haskell, but it does provide abstractions for functional-style code. This can make your programs more robust and declarative.
Rust doesn't just catch errors at compile-time - it also encourages exhaustive testing. Here are some techniques for rigorously testing Rust code:
Rust includes built-in unit test support:
rust
Copy code
#[cfg(test)] mod tests { #[test] fn test_add() { assert_eq!(2 + 2, 4); } }
Just cargo test
to run them.
Cargo supports integration test binaries:
rust
Copy code
// tests/int.rs #[test] fn test_integration() { // Run external program // Assert on outputs }
Test binaries with cargo test --test int
.
Crates like quickcheck
provide property based testing:
rust
Copy code
#[quickcheck] fn reverse_reverse(xs: Vec<isize>) -> bool { xs == xs.clone().into_iter().rev().rev().collect() }
Quickly generates random data as test cases.
Rust testing tools ensure your programs are correct - not just free of crashes.
A huge benefit of Rust is easy interoperability with C libraries. Let's walk through a basic C FFI example.
First, a C library:
c
Copy code
// add.c int add(int x, int y) { return x + y; }
We can call this from Rust:
rust
Copy code
// src/lib.rs use libc::c_int; extern "C" { fn add(x: c_int, y: c_int) -> c_int; } pub fn call_add(x: i32, y: i32) -> i32 { unsafe { add(x, y) } }
Key steps:
extern "C"
block declares C ABIunsafe
This allows reusing battle-tested C libraries in Rust!
Rust's performance and reliability make it a great backend language choice. Let's see a simple web app in Rust.
We'll use the Rocket web framework along with the Maud templating engine.
Our Cargo.toml
will include:
[dependencies] rocket = "0.5.0-rc.2" maud = "0.24.0"
Here is a basic route handler:
#[get("/")] fn index() -> Template { let context = index_context(); Template::render("index", context) }
And the Maud template:
@(context: index::Context) h1 { "#[context.project] App" }
We can also easily expose Rust types to client JavaScript using WASM.
Rust is well-suited for safe, scalable web development.
Lifetimes - Rust's secret sauce for references. You've seen simple lifetime syntax like:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
This says x
and y
must live as long as output. But Rust has even more flexible lifetime capabilities:
We can parameterize functions over lifetimes:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x }
Now x
and y
can have different lifetimes.
Rust can omit explicit lifetimes when they can be inferred:
fn len(s: &str) -> usize { s.len() }
Input and output lifetimes are inferred.
The 'static
lifetime means data lives for the entire program:
let STATIC_STR: &'static str = "Hello";
Useful for global constants.
Lifetime syntax gives you fine-grained control over references in Rust.
Rust keeps you safe - except for clearly marked unsafe
code:
unsafe { let x = 5; }
Why unsafe? Low-level use cases like:
Some guidelines for unsafe
:
While rare, unsafe
is enabling for systems programming. Use judiciously.
We've covered a lot of ground here! You should now feel empowered to build more complex, high-performance applications in Rust. But this is just the beginning...
Rust has an incredibly rich ecosystem. Here are some ideas for furthering your Rust journey:
The Rust language and community are incredibly welcoming and eager to help you learn. Keep coding, stay curious, and have fun with Rust!
Rust provides C++ level control over memory and performance without compromising on safety or productivity. Rust's enums, traits, patterns, and safety guarantees create a much more ergonomic systems language.
Rust shines for performance critical applications like servers, game engines, operating systems, embedded devices, and any code where stability and speed are priorities.
Rust uses an ownership model checked at compile time to ensure references are always valid. The compiler verifies no dangling pointers, use-after-free, or data races can occur.
Organizations using Rust in production include Microsoft, Amazon, Dropbox, Figma, Discord, Cloudflare, Stripe, Uber, Reddit, and many more!
Rust has a steeper initial learning curve than other languages like Python. But core concepts like ownership and borrowing become second nature pretty quickly. The compiler also provides helpful error messages as you learn. Overall, Rust rewards the time investment.
you might enjoy the Previous article to this Rust for Beginners: A Comprehensive 3-Hour Crash Course
CONTACT
+44 (0)20 8446 7555
Copyright © 2024 JBI Training. All Rights Reserved.
JB International Training Ltd - Company Registration Number: 08458005
Registered Address: Wohl Enterprise Hub, 2B Redbourne Avenue, London, N3 2BS
Modern Slavery Statement & Corporate Policies | Terms & Conditions | Contact Us