Rust
18 min readUnfortunately am into this rust thing now.. there is no going back, lets learn this mf.
Intro | notes to me#
Rust is a compiled programming language like go | C | C++, to add more in rust memory management is manual unlike
go | java | js etcc, where you will get automatic GC on ur runtime.
In Rust there is a in house tool called cargo, which is the dependency manager, similar to npm in js, cargo new project
creates a new project with directories src/main.rs, cargo.toml files, cargo.toml files contains the project
information like project_name, dependencies needed etc.
- cargo build - builds your program and puts the executable under
target/debuge/*. - cargo check - builds your program and checks if there are any issues, but it won’t produce any binary/executable so its fast compared to build.
- cargo run - builds and produce an executable and also runs the executable for us, similar to
go run main.go
fn hello_world() {
println!("Hello World!");
}
fn main() {
hello_world();
}
Above is the basic Hello world program in rust, as you can see in rust function is declared using fn keyword,
one interesting thing i found is instead of normal println here we are calling println!, the !
means we are calling a macro(kinda c macro) not a normal function.
Crate#
Crates in rust are kinda of dependencies/npm packages in binary or library, that we can use in our application, these can be downloaded using cargo(package manager).
Variables and Mutability#
Any Variables that u declare in rust, by default it is immutable, this has lot of advantages.
- Safety, devs will know for sure this variable will not be changed for any reason.
- Easier for compilers to do optimization, since we know the exact size and the reason that its not going to change.
- Also used for concurrency, many threads can easily read the same variable, coz no one can edit it.
with the above advantages, of course we can define mutable variables as well with the keyword mut.
fn main() {
// let name = "Manikandan";
// println!("Name => {name}");
// name = "Manikandan Arjunan"; // this is not possible in rust by default, since it is immutable, we need
// to add mut in line 2 variable declaration
let mut name = "Manikandan";
println!("Name initialize => {name}");
name = "Manikandan Arjunan"; // this is not possible in rust by default, since it is immutable, we need
println!("Name after update => {name}");
}
Constants#
Constants are similar to variables but will have immutable by default and cannot be changed.
Nthg complex, just same constant that we learned in JS, C, Go etc, mut is not allowed to declare when using
constants.
Shadowing#
Shadowing is basically redeclaring the same variable again, something like below, i guess
rust has this feature so that devs can feel the immutability without using the mut keyword.
not sure this is basically breaking the fundamental point that rust is solving => optimization.
fn main() {
let age = 12;
println!("Before shadowing: {age}");
let age = "Manikandan"; //invalid i know, but this is shadowing, technically
// i can declare same variable again either with same type or different
println!("After shadowing: {age}");
}
NOTE:
- In shadowing we can literally change types as u can see above.
- However if u declare a variable using mut
let mut name = "Manikandan", u can’t change the type of name here.
Datatypes#
Rust is a strongly typed language, meaning the compiler should know the type of any variable, usually rust compiler knows a variable type by its value or the type that is defined(this is needed, when u convert one variable to another etc)
fn main() {
// explicit typing, since we are converting from one to another.
let age: u16 = "27".parse().expect("Not a age");
// implicit typing, rust compiler will know the type name on compile time.
let name = "Manikandan Arjunan";
println!("{age} {name}");
}
Types of datatypes in rust:
Scalar - These are individual types which represent a single value or fixed size value like
Integer,Float,Bool,Char- Integer - Everyone knows this,
Integeris a type which includes only integers(no fractions), it also hassigned(+ve and -ve) andunsigned(only +ve), Below are the different integer types that rust has, by default if u do something likelet age = 21;, by default rust compiler allocatesi32
- Floating Point -
Floatis basically fractional/decimal numbers. - Boolean -
Booleanrepresentstrue|false.
let t = true; // implicit compiler knows the type let f: bool = false; // with explicit type annotation.- Boolean -
Booleanrepresentstrue|false. - Char -
charrepresents a single character which is of 4bytes, unlike 1 byte in c language, this is coz it supports more characters like ascii, emoji, japanese etc..
- Integer - Everyone knows this,
Compound - Compound types are basically a group of values with one specific types, EG: Array, objects etc.
- Tuple - Tuple type is basically a grouping number of values with variety of types, kinda array but not.
fn main() { let tup1: (i16, char, bool) = (2, 'M', false); let (x, y, z) = tup1; // kinda destructuring in compare with js world. println!("{x} {y} {z}"); println!("{}", tup1.0); // this is also possible }- Array - As we know
arraytype is basically collection of values but with same type(unlike other dynamic language), also in rust array size is fixed by default.
fn main() { let arr: [i16; 5] = [1, 2, 3, 5, 6]; // this is one way of declaring array [i16-> type; 5 -> // size] let arr = [1, 2, 3, 5, 6]; // this is another way of declaring array, where the size // and type are determined by the compiler. let arr = [3; 5]; // this is another way of initializing where if u want to repeat a // specific number with n times, in this case number 3 for 5times. println!("{}", arr[0]); }
Functions#
Functions are similar to everyother programming language, with some minor tweaks, mentioned in the below code
// params as usual specify type, for return type use -> arrow operator
// fn add(x: i16, y: i16) -> i16 {
// return x + y;
// }
// if u see below i have not included semi colon, this is intentional
// x + y is basically an expression so it returns(similar to () => 1 in js)
// if u include semi colon x + y; this becomes statement, so rust returns default
// value () empty tuple
fn add(x: i16, y: i16) -> i16 {
x + y
}
// fn is the keyword, main is the default function that rust calls automatically on any file
fn main() {
add(1, 2);
println!("Hello, world!");
}
Ownership#
Ownership is pretty important concept in rust, this basically explains on how rust deals with memory
management without Garbage Collector or managing manually.
Generally in programming languages there are two ways to manage memory, one is through garbage collection(
JS, Python, Golang etc), another one is through manual(C, C++ using malloc, calloc, free etc),
but rust deals with managing memory in a brand new way called Ownership/Borrowing.
Ownership => A set of rules that the compiler check, if any of those rules are violated, program won’t even compile basically.
Before going into this Ownership, lets understand some fundamental things about stack and heap,
understanding this is important to understand rust model of managing memory.
Stack and heap:
- Stack: Stack is a fixed size of memory that each programming
language(program/application) gets when starts.
- Go: 2KB - 1GB as it grows.
- C, Rust: 8MB
- Javascript: 1 - 2MB
Whenever you write a code and compile it(even js compiles), ur actual code(binary | actual in case of js)
will stays under text | code memory, all your fixed size variables like int, char, etc stored under your stack memory.
Consider the below C code
void callFun2() {
int age = 30;
return;
}
void callFun1() {
int age = 20;
callFun2();
}
int main (){
int age = 10;
callFun1();
return 0;
}
Once u compile and executes(the binary) the above c code, the first function main get pushed into the stack and all its local variables,
inside the main we are calling a function called callFun1, now this callFun1 is pushed on top of stack and its variables
now callFun2 is pushed coz its been called inside the callFun1, once callFun2 is executed
it gets popped off and returned to the calling function(in our case callFun1) using return address and this process repeats
untill its return to the OS(main function returns to OS).
Now you may have a question, whats the limit for this stack size, is it infinite or whole computer's memory or what?
the answer is each programming language gets a fixed stack memory size(these are contiguous) for each program/code that you
executes, whenever you go beyond these language specific memory stack size, you generally get `stack overflow` errors
This usually happens if u have tooo much nested calls and lot of variables(typically seen in non-exited recursive
function). Below are some of the stack sizes of popular programming languages:
- Go: starts from 2KB - 1GB(meaning initially it has 2KB, based on the function calls, compiler scales the stack memory).
- C, Rust: 8MB
- Javascript: 1-2MB
NOTE: All your source code and other stuff lives in different area called `text/code memory`.
in stack only ur function return address variables etc are stored. thats why this MB or KB level
of memory is more than enough for any level of programs.
Another NOTE: Since these stack memory are contiguous, its way faster to lookup and clear this memory.
for eg: lets say u wrote a C code and executing it, ur program will get 8MB of contiguous memory not random.
There is no need for anyone to look after the things(variables) to delete when its not getting used.
Below is minimal stack representation for the above C code.

Fun Fact: There are things like "Closures" in javascript or go or even rust, stores some things in heap
instead of stack, even for primitive variables. Eg: below
function a () {
let x = 0 // closure stored in heap, instead of stack
return (y) => {
return x + y
}
}
// go
func temp() func(int) int {
x := 0
return func(y int) {
return x + y
}
}
- heap: Heap is used to store dynamic/unknown/growing things, which u can't know the exact memory size on
whether it grows or shrinks, eg: dynamic arrays(slices, vectors), hashmap, objects etc.
Its difficult to clean heap rather than stack, coz heap memory is random, OS gives u the free
memory address and its the application responsibility to use it and clean,
in stack when the function popped off, all its things will be popped off(cleared), but
in heap, u need to manually clear from your application end.
C - Its manual, `free` will free the heap memory, malloc, calloc etc gives the memory from heap via OS.
Go, JS, Python etc - Its automatic, the compiler or the runtime has something called "Garbage Collector",
which automatically cleans and provides you the memory, so your application don't need to maintain anything
manually.
The job of this Garbage Collector is to keep track of all the things that are in the heap memory
for a particular program(process) and then clears periodically
if its not been used/referenced by anything in that program.
From the above understanding on stack and heap, we can conclude that in any programming language, u only need to
concentrate on things to be cleared that are in the heap not on stack, stack memory gets popped off
automatically when function complete its execution, for heap, either it should be manual or
language needs to implement something called Garbage Collector and include it in the language runtime.
lets proceed with rust ownership now. the basic rules of rust ownership are
- Each and every value in rust has an owner.
- There can be only one owner at a time.
- When owner goes out of scope, the value will be dropped.
Lets take string datatype in rust to illustrate this ownership example, coz strings are stored on heap
since we don’t know the exact size of those datatypes, btw when i meant string datatype, i was referring
to String namespace declaration not by using literals(these are fixed size and can live in stack), EG Below
let str = "Manikandan"; // this is a string literal and it literally has 4 * 10 bytes of memory.
let str2 = String::from("Manikandan"); // this is string will be stored under heap, where String
// is kinda namespace from standard libray, from that we are using a function called from.
// Since in string literals, we know the exact bytes at compile time, so we can stuff this into our
// stack memory itself, we don't need heap memory for this.
Consider the below code
fn main() {
let str = String::from("Manikandan")
println!(str)
}
If u see the above code, when u declare str, its memory is allocated to heap, and its our responsibility to clean
our means that application owner(manual) or the language(using GC or anyother way)..
In rust this is done something by a special function call drop, rust compiler
will automatically call this drop function, whenever any heap variable goes
out of scope, in the above scenario variable str goes out of scope as soon the function main() } ends
after this rust automatically calls drop(&str) for us at compile time.
TL;DR: Basically rust compiler will insert a code drop(&var) during the compilation time
on every heap variable exactly above the line where it considers that variable will go
out of scope on the next line.
Now consider the below example
fn main() {
let str = String::from("Manikandan")
{
let str2 = String::from("Arjunan")
println!(str)
println!(str2)
}
println!(str)
println!(str2) // error during compile time
}
in the above example, if u see the last line println!(str2), actually this won’t or shouldn’t work
the variable str2 is declared inside the curly braces {} scope, once rust compiler sees the }
it immediately calls drop(&str2), so the last line is not even possible.
Another example
1: fn main() {
2: let str = String::from("Manikandan")
3: {
4: let str2 = str
5: println!(str) // error, coz rust compiler calls drop(&str) before this line, since the ownership
// of str is transferred to str2.
6: println!(str2)
7: }
8: println!(str) // error
9:}
in the above example, we are defining a string str and then assinging that to str2 on line 4, since str
is not a primitive datatype, copying str to another like this way str2 = str, will only copyies its pointer,
as all languages this won’t copy the exact value, instead this copies the pointer from str and points to str2,
the same happens to rust as well, but the only key difference in rust this type of copy is called moving the ownership,
as soon as the rust compiler sees any ownership has been transferred to another in our case str2 = str,
it immediately drops the memory of str drops(&str),so the line println!(str) at
the last will throw error at compile time.
fn main() {
let str = String::from("Manikandan")
{
let str2 = str.clone()
println!(str)
println!(str2)
}
println!(str)
}
the above code, works, coz .clone() basically will copy the exact data(“Manikandan” in our case) and puts
it into str2, so rust compiler doesn’t need to call drop(&str) after the line str2 = str.clone(), coz
there is no ownership transfer
Another example
fn main() {
let str = String::from("Manikandan"); // str is declared and allocated in heap
takes_ownership(str); // passing(in rust terms "moving") the value to takes_ownership function
// str gets dropped
println!(str) // error
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
}
in the above one, you can see we are basically passing str from main to takes_ownership function
basically moving the str from main to takes_ownership, as soon as we call takes_ownership,
the rust compiler decides the str ownership now has been transferred from main to takes_ownership function
after the function call takes_ownership(str) the str would be dropped by rust, after this line takes_ownership(str)
you can’t literally use str in main function.
If you want to use that str again in ur main function, either u need to return the moved str from the takes_ownership
as i shown in the below code, but this will be an headache, coz u dont want to pass and return everytime,
there are ways to avoid this, which we can see later below.
fn main () {
let str = string::from("Manikandan"); // str is allocated to heap
let str = takes_ownership(str); // the above str will be dropped and a new str will be allocated from the
// return value of "takes_ownership", since rust have the ability to create same variable name its possible
// to bind in str itself
println!(str)
}
fn takes_ownership(some_string: string) -> string {
return some_string
}
more example comparing rust and other languages
fn main() {
let str = String::from("Manikandan"); // str is declared and allocated in heap
takes_ownership(str); // passing(in rust terms "moving") the value to takes_ownership function
// str gets dropped
println!(str) // error
}
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
}
package main
import "fmt"
func main() {
custom_map := make(map[string]int)
custom_map["age"] = 1
print_map(custom_map) //above custom_map won't be dropped immediately coz it is available on
// below line if u see, there is a dedicated GC on every go run time binary
// which tracks and delete if its not referenced by any, same follows for JS, python
fmt.Println(custom_map)
}
func print_map(cus_map map[string]int) {
fmt.Println(cus_map)
}
From the above example even rust could implement the same as go, but it comes with a cost, u need to implement ur own language garbage collector, which u need to attach on ur every binary executable etc. to avoid these and also to avoid manual memory management(which is difficult if the program grows), rust tried to comes up with a brand new way of managing/maintaining the memory. imo its amazing for me Rust is the first language to come up with this kinda of solution to manage memory.
References#
If you want to use that
stragain in ur main function, either u need to return the movedstrfrom thetakes_ownershipas i shown in the below code, but this will be an headache, coz u dont want to pass and return everytime, there are ways to avoid this, which we can see later below
as this stated above, in order to prevent this, we can also pass a variable with a reference(borrowing), instead of
moving that variable, we can pass the reference.
NOTE: if u pass the reference, rust compiler won’t call drop, coz the ownership is still with u. EG: below
Some borrowing rules:
- You cannot create more than one mutable reference/borrow.
- You can create multiple immutable reference/borrows.
- You cannot create one mutable and another immutable reference/borrow.
fn main() {
let str = String::from("Manikandan");
print_name(&str); // str won't drop, coz u are moving only the reference
// so rust won't need to drop str, basically borrowing
println!(str);
}
fn print_name(str: &String) {
println!("{*str}") // even if u use println("{str}"), rust still prints
// Manikandan, its coz prinln! macro implemented in a way to dereference and print
// if its a reference
}
Use-cases on when this references(borrowing) will be useful
case 1:
fn main() {
let str = String::from("Manikandan");
print_name(&str);
println!(str);
}
fn print_name(str: &String) {
println!("{str}")
}
if u want to pass ur variable to a function only to read, in this case u don’t need to transfer the ownership, these cases are exactly suitable to use borrow instead of transferring the owner.
case 2:
fn main() {
let mut str = String::from("Manikandan");
println!(str); // Manikandan
modify_str(&mut str);
println!(str); // Manikandan Arjunan
}
fn modify_str(str: &mut String) {
if str == "Manikandan" {
str.push_str(" Arjunan")
}
}
if u want to pass ur variable to a function only to read and also to modify, these type of cases u can transfer a mutable reference so that the external function can read and do modifications
NOTE: you can only create single mutable reference, rust is deliberately did this at compile time, to avoid race conditions(where multiple mutable varibles modify the same stuff), however u can create n number(immutable/normal references).
Like the above there are many cases
- if u want to share read only access to many external functions.
- to avoid copying/moving large string. ……
Slice Type#
We have talked about string literals let name = "Manikandan" many times above, basically
all the string literals are one of slices and it has a type called &str string literals
are immutable and also &str type is also immutable.
- String == &str, all String types when we dereference(
&) it will match&strtype, these are all immutable.
fn first_word(words: &str) -> &str {
let bytes = words.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b' ' {
return "Hello";
}
}
return "Manikandan";
}
fn main() {
let mut str = String::from("Hello HiHas");
let word = first_word(&str);
str.clear(); // Error
println!("{}", word);
}
In the above example if u look, we are passing &str(which is the reference of our str String::from(“hello ..”))
into first_word and the return type of first_word is also &str, in other programming languages its basically means
input &str is different return type of first_word is different reference, but in rust its not,
rust compiler automatically thinks the input to first_word is an immutable reference to str(String::from()),
and the return type of first_word also is an immutable reference of str(String::from()),
even though am returning Manikandan | Hello, rust compiler doesn’t care.
coz of this we are again calling str.clear() clear basically take mutable reference, which exploits our
borrowing rule, where one mutable and one immutable reference is not possible.
TL;DR - Rust basically checks the function signature, and notice that u are passing a reference and returning the same reference, so the function signature return value is an immutable reference.
fn first_word(words: &str) -> u16 {
let bytes = words.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b' ' {
return 1;
}
}
return 2;
}
fn main() {
let mut str = String::from("Hello HiHas");
let word = first_word(&str);
str.clear(); // no Error
println!("{}", word);
}
no error on above coz the rust looks the function signature fn first_word(words: &str) -> u16,
it sees ok return type is u16, even though input param is taking reference of String, as soon as the function
exits reference of String is of no use, so as soon as this line is executed first_woord(&str),
rust immediately knows &str(immutable String reference) is no longer used.