Summary
- What is the Rust language designed for
- How to install Rust tools and compile small programs
- Ownership and borrowing
About Rust
Rust is a modern systems programming language that offers a compelling proposition: the low-level control and performance of C/C++, combined with powerful compile-time safety guarantees that prevent entire classes of common memory errors.
Rust started in 2006 as a personal project by a software developer named Graydon Hoare at Mozilla (creators of the Firefox web browser). In 2009, the development of Rust was officially sponsored by Mozilla, and after a lot of work Rust 1.0 was released in 2015. Rust has some obvious connections to Java and C++, but also to strongly typed functional languages like ML and Haskell. Mozilla became interested in it for use in web programming to achieve memory safety, and a very large global community of developers has now formed around the language.
For programmers accustomed to C and C++, Rust provides a way to write high-performance code without fearing segmentation faults, buffer overflows, or data races. For those familiar with Java or Python, Rust offers comparable memory safety but without the overhead or unpredictability of a garbage collector. It achieves this through a unique ownership model, which can be thought of as a strict set of protocols, enforced by the compiler, for managing access to sensitive resources.
Getting Started
The easiest way to install Rust is via rustup. Just run this command
and follow the instructions:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
In a pinch, you can also program rust in the browser via the rust playground.
There is fantastic and free documentation available to help you learn Rust in The Rust Book. But for SI413 we will just get a small taste of the language, so these course notes should be enough.
Hello world
Let’s begin our first operation. Create a file named main.rs.
// main.rs
fn main() {
println!("Awaiting transmission...");
}
Compile and run this using the Rust compiler, rustc:
rustc main.rs
./main
Cargo
For any project more complex than a single file, you will use Cargo. Cargo is Rust’s build system and package manager. It handles compiling code, managing dependencies (called “crates”), running tests, and more.
To create a new project with Cargo:
cargo new operation_rust
cd operation_rust
This creates a new directory with a Cargo.toml file and a
src/main.rs file. To build and run your project,
simply use cargo run.
Basic Syntax
Rust’s syntax will feel familiar if you know C-family languages.
-
Variables: Declared with
let. They are immutable by default. This is a core principle: resources should not be changed unless explicitly authorized. Usemutto authorize modification.let agent_id = "007"; // This ID cannot be changed. let mut target_location = "Geneva"; // The target's location can be updated. target_location = "Monaco"; -
Functions: Declared with
fn. Type annotations for parameters and the return value are mandatory for clear and explicit contracts.fn generate_intel(asset_name: String, priority: u32) -> String { // The format! macro creates a String without printing it. format!("Intel on {} has priority: {}", asset_name, priority) }
The Ownership Model: Managing The Asset’s Brief
This is the central feature of Rust. Imagine a single, physical brief on a top-secret asset. It’s a unique, critical resource. The agency’s protocols for handling this brief are strict and absolute — just like Rust’s ownership rules.
The Three Rules of Custody (Ownership)
- Each value (the brief) in Rust has a designated handler (a part of the code), called its owner.
- There can only be one owner at a time. (Only one agent has custody of the brief).
- When the owner goes out of scope (the mission ends), the value is dropped (the brief is incinerated).
Move Semantics: Transfer of Custody
Let’s represent our asset’s brief with a String, which stores its data on the heap. When you assign this brief to a new agent, custody is transferred. The original agent no longer has it. This is called a move.
fn main() {
// Agent "handler_a" acquires the brief.
let asset_brief = String::from("Asset name: 'Viper'. Location: Undisclosed.");
// The brief is transferred to another function (another handler).
// Custody MOVES from `asset_brief` to the `relay_intel` function.
relay_intel(asset_brief);
// The following line will NOT compile!
println!("Handler A still has: {}", asset_brief);
// Error: borrow of moved value: `asset_brief`
// Protocol violation: Handler A cannot access a brief they no longer possess.
}
fn relay_intel(brief: String) {
println!("Intel relayed: '{}'", brief);
} // `brief` goes out of scope. The mission for this handler is over.
// Rust `drop`s the variable, and the brief is incinerated.
By invalidating asset_brief after the move, the Rust compiler prevents
“use after free” bugs. You cannot use a resource you no longer own.
The C Pitfall: Double Incineration
In C, the protocol is based on convention, not enforcement. An agent can mistakenly incinerate a brief that another agent is also expected to incinerate later, causing a disaster.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void relay_intel_c(char* brief) {
printf("Intel relayed: '%s'\n", brief);
free(brief); // This handler believes their job is done and incinerates the brief.
}
int main() {
char* asset_brief = strdup("Asset name: 'Viper'");
relay_intel_c(asset_brief);
// DANGER: `asset_brief` now points to incinerated (freed) data.
// The original handler, unaware, tries to incinerate it again.
free(asset_brief); // RUNTIME ERROR: Double free!
return 0;
}
References and Borrowing: Reviewing Photocopies
What if an analyst just needs to read the brief without taking custody? For this, the agency issues a temporary photocopy. In Rust, this is a reference. Creating a reference is called borrowing.
fn main() {
let asset_brief = String::from("Asset is moving to Berlin.");
// We pass a reference (&asset_brief) to the analyst function.
// The function "borrows" the brief. It gets a photocopy.
let length = analyze_brief(&asset_brief);
println!("Brief analysis complete. Length is {}.", length);
// `asset_brief` is still valid and in our custody.
}
// The type `&String` means this function borrows a String.
fn analyze_brief(brief_copy: &String) -> usize {
brief_copy.len()
} // `brief_copy` (the photocopy) goes out of scope. The copy is shredded,
// but the original `asset_brief` remains untouched.
The Protocol for Borrowing
To prevent conflicting intelligence, the agency’s records officer (the compiler) enforces one critical protocol:
At any time, you can have either one officer redacting the brief (
&mut T) OR any number of analysts reviewing photocopies (&T), but never both.
You can have many read-only views, but as soon as someone needs to write, they must have exclusive access.
fn main() {
let mut asset_brief = String::from("Asset name: 'Viper'");
// An officer is granted exclusive access to redact the brief.
redact_brief(&mut asset_brief);
println!("Updated brief: {}", asset_brief);
}
fn redact_brief(brief: &mut String) {
brief.push_str(". Codename compromised.");
}
The C Pitfall: Using Outdated Intelligence
The Rust compiler’s “Records Officer” — the borrow checker — also ensures that no photocopy outlives the original brief. This prevents agents from acting on intelligence from a brief that has already been destroyed.
fn main() {
let photocopy;
{
let original_brief = String::from("Meet at the bridge at midnight.");
photocopy = &original_brief;
} // `original_brief` is incinerated here. The mission segment is over.
// The following line will NOT compile!
println!("Acting on outdated intel from photocopy: {}", photocopy);
// Error: `original_brief` does not live long enough
}
The compiler sees that original_brief will be destroyed and stops us
from using photocopy, which would have become a dangling pointer.
In C, this is a classic source of catastrophic failures.
#include <stdio.h>
char* get_temporary_intel() {
char brief[] = "Meet at the bridge at midnight.";
return brief; // PROTOCOL VIOLATION: Returning a photocopy of a brief
// that is about to be incinerated.
}
int main() {
char* outdated_intel = get_temporary_intel();
// `outdated_intel` now points to memory that is no longer valid.
// The stack space for `brief` has been wiped.
printf("Acting on dangling pointer: %s\n", outdated_intel); // Undefined Behavior!
return 0;
}
This C code compiles, but the agent acts on corrupted information, potentially compromising the entire operation. Rust prevents this entire category of errors at compile time.