Memory Management
Rust has memory-safety to avoid a whole class of bugs related to memory access, and which end up being the source of many security vulnerabilities in software. However, Rust can guarantee memory-safety at compile-time; there is no run-time making checks. The one exception here is array bound checks that are done by the compiled code at run-time, be that the Rust compiler. It is possible to write unsafe code in Rust, and in fact, both languages even share the same keyword, literally unsafe
, to mark functions and blocks of code where memory-safety is no longer guaranteed.
Rust has no garbage collector (GC). All memory management is entirely the responsibility of the developer. That said, safe Rust has rules around ownership that ensure memory is freed as soon as it's no longer in use (e.g. when leaving the scope of a block or a function). The compiler does a tremendous job, through (compile-time) static analysis, of helping manage that memory through ownership rules. If violated, the compiler rejects the code with a compilation error.
In JavaScript, there is no concept of ownership of memory beyond the GC roots (static fields, local variables on a thread's stack, CPU registers, handles, etc.). It is the GC that walks from the roots during a collection to detemine all memory in use by following references and purging the rest. When designing types and writing code, a JavaScript developer can remain oblivious to ownership, memory management and even how the garbage collector works for the most part, except when performance-sensitive code requires paying attention to the amount and rate at which objects are being allocated on the heap. In contrast, Rust's ownership rules require the developer to explicitly think and express ownership at all times and it impacts everything from the design of functions, types, data structures to how the code is written. On top of that, Rust has strict rules about how data is used such that it can identify at compile-time, data race conditions as well as corruption issues (requiring thread-safety) that could potentially occur at run-time. This section will only focus on memory management and ownership.
There can only be one owner of some memory, be that on the stack or heap, backing a structure at any given time in Rust. The compiler assigns lifetimes and tracks ownership. It is possible to pass or yield ownership, which is called moving in Rust. These ideas are briefly illustrated in the example Rust code below:
#![allow(dead_code, unused_variables)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 12, y: 34 }; // point owned by a
let b = a; // b owns the point now
println!("{}, {}", a.x, a.y); // compiler error!
}
The first statement in main
will allocate Point
and that memory will be owned by a
. In the second statement, the ownership is moved from a
to b
and a
can no longer be used because it no longer owns anything or represents valid memory. The last statement that tries to print the fields of the point via a
will fail compilation. Suppose main
is fixed to read as follows:
fn main() {
let a = Point { x: 12, y: 34 }; // point owned by a
let b = a; // b owns the point now
println!("{}, {}", b.x, b.y); // ok, uses b
} // point behind b is dropped
Note that when main
exits, a
and b
will go out of scope. The memory behind b
will be released by virtue of the stack returning to its state prior to main
being called. In Rust, one says that the point behind b
was dropped. However, note that since a
yielded its ownership of the point to b
, there is nothing to drop when a
goes out of scope.
A struct
in Rust can define code to execute when an instance is dropped by implementing the Drop
trait.
Rust has the notion of a global lifetime denoted by 'static
, which is a reserved lifetime specifier. A very rough approximation in C# would be static read-only fields of types.
In JavaScript, references are shared freely without much thought so the idea of a single owner and yielding/moving ownership may seem very limiting in Rust, but it is possible to have shared ownership in Rust using the smart pointer type Rc
; it adds reference-counting. Each time the smart pointer is cloned, the reference count is incremented. When the clone drops, the reference count is decremented. The actual instance behind the smart pointer is dropped when the reference count reaches zero. These points are illustrated by the following examples that build on the previous:
#![allow(dead_code, unused_variables)]
use std::rc::Rc;
struct Point {
x: i32,
y: i32,
}
impl Drop for Point {
fn drop(&mut self) {
println!("Point dropped!");
}
}
fn main() {
let a = Rc::new(Point { x: 12, y: 34 });
let b = Rc::clone(&a); // share with b
println!("a = {}, {}", a.x, a.y); // okay to use a
println!("b = {}, {}", b.x, b.y);
}
// prints:
// a = 12, 34
// b = 12, 34
// Point dropped!
Note that:
-
Point
implements thedrop
method of theDrop
trait and prints a message when an instance of aPoint
is dropped. -
The point created in
main
is wrapped behind the smart pointerRc
and so the smart pointer owns the point and nota
. -
b
gets a clone of the smart pointer that effectively increments the reference count to 2. Unlike the earlier example, wherea
transferred its ownership of point tob
, botha
andb
own their own distinct clones of the smart pointer, so it is okay to continue to usea
andb
. -
The compiler will have determined that
a
andb
go out of scope at the end ofmain
and therefore injected calls to drop each. TheDrop
implementation ofRc
will decrement the reference count and also drop what it owns if the reference count has reached zero. When that happens, theDrop
implementation ofPoint
will print the message, “Point dropped!” The fact that the message is printed once demonstrates that only one point was created, shared and dropped.
Rc
is not thread-safe. For shared ownership in a multi-threaded program, the Rust standard library offers Arc
instead. The Rust language will prevent the use of Rc
across threads.
let stack_point = Point { x: 12, y: 34 };
let heap_point = Box::new(Point { x: 12, y: 34 });
Like Rc
and Arc
, Box
is a smart pointer, but unlike Rc
and Arc
, it exclusively owns the instance behind it. All of these smart pointers allocate an instance of their type argument T
on the heap.
The new
keyword in JavaScript creates an instance of a "type", and while members such as Box::new
and Rc::new
that you see in the examples may seem to have a similar purpose, new
has no special designation in Rust. It's merely a coventional name that is meant to denote a factory. In fact they are called associated functions of the type, which is Rust's way of saying static methods.