Copied to Clipboard
💡 Popular crates like serde use proc macros to generate serialization/deserialization code automatically with just #[derive(Serialize, Deserialize)].
14. Modules & the Cargo Ecosystem
Modules
Rust organizes code into a module tree. You define modules with mod, control visibility with pub, and bring items into scope with use.
mod math {
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn subtract(a: i32, b: i32) -> i32 { a - b }
pub mod advanced {
pub fn power(base: i32, exp: u32) -> i32 {
base.pow(exp)
}
}
}
use math::advanced::power;
fn main() {
println!("{}", math::add(3, 4)); // 7
println!("{}", math::subtract(10, 4)); // 6
println!("{}", power(2, 8)); // 256
}
Splitting across files
src/
├── main.rs
├── math.rs ← mod math; in main.rs
└── math/
└── advanced.rs ← pub mod advanced; in math.rs
// main.rs
mod math;
use math::add;
fn main() {
println!("{}", add(2, 3));
}
// math.rs
pub mod advanced;
pub fn add(a: i32, b: i32) -> i32 { a + b }
Cargo — The Build System & Package Manager
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
anyhow = "1"
[dev-dependencies]
pretty_assertions = "1"
Essential Cargo commands:
cargo new my_app # create a new project
cargo build # compile (debug)
cargo build --release # compile (optimized)
cargo run # build + run
cargo test # run all tests
cargo doc --open # generate and open docs
cargo add serde # add a dependency
cargo update # update dependencies
cargo clippy # lint your code
cargo fmt # auto-format
15. Testing in Rust
Testing is built into the language — no extra framework needed. Tests live right next to your code.
Unit Tests
pub fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide_normal() {
assert_eq!(divide(10.0, 2.0), Some(5.0));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(divide(5.0, 0.0), None);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic() {
let v = vec![1, 2, 3];
let _ = v[10]; // should panic
}
#[test]
fn test_with_message() {
let result = divide(9.0, 3.0);
assert!(result.is_some(), "expected Some, got None");
assert!((result.unwrap() - 3.0).abs() < f64::EPSILON);
}
}
Run with:
cargo test # run all tests
cargo test test_divide # run tests matching the name
cargo test -- --nocapture # show println! output
Integration Tests
Integration tests live in a top-level tests/ folder and test your public API.
src/
└── lib.rs
tests/
└── integration_test.rs
// tests/integration_test.rs
use my_app::divide;
#[test]
fn test_public_api() {
assert_eq!(divide(20.0, 4.0), Some(5.0));
}
Test with Result
#[test]
fn test_returns_result() -> Result<(), String> {
let val = divide(10.0, 2.0).ok_or("division failed")?;
assert_eq!(val, 5.0);
Ok(())
}
💡 Use cargo test -- --test-threads=1 if tests share state and need to run sequentially.
16. Closures as Return Types & Higher-Order Functions
Rust can return closures from functions, enabling functional patterns like currying and function factories.
// Return a closure using impl Fn
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
// Return a closure on the heap using Box<dyn Fn>
fn make_multiplier(x: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |y| x * y)
}
fn apply_twice<F: Fn(i32) -> i32>(f: F, val: i32) -> i32 {
f(f(val))
}
fn main() {
let add5 = make_adder(5);
let triple = make_multiplier(3);
println!("{}", add5(10)); // 15
println!("{}", triple(7)); // 21
println!("{}", apply_twice(add5, 0)); // 10
println!("{}", apply_twice(|x| x * 2, 3)); // 12
}
When to use impl Fn vs Box<dyn Fn>:
impl Fn |
Box<dyn Fn> |
| Storage in struct |
❌ |
✅ |
| Return from function |
✅ |
✅ |
| Runtime cost |
Zero |
Small heap alloc |
| Works in collections |
❌ |
✅ |
17. Unsafe Rust
Rust's safety guarantees are enforced at compile time — but sometimes you need to step outside them. unsafe blocks let you do four things the borrow checker won't normally allow.
What unsafe unlocks:
- Dereference raw pointers
- Call unsafe functions or C functions
- Access or mutate static mutable variables
- Implement unsafe traits
fn main() {
// Raw pointers
let x = 42;
let r: *const i32 = &x; // create raw pointer (safe)
unsafe {
println!("{}", *r); // dereference (unsafe)
}
// Mutable raw pointer
let mut y = 10;
let rw: *mut i32 = &mut y;
unsafe {
*rw += 1;
}
println!("{}", y); // 11
}
Unsafe functions:
unsafe fn dangerous_operation(ptr: *mut i32, len: usize) {
for i in 0..len {
*ptr.add(i) *= 2;
}
}
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
let ptr = data.as_mut_ptr();
let len = data.len();
unsafe {
dangerous_operation(ptr, len);
}
println!("{:?}", data); // [2, 4, 6, 8, 10]
}
Static mutable variables:
static mut COUNTER: u32 = 0;
fn increment() {
unsafe { COUNTER += 1; }
}
fn main() {
increment();
increment();
unsafe { println!("Counter: {}", COUNTER); } // 2
}
⚠️ unsafe doesn't turn off the borrow checker entirely — it only unlocks those four specific superpowers. You're taking responsibility for ensuring correctness in those blocks. Keep unsafe blocks as small as possible and document why they're safe.
18. FFI — Calling C from Rust
Rust can interoperate with C libraries using its Foreign Function Interface. This is how Rust wraps system APIs, game engines, and existing C codebases.
Calling a C standard library function
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn abs(x: i32) -> i32;
}
fn main() {
// Calling C's abs()
let result = unsafe { abs(-42) };
println!("{}", result); // 42
// Calling C's strlen()
let s = CString::new("hello").unwrap();
let len = unsafe { strlen(s.as_ptr()) };
println!("{}", len); // 5
}
Exposing Rust to C
// lib.rs — compile with: rustc --crate-type=cdylib
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// main.c — link against the compiled Rust library
extern int add(int a, int b);
int main() {
printf("%d\n", add(3, 4)); // 7
}
Using bindgen for real C libraries
For larger C libraries, use bindgen to auto-generate Rust bindings:
cargo add bindgen --build
// build.rs
fn main() {
bindgen::Builder::default()
.header("wrapper.h")
.generate()
.unwrap()
.write_to_file("src/bindings.rs")
.unwrap();
}
💡 Most real-world FFI is wrapped in a safe Rust API. The unsafe extern calls live in a *-sys crate (e.g. openssl-sys), and a higher-level crate wraps them in a safe interface (e.g. openssl).
Wrapping Up
Here's what Part 3 covered:
| Concept |
What it gives you |
| Macros |
Compile-time code generation, reduce boilerplate |
| Modules & Cargo |
Code organization, dependency management |
| Testing |
Built-in unit, integration, and doc tests |
| Higher-order functions |
Functional patterns, closures as values |
| Unsafe Rust |
Raw pointers and low-level control when needed |
| FFI |
Interop with C libraries and system APIs |
What's in Part 4?
- Concurrency with threads and channels
- The
Send and Sync traits
-
Mutex, RwLock, and atomics
- Building a real CLI tool with
clap
If this helped, drop a ❤️ and follow for Part 4!