Memory
Stack
Special region of the process memory that stores variables created by each function.
For every function call a new stack frame is allocated on top of the current one.
The size of every variable on the stack has to be known at compile time.
When a function exists its stack frame is released.
The stack is limited in size.
Stack frame is the amount of memory allocated to a function, which is used to store all of the local variables and function parameters.
In the example below the stack frame will be large enough to store the two int
values and the single float32
type. Once the main
has exited, the stack frame allocated on entry will be released.
fn main()
{
let a = 10;
let b = 20;
let pi = 3.14f32;
}
Stack overflow is achieved when we run out of stack space by allocating stack frames.
Heap
Region of the process memory that is NOT automatically managed.
The heap has no size restrictions, it’s only limited by the system’s resources.
The heap is is accessible by any function, anywhere in the program.
Heap allocations are expensive and slow. Avoid them when possible.
Smart Pointers
Wrapper around the raw Pointer
adding additional capabilities to it.
Common smart pointers in the standard library:
Box<T>
for allocating values on the heapRc<T>
, a reference counting type that enables multiple ownershipRef<T>
andRefMut<T>
, accessed throughRefCell<T>
, a type that enforces the borrowing rules at runtime instead of compile time
Data Types
- Numbers:
u8
,u16
,u32
,u64
,u128
- unsigned integeri8
,i16
,i32
,i64
,i128
- signed integersusize
/isize
: unsigned/signed pointer-sized integers, thus its actual size depends on the architecture your are compiling your program forf32
: float numbersf64
: float with double precision
- Boolean:
bool
- Single Unicode value:
char
(always 4 bytes)
Functions
Function parameters must have their typed specified.
Functions must have their return type specified.
The last line without a semicolon at the end is considered the return
expression.
Example:
fn calculate_weight_on_mars(weight: f32) -> f32{
weight // same as return weight;
}
Macros
In short, rust code that generates more rust code. Quite powerful, but very complex and usually hard to maintain.
To distinguish a macro from a function call, look for the !
👀
Example:
fn main() {
println!("Hello world! From a macro.")
}
Mutability
Variables in Rust are immutable by default.
To make a variable mutable, use the keyword mut
:
Example:
let mut weight = 51.14;
Ownership
Rules of ownership, concept absent from most programming languages:
- Each value in Rust is owned by a variable
- When the owner goes out of scope, the value will be deallocated
- There can only be ONE owner at a time
Example:
fn main() {
let input: String = String::from("test");
some_fn(input);
println!("Input is: {}", input);
}
fn some_fn(s: String) {
s;
}
The value of input
is not available to the println!
macro, because its ownership is transfered to s
(of some_fn
) and when s
goes out of scope it’s deallocated and so is the value of input
.
References & Borrowing
Improving on the example above, we can actually pass input
to some_fn
without losing its value, by telling Rust we are going to pass it by reference:
fn main() {
let input: String = String::from("test");
some_fn(&input);
println!("Input is: {}", input);
}
fn some_fn(s: &String) {
s;
}
However s
in some_fn
is immutable and if we want to update its value, the compiler won’t let us.
To change that behaviour, we need not only to tell that we are passing reference to some_fn
, but also tell it’s going to be a mutable reference using &mut
.
Final code:
fn main() {
let mut input: String = String::from("test");
some_fn(&mut input);
println!("Input is: {}", input);
}
fn some_fn(s: &mut String) {
s.push_str("!");
}
In the same scope, we can have only one mutable borrow or as many as we want immutable borrows.
The compiler won’t let us have an immutable and mutable borrow at the same time.