Over the decades, Humans have proved to be pretty bad at producing bug-free software. Trying to apply our approximative, fuzzy thoughts to perfectly logical computers seems doomed.
While the practice of code reviews is increasing, especially with the culture of Open Source becoming dominant, the situation is still far from perfect: it costs a lot of time and thus money.
What if, instead, we could have a companion, always available, never tired, and the icing on the cake, that doesn’t cost the salaray of a developer that would help us avoid bugs in our software before they reach production?
Let’s see how a modern compiler and type system helps prevent many bugs and thus helps increase the security for everyone and reduces the costs of software production and maintenance.
Resources leaks
It’s so easy to forget to close a file or a connection:
resp, err := http.Get("http://kerkour.com")
if err != nil {
// ...
}
// defer resp.Body.Close() // DON'T forget this line
On the other hand, Rust enforces RAII (Resource Acquisition Is Initialization) which makes it close to impossible to leak resources: they automatically close when they are dropped.
let wordlist_file = File::open("wordlist.txt")?;
// do something...
// we don't need to close wordlist_file
// it will be closed when the variable goes out of scope
Unreleased mutexes
Take a look at this Go code:
type App struct {
mutex sync.Mutex
data map[string]string
}
func (app *App) DoSomething(input string) {
app.mutex.Lock()
defer app.mutex.Unlock()
// do something with data and input
}
So far, so good. but when we want to process many items, things can go very bad fast
func (app *App) DoManyThings(input []string) {
for _, item := range input {
app.mutex.Lock()
defer app.mutex.Unlock()
// do something with data and item
}
}
We just created a deadlock because the mutex lock is not released when expected but at the end of the function.
In the same way, RAII in Rust helps to prevent unreleased mutexes:
for item in input {
let _guard = mutex.lock().expect("locking mutex");
// do something
// mutex is released here as _guard is dropped
}
Missing switch cases
Let’s imagine we are tracking the status of a product on an online shop:
const (
StatusUnknown Status = 0
StatusDraft Status = 1
StatusPublished Status = 2
)
switch status {
case StatusUnknown:
// ...
case StatusDraft:
// ...
case StatusPublished:
// ...
}
But then, if we add the StatusArchived Status = 3
variant and forget to update this switch
statement, the compiler still happily accepts the program and lets us introduce a bug.
While in Rust, a non-exhaustive match
produces a compile-time error:
#[derive(Debug, Clone, Copy)]
enum Platform {
Linux,
MacOS,
Windows,
Unknown,
}
impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Platform::Linux => write!(f, "Linux"),
Platform::Macos => write!(f, "macOS"),
// Compile time error! We forgot Windows and Unknown
}
}
}
Invalid pointer dereference
As far as I know, it’s not possible to create a reference to an invalid address in safe Rust.
type User struct {
// ...
Foo *Bar // is it intended to be used a a pointer, or as an optional field?
}
And even better, because Rust has the Option
enum, you don’t have to use null
pointer to represent the absence of something.
struct User {
// ...
foor: Option<Bar>, // it's clear that this field is optional
}
Uninitialized variables
Let’s say that we are processing users accounts:
type User struct {
ID uu