- Rust is a systems programming language that focuses on strong
compile-time correctnessguarantees - Strong memory guarantees make writing correct concurrent Rust code easier than in other languages
- It is focused on three goals: safety, speed, and concurrency. It maintains these goals without having a garbage collector
- Good interfacing with wasm
- No garbage collector
- Ownership
- The scope that will free the resource. Owned.
- Mutable Reference
- Can be only one. No one can Read and write. Exclusive access. No responsibility to free. Only borrowing it. Exclusive.
- Immutable Reference
- No modification. Multiple read. Shared.
// Wont work. Stack Allocated. Borrowed pointer will leave. Borrowed value will not.
fn dangling() -> &int {
let i = 1234;
return &i;
}
// Will Work. Heap Allocated.
fn dangling() -> ~int {
let i = ~1234;
return i;
}
fn add_one() -> int {
let num = dangling();
return *num + 1;
}// rust
let i = ~1234;
// C++
int *i = new int;
*i = 1234;
// The Rust compiler also figures out the lifetime of i, and then inserts a corresponding free call after it’s invalid, like a destructor in C++. You get all of the benefits of manually allocated heap memory without having to do all the bookkeeping yourself.
// All of this checking is done at compile time, so there’s no runtime overhead. You’ll get (basically) the exact same code that you’d get if you wrote the correct C++, but it’s impossible to write the incorrect version, thanks to the compiler
// Rust allows you to spin up ‘tasks,’ which are lightweight, ‘green’ threads. These tasks do not have any shared memory, and so, we communicate between tasks with a ‘channel’// Copying the vector. Not efficiecnt.
fn main() {
let numbers = [1,2,3];
let (port, chan) = Chan::new();
chan.send(numbers);
do spawn {
let numbers = port.recv();
println!("{:d}", numbers[0]);
}
}
// Sending Reference.
extern mod extra;
use extra::arc::Arc;
fn main() {
let numbers = [1,2,3];
// Arc stands for ‘atomically reference counted,’ and it’s a way to share immutable data between multiple tasks.
let numbers_arc = Arc::new(numbers);
for num in range(0, 3) {
let (port, chan) = Chan::new();
chan.send(numbers_arc.clone());
do spawn {
let local_arc = port.recv();
let task_numbers = local_arc.get();
println!("{:d}", task_numbers[num]);
}
}
}
// In case of mutating
extern mod extra;
use extra::arc::RWArc;
fn main() {
let numbers = [1,2,3];
let numbers_arc = RWArc::new(numbers);
for num in range(0, 3) {
let (port, chan) = Chan::new();
chan.send(numbers_arc.clone());
do spawn {
let local_arc = port.recv();
// Closures as arguemnt
// This acquire a mutex, and then pass the data to this closure. After the closure does its thing, the mutex is released
// You can see how this makes it impossible to mutate the state without remembering to aquire the lock. We gain the efficiency of shared mutable state, while retaining the safety of disallowing shared mutable state.
local_arc.write(|nums| {
nums[num] += 1
});
local_arc.read(|nums| {
println!("{:d}", nums[num]);
})
}
}
}- Rust maintains
Hygenic macro
Any borrow must last for a scope no greater than that of the owner. We may have one or the other of these two kinds of borrows, but not both at the same time:
- One or more references (&T) to a resource,
- Exactly one mutable reference (&mut T).
Borrow checker will check you have not used anything after you have gotten rid of it. Mutable access to something shared.
// Mutability is a property of either a borrow (&mut) or a binding (let mut). Mutability is a property of the binding, not of the structure itself.
- exterior mutability
- interior mutability
struct Point {
x: i32,
mut y: i32, // Not allowed.
}
// To achive field level mutability.
use std::cel~l::Cell;
struct Point {
x: i32,
y: Cell<i32>,
}
let point = Point { x: 5, y: Cell::new(6) };
point.y.set(7);
println!("y: {:?}", point.y);let Point(_, origin_y, origin_z) = origin;
struct Inches(i32);
let length = Inches(10);
let Inches(integer_length) = length;Also called tagged union
Match is also an expression, which means we can use it on the right-hand side of a let binding or directly where an expression is used:
let x = 5;
let number = match x {
1 => "one",
2 => "two",
3 => "three",
4 => "four",
5 => "five",
_ => "something else",
};let x = 1;
let c = 'c';
match c {
x => println!("x: {} c: {}", x, c),
}
println!("x: {}", x)
// This prints:
// x: c c: c
// x: 1Sometimes it’s a nice way of converting something from one type to another; in this example the integers are converted to String.
This ‘associated function’ builds a new Circle for us. Note that associated functions are called with the Struct::function() syntax, rather than the ref.method() syntax. Some other languages call associated functions ‘static methods’.
struct Circle {
x: f64,
y: f64,
radius: f64,
}
// normal methods
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
fn grow(&self, increment: f64) -> Circle {
Circle { x: self.x, y: self.y, radius: self.radius + increment }
}
}
fn main() {
let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
println!("{}", c.area());
let d = c.grow(2.0).area();
println!("{}", d);
}
// Asscoiated methods
impl Circle {
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
}A ‘string’ is a sequence of Unicode scalar values encoded as a stream of UTF-8 bytes.
let greeting = "Hello there."; // greeting: &'static strRun
// "Hello there." is a string literal and its type is &'static str. A string literal is a string slice that is statically allocated, meaning that it’s saved inside our compiled program, and exists for the entire duration it runs. The greeting binding is a reference to this statically allocated string. Any function expecting a string slice will also accept a string literal.
// Because strings are valid UTF-8, they do not support indexing:
let s = "hello";
println!("The first letter of s is {}", s[0]); // ERROR!!!Run
// Usually, access to a vector with [] is very fast. But, because each character in a UTF-8 encoded string can be multiple bytes, you have to walk over the string to find the nᵗʰ letter of a string. This is a significantly more expensive operation, and we don’t want to be misleading. Furthermore, ‘letter’ isn’t something defined in Unicode, exactly. We can choose to look at a string as individual bytes, or as codepoints:
let hachiko = "忠犬ハチ公";
for b in hachiko.as_bytes() {
print!("{}, ", b);
}
// 229, 191, 160, 231, 138, 172, 227, 131, 143, 227, 131, 129, 229, 133, 172,
for c in hachiko.chars() {
print!("{}, ", c);
}
println!("");
// 忠, 犬, ハ, チ, 公,Rust understands a few of these types, but they have some restrictions. There are three:
- We can only manipulate an instance of an unsized type via a pointer. An &[T] works fine, but a [T] does not.
- Variables and arguments cannot have dynamically sized types.
- Only the last field in a struct may have a dynamically sized type, the other fields must not. Enum variants must not have dynamically sized types as data.
enum Option<T> {
Some(T),
None,
}Trait contains method signature.
struct Circle {
x: f64,
y: f64,
radius: f64,
}
trait HasArea {
fn area(&self) -> f64;
fn is_larger(&self, &Self) -> bool;
}
impl HasArea for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
fn is_larger(&self, other: &Self) -> bool {
self.area() > other.area()
}
}
// with generics
fn print_area<T: HasArea>(shape: T) {
println!("This shape has an area of {}", shape.area());
}
trait Graph {
type N;
type E;
fn has_edge(&self, &Self::N, &Self::N) -> bool;
fn edges(&self, &Self::N) -> Vec<Self::E>;
}#[derive(Debug)]
struct Foo;
fn main() {
println!("{:?}", Foo);
}
// Clone
// Copy
// Debug
// Default
// Eq
// Hash
// Ord
// PartialEq
// PartialOrdstruct Firework {
strength: i32,
}
impl Drop for Firework {
fn drop(&mut self) {
println!("BOOM times {}!!!", self.strength);
}
}
fn main() {
let firecracker = Firework { strength: 1 };
let tnt = Firework { strength: 100 };
}
// BOOM times 100!!!
// BOOM times 1!!!
// Arc<T> type is a reference-counted type. When Drop is called, it will decrement the reference count, and if the total number of references is zero, will clean up the underlying value.When code involves polymorphism, there needs to be a mechanism to determine which specific version is actually run. This is called ‘dispatch’.
There are two major forms of dispatch:
- Static dispatch
- Dynamic dispatch
While Rust favors static dispatch, it also supports dynamic dispatch through a mechanism called ‘trait objects’.
fn do_something<T: Foo>(x: T) {
x.method();
}
// will be converted to this
fn do_something_u8(x: u8) {
x.method();
}
fn do_something_string(x: String) {
x.method();
}
// This has a great upside: static dispatch allows function calls to be inlined because the callee is known at compile time, and inlining is the key to good optimization. Static dispatch is fast, but it comes at a tradeoff: ‘code bloat’, due to many copies of the same function existing in the binary, one for each type.
// Furthermore, compilers aren’t perfect and may “optimize” code to become slower. For example, functions inlined too eagerly will bloat the instruction cache (cache rules everything around us). This is part of the reason that #[inline] and #[inline(always)] should be used carefully, and one reason why using a dynamic dispatch is sometimes more efficient.
// However, the common case is that it is more efficient to use static dispatch, and one can always have a thin statically-dispatched wrapper function that does a dynamic dispatch, but not vice versa, meaning static calls are more flexible. The standard library tries to be statically dispatched where possible for this reason.
// This operation can be seen as ‘erasing’ the compiler’s knowledge about the specific type of the pointer, and hence trait objects are sometimes referred to as ‘type erasure’.
// Dynamic dispatch
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = 5u8;
do_something(&x as &Foo);
}
// this comes at the cost of requiring slower virtual function calls, and effectively inhibiting any chance of inlining and related optimizations from occurring.
// The methods of the trait can be called on a trait object via a special record of function pointers traditionally called a ‘vtable’ // Covariance
// any lifetime can be treated as static
let x: &'static str // more useful
let a: &'a str
a = x
// Contravariance
fn foo(&'a str) { // less usefull
}
fn foo(&'static str) { // less usefull
}
// Invariance
fn foo(s: &mut &'a str, a: &'a str) {
}
let mut x = ""
foo(x, "" )
use std::marker::PhantomData;
struct Deserializer<T> {
_t: PhantomData<T>,
}
// Mutable references are invariant in the type they point to,
// and covariant in their lifetime- When a type T implements Send, it indicates that something of this type is able to have ownership transferred safely between threads.
Other Points
- Static mut is unsafe, and so must be done in an unsafe block.
- Any type stored in a static must be Sync, and must not have a Drop implementation.
- They follow the “read-write lock” pattern, such that one may either have only one mutable reference to some data, or any number of immutable ones, but not both.
- Mutex gives exclusive or mutable access to a key through a shared reference to mutex
- R/W lock, mutex either have lock for multiple read or single write
- Locks provide safe wrapper around aliased or shared pointer.
- Learning curve is borrow checking
- Carriage return line feed used in windows, linux uses line feed.
- Calling rust has no abstraction like cGo, JNI.
- Runtime programs
- Stuffs that your programming language puts that you dont write.
- Tokio core is event loop of tokio
#[cfg(foo)]
// in toml define oneCargo is Rust’s build system and package manager, and Rustaceans use Cargo to manage their Rust projects. crate - library - holds module
cargo init
cargo new crate_nameincomplete Trait objects - has lackings, read again Closures