SOYCRAFT 4 will be launching on the 15th of July! Check out /craft/ for more information.
SNCA:Rust/Tutorial
The following is a brief tutorial on the Rust programming language. Because much of the design of Rust is influenced by C++, this tutorial makes the assumption you are at least familiar with C++; see the C++ tutorial for additional context.
Hello World[edit | edit source]
The Hello World program in Rust is as follows:
fn main() {
println!("Hello, world!");
}
Unlike in C and C++, the standard library is always implicitly available in Rust. Also, the main function in Rust typically returns () (the "unit type"), basically similar to void in C, C++, Java, C#, etc.
println!() is a macro. Unlike macros in C/C++, Rust macros interact directly with the syntax tree of Rust rather than through direct text substitution. A macro in Rust ends with an exclamation mark at the end of its name.
Basic types[edit | edit source]
Rust consists of the following basic types:
i8,i16,i32,i64,i128: signed integers of sizes 8, 16, 32, 64, 128 respectivelyu8,u16,u32,u64,u128: unsigned integers of sizes 8, 16, 32, 64, 128 respectivelyf32,f64,f128: floating point numbers of sizes 32, 64, and 128 respectivelybool: a boolean value, eithertrueorfalsechar: a single Unicode scalar value (note that unlike C/C++ wherecharis ASCII,charin Rust is a full Unicode character)(): the unit type, has exactly one value (itself) and represents nothing, similar tovoidin other languagesstr: a string slice, the most primitive string typeisize: a pointer-sized signed integer type, usually 8 bytes, similar tossize_tin Cusize: a pointer-sized unsigned integer type, usually 8 bytes, similar tosize_tin C
Other deeply fundamental types include:
String: a UTF-8 encoded growable string that owns its contents. It is distinct fromstr(the primitive).Option<T>: an option type, representing either the presence of a value (of typeT) or the absence of the object. If there is nothing inOption, its value isNone.Result<T, E>: a result type, representing either an object (of typeT) or an error (of typeE)[T; N]: an array of objects of typeTof lengthN(T1, T2, ..., TN): a tuple type, which can hold an indefinite number of objects of any typeVec<T>: a vector type, representing a dynamic array storing objects of typeTHashMap<K, V>: a dictionary-like type storing keys of typeK, and mapping them to objects of typeV
Variables[edit | edit source]
Much like in other languages, variables in Rust have a similar declaration, but use the keyword let. In most cases, the type of the variable can be omitted because it can usually be inferred by the compiler, but to explicitly label the type, use a colon.
fn main() {
let age = 18;
let name: &str = "Nate"; // Explicitly declare the type as &str (string slice)
let website = "soyjak.party";
println!("{name} is {age}, and thus old enough to post on {website}!");
}
In Rust, variables are immutable (like C/C++ const) by default. To define a compile-time constant, use const (which is like C/C++ constexpr). To allow variables to be mutable, use let mut.
let a: i32 = -5; let mut b: u16 = 8; const MAX_VALUE: usize = 300;
Control flow[edit | edit source]
Rust features the usual control flow structures as seen in C:
let x = 5;
if x > 0 {
println!("x = {x} is positive");
} else if x < 0 {
println!("x = {x} is negative");
} else {
println!("x is exactly zero");
}
Rust also features a if let, which unwraps an Option<T>:
let opt: Option<i32> = Some(5);
if let Some(x) = opt {
println!("x's value is {x}");
}
Also, instead of switch, Rust introduces the match block. This acts more generally than switch, particularly useful in pattern matching.
let day = 2;
match day {
1 => println!("Monday"),
2 => println!("Tuesday"),
3 => println!("Wednesday"),
_ => println!("Some other day"),
}
Loops[edit | edit source]
Rust features the same loops as seen in C and C++, and the same break and continue statements.
For loop[edit | edit source]
For loops in Rust have a different syntax, as they are iterator-based. This is more similar to the range-based for loop in C++:
// prints for i = 0, 1, 2, 3, 4
for i in 0..5 {
println!("{i}");
}
// prints for i = 0, 1, 2, 3, 4, 5
for i in 0..=5 {
println!("i = {i}");
}
While loop[edit | edit source]
The while loop in Rust is basically the same as in C/C++:
let mut x = 3;
while x > 0 {
println!("x = {x}");
x -= 1;
}
Infinite loop[edit | edit source]
In Rust, instead of using while true, there is an explicit syntax for an infinite loop:
loop {
println!("Cobson will always be a gem!");
}
Ownership[edit | edit source]
In Rust, every value has only a single owner. Once that owner goes out of scope, the value is dropped (i.e. destroyed, freed). When a value is passed or assigned, ownership moves:
let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2
// println!("{}", s1);
// Error, as s1 is no longer valid
Passing a value into a function moves it, but it can be given back by returning it. Meanwhile, it can be "borrowed", i.e. taken a reference (no ownership is taken).
Each value has one owner and there can only be one owner at a time.
Rust enforces a set of rules that ensure memory safety without a garbage collector:
- You may have any number of immutable references (
&T), OR exactly one mutable reference (&mut T), but not both simultaneously. - References must always be valid; there are no dangling references in Rust.
- Borrowing does not transfer ownership in Rust.
- A borrow ends when a variable is last used. For example:
let mut name = String::from("Cobson");
let r1 = &s;
println!("{r1}"); // last use of r1
let r2 = &mut s; // allowed now
r2.push_str(" will always be a gem!");
Reading input[edit | edit source]
To read input, use std::io::stdin() and call the read_line() function.
use std::io;
fn main() {
println!("What's your name, nusoi?");
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("Failed to read name!");
println!("{name}? That's a gemmy name.");
}
Thus, to create a star pyramid in Rust:
use std::io::{self, Write};
fn main() {
let mut input = String::new();
print!("Enter number of rows: ");
Write::flush(&mut io::stdout())
.unwrap();
io::stdin()
.read_line(&mut input)
.expect("Failed to read");
let rows: i32 = input.trim()
.parse()
.expect("Not a number!");
println!();
let mut i = rows;
while i > 0 {
for _ in 1..=(i / 2) {
print!(" ");
}
for _ in i..=rows {
print!("*");
}
println!();
i -= 2;
}
}
Functions[edit | edit source]
In Rust, a function is declared with the fn keyword. To return a value from a function immediately, use the return keyword, but if you are at the end of the function, you can omit the return and semicolon at the end (no semicolon indicates an expression, which is returned, while a present semicolon indicates a statement and thus returns nothing (()).
fn post_bait() {
println!("Trans rights are human rights!");
}
fn current_admin_number() -> i32 {
6
}
Structs[edit | edit source]
A struct is like what it is in C. It defines a custom aggregate data type.
struct Nusoi {
name: String,
years: u16,
}
let nate: Nusoi = Nusoi {
name: String::from("Nate Higgers"),
years: 1,
};
However, methods cannot be defined here. Instead, you must define them in a impl block:
impl Nusoi {
fn new(name: String, years: u16) -> Self {
Self { name, years }
}
fn participate_in_raids(&self) {
println!("OYYYY DOCTOS 'ox and 'ape this tranny!!!");
}
}
Traits[edit | edit source]
Rust does not support inheritance like in C++ or Java, however a trait behaves similar to an interface in Java: a set of methods that a struct implements.
trait Admin {
fn ban(&self, user: &Nusoi, reason: &str);
}
impl Admin for Nusoi {
fn ban(&self, user: &Nusoi, reason: &str) {
println!("Banning user {user.name} for reason: {reason}");
}
}
If we want to interact with types that implement a trait then you need to use either dynamic or static dispatch.
// STATIC DISPATCH // The compiler automatically generates code at compile time. It doesn't have a runtime cost but it will increase compile times if there is a lot of code depending on it. fn foo(admin: impl Admin) { /* ... */ }
// DYNAMIC DISPATCH // Values that implement a trait can be of any size. We cannot hold them on the stack so we need to box the values. This has a runtime cost because heap allocations are generally slower than stack allocations. fn bar(admin: Box<dyn Admin>) { /* ... */ }
Enums[edit | edit source]
Enums in Rust are similar to that of C:
enum Direction {
North,
South,
East,
West,
}
But, Rust enums are in fact much more powerful than in C. They can hold different data, and can be pattern-matched over with match:
enum Message {
Text(String),
Number(i32),
Quit,
}
let m1 = Message::Text(String::from("o algo"));
let m2 = Message::Number(42);
let m3 = Message::Quit;
fn handle_message(msg: Message) {
match msg {
Message::Text(s) => println!("Text: {s}"),
Message::Number(n) => println!("Number: {n}"),
Message::Quit => println!("Quit message"),
}
}
enums may also have struct-like variants:
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
}
let c = Shape::Circle { radius: 2.5 };
match c {
Shape::Circle { radius } => println!("Circle with radius {radius}"),
Shape::Rectangle { width, height } => {
println!("Rectangle {width} x {height}")
}
}
Furthermore, enums may also have methods with an impl block.
use std::f64::consts::PI;
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
}
}
}
Enums in Rust (or discriminated unions in language design) are basically unions with a variable holding the current state of the union to prevent invariants being violated. If you want to do it the C way for FFI or performance reasons you can use the union keyword:
// The size of this union is the size of the largest member. In this case, it would be u64 or 8 bytes. union MyUnion {
a: i32, b: u64, c: bool,}
let my_union = MyUnion { a: 0 }; let a = unsafe { my_union.b }; // Reading the fields of a union is always unsafe since we don't know what the current value might be.
Generics[edit | edit source]
A generic in Rust is basically what templates are in C++: they allow for flexible, reusable code by instantiating a different version of the function for each type.
use std::ops::{Add, Sub, Mul, Div};
fn identity<T>(value: T) -> T {
value
}
let x = identity(5); // T = i32
let y = identity("Cobson"); // T = &str
trait Number: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T> {}
impl<T> Number for T
where T: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T> {}
#[derive(Debug, Clone, Copy)]
struct Point<T: Number> {
x: T,
y: T,
}
Rust's generics require bounds to opt into certain features (like addition or subtraction) while C++ templates are duck-typed.
Lifetimes[edit | edit source]
In Rust, one can also use lifetime parameters to describe how long a reference is valid. This is in particular useful for preventing dangling references. A lifetime parameter is usually denoted with a lowercase letter and an apostrophe in front, and appears in the generics list (before the generics).
Consider the following code:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
This code is considered incorrect Rust, as the returned reference cannot be determined whether it comes from x or y, but the compiler must know how long the returned reference is guaranteed to be valid. To remedy this, we introduce a lifetime parameter 'a:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("Do nusois");
let s2 = String::from("really?");
let result = longest(&s1, &s2);
println!("{result}");
}
This lifetime parameter 'a indicates that both inputs must be valid for at least 'a, and that the returned reference is guaranteed to be valid for 'a; in other words, the return value lives as long as the shorter of the two input lifetimes.
However, lifetime annotations cannot extend lifetimes. Consider the following:
fn bad<'a>() -> &'a str {
let s = String::from("JSID quote!!!");
&s
}
This fails, as s is destroyed when the function returns; lifetime annotations can only describe lifetimes, but not create or extend them.
A struct which stores reference must declare lifetimes.
struct Nusoi<'a> {
name: &'a str,
years: u16,
}
fn main() {
let name = String::from("Nate");
let nate = Nusoi {
name: &name,
years: 1,
};
println!("{nate.name} is a nusoi who has been on the bald men with glasses website for {nate.years} year(s).");
}
In some cases, it is necessary to use multiple lifetime parameters, when references are unrelated.
fn first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
The return value is tied only to x, while y may have an entirely different lifetime.
Type generics and lifetime generics appear in the same place:
struct Holder<'a, T> {
value: &'a T,
}
impl<'a, T> Holder<'a, T> {
fn get(&self) -> &'a T {
self.value
}
}
fn choose<'a, T>(x: &'a T, y: &'a T, choose_first: bool) -> &'a T {
if choose_first {
x
} else {
y
}
}
fn main() {
let a = String::from("apple");
let b = String::from("banana");
let chosen = choose(&a, &b, true);
let holder = Holder {
value: chosen,
};
println!("{}", holder.get());
}
Macros[edit | edit source]
Macros are a way to generate code at compile time. They take a set of tokens and they output a set of tokens. Writing a macro in Rust is harder than a simple function but it might be required for certain tasks. This is why macro calls need to have a bang (!) at the end of them. For example, this tutorial has been using the println! macro to print formatted strings.
The simplest way to define a macro in Rust is with the macro_rules! keyword.
macro_rules! our_own_vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
let some_vec: Vec<i32> = our_own_vec![67, 43, 42, 19, 54];There are some tasks that even macro_rules! cannot handle. In those situations we need to write a procedural macro. While procedural macros are very complicated to fully write on the bald men with glasses wiki, the general explanation is that you write a function in a different crate that takes a token stream and outputs a token stream. Procedural macros can be used as regular function calls, attributes and derives.
// 1. Regular function calls - Please note that these are not functions since they can change the control flow of our application.some_macro!();
// 2. Attributes
- [super_cool_macro]
struct Foo;
// 3. Derives - You may have seen these before.
struct Bar;
- [derive(Default)]
Procedural macros can be very slow. Be careful with them since they will 'ape your compile times if you abuse them.