about
09/17/2022
RustBites - Safety
Rust Bite - Safety
Rust is safe by construction via compiler enforcements
1.0 Why Rust?
2.0 Memory Safety
- Can not construct uninitialized references.
-
Can not allow references to become invalid due to a reallocation of memory by the referend.
This could be caused by a vector, with a reference to its interior, reallocating to make room for additional data.
- References can not outlive instances they reference.
- Can not read from, or write to, memory accessed by indexing beyond the size of any indexable collection.
You will probably want to view "picture-in-picture" or "full-screen"
so details are large enough to be seen easily.
3.0 Rust Safety Invariants
- Safe referencing - No shared mutabilty through references.
- Safe indexing - All collections, including native arrays, are sized, and any attempt to index outside that sized area results in a panic, e.g., an orderly shutdown of the current thread before any reads or writes can complete.
4.0 Ownership - static analysis
- There is only one owner for every data item. That owner is responsible for deallocating the data when it goes out of scope. It does that with a drop operation, very like a C++ destructor invocation.
- Ownership can be transferred with move operations.
- Ownership can be borrowed by creating references.
- Any number of readers (immutable references) may have access to a data value simultaneously.
- Writers (mutable owner or mutable reference) get exclusive access to a value - no other readers or writers.
5.0 Non-lexical Filters - refining static analysis
- Calling a function with one or more immutable references following a mutable borrow will build successfully, since no data in the function can be mutated while the function is being invoked. The same is true of other types of expressions that take immutable references.
- Creating and using an immutable reference in the scope of a mutable reference will build if, and only if, the mutable reference is not used after declaration of the immutable reference.
6.0 Interior Mutability - invariant analysis at run-time
7.0 Evaluating Safety
- Much of the territory between the boundaries, i.e., region #2, is covered by Rust standard types, which use unsafe blocks that are very carefully vetted by the development team to be sound. So your program simply uses one or more of those types without directly using unsafe blocks. Consider the standard library's vector. Some of its operations clearly violate the scope-based rule about no shared mutability. It holds a reference to its internal memory, but will hand out a mutable reference to your program. That is safe because the vector (and compiler) conspire to manage change on your behalf by refusing to mutate when there are outstanding references. If you are curious you can look at unsafe blocks in vector from its source documentation. We use the vector, the compiler doesn't complain because of vector's few small well crafted unsafe blocks, and we benefit from the library developer's expertise. Seldom do you know your program is walking in the valley of "hard to evaluate region #2" You just use the standard library types and all is well.
- In some of that territory, program operations don't violate the invariants, but that can't be determined via static analysis, e.g., there is no simultaneous shared mutation using references because of the way the program operates in time. But that cannot be evaluated statically. For this case one may use "interior mutability", discussed in section 6.
- The design may be changed to avoid violating invariants, often by using indexing instead of references, like the directed graph class we mentioned above.
Productivity Tips
- Use the Rust library linked types (HashMap, linked_list, ...) instead of trying to craft your own.
- Find alternatives to custom linked data structures, as we did for the graph type discussed above. That will usually involve use of vectors.
- Pass arguments to functions by reference. If you pass by value the caller's argument will be moved into the function and become permanently invalid in the caller's scope.
- In methods of a type you've developed return a member value by reference. Otherwise it would be moved to the caller and the member becomes invalid. If returning by value is important for ergonomic reasons you can return a clone by value, incurring the performance penalty of making a clone.
- For all other functions, return a locally created object by value so the data is moved to the caller. That's fast and the returned data continues to be valid. In this case, Rust won't let you return by reference, citing lifetime issues.
7.0 References:
Reference | Description |
---|---|
Jon Gjengset | Considering Rust - why should you explore Rust? |
intorust.com | Excellent discussion of ownership and safety by an expert in five short videos. |
Temporarily opt-in to shared mutation | Blog by Alice Ryhl, a primary maintainer of the Tokio crate. |
Rust: Unsafe in Rust: Syntactic Patterns | Interesting analysis of unsafe in crates reported by Alex Ozdemir |
Rust: A unique perspective | Best description of Rust ownership and sharing that I've found. Check it out! |
RustTour.pdf | Quick tour of the Rust programming language emphasizing its unique attributes. |
Why Rust for safe systems programming - Microsoft | Microsoft is exploring Rust for system programming. |
The Rust Book - Ownership | Ownership rules with examples and discussion |
Rust edition-guide | Non-lexical lifetimes |
Rust Book | RefCell<T> and the Interior Mutability Pattern |