Bugs that the Rust compiler catches for you

120
0
Bugs that the Rust compiler catches for you

I like constituents, because they are clever!!
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, // it’s clear that this field is optional
}
Uninitialized variables
Let’s say that we are processing users accounts:

type User struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Email string
}

func (app *App) CreateUser(email string) {
// …
now :=time.Now().UTC()

user :=User {
ID: uuid.New(),
CreatedAt: now,
UpdatedAt: now,
Email: email,
}
err=app.repository.CreateUser(app.db, user)
// …
}
Good, but now, we need to add the field AllowedStorage int64 to the User structure.

If we forget to update the CreateUser function, the compiler will still happily accept the code without any changes and use the default value of and int64: 0, which may not be what we want.

While the following Rust code

struct User {
id: uuid::Uuid,
created_at: DateTime,
updated_at: DateTime,
email: String,
allowed_storage: i64,
}

fn create_user(email: String)
let user=User {
id: uuid::new(),
created_at: now,
updated_at: now,
email: email,
// we forgot to update the function to initialize allowed_storage
};
}
produces a compile-time error, preventing us from shooting ourselves in the foot.

Unhandled exceptions and errors
It may sound stupid, but you can’t have unhandled exceptions if you don’t have exceptions…

panic!() exists in Rust, but that’s not how recoverable errors are handled.

Thus, by imposing the programmers to handle each and every error (or the compiler refuses to compile the program), all while providing good tools to handle errors (the Result enum and the ? operator), the Rust compiler helps to prevent most (if not all) errors related to error handling.

Data races
Thanks to the Sync and Send traits, Rust’s compiler can statically assert that no data race is going to happen.

How does it work? You can learn more in the good write-up by Jason McCampbell.

In Go, data streams are hidden behind the io.Writer interface. On one hand, it allows to simplify its usage. On the other hand, it can reserve some surprise when used with types we don’t expect to be a stream, bytes.Buffer for example.

And that’s exactly what happened to me a month ago: a bytes.Buffer was reused in a loop to render templates which leads the templates to be appended to the buffer instead of the buffer to be cleaned and reused.

It would have never happened in Rust as Streams are a very specific type and would never have been used in this situation.

Some Closing Thoughts
Are smart compilers the end of bugs and code reviews?

Of course not! But a strong type system, and the associated compiler are a weapon of choice for anyone who wants to drastically reduce the number of bugs in their software and make their users / customers happy.

Read More
Share this on knowasiak.com to discuss with people on this topicSign up on Knowasiak.com now if you’re not registered yet.

Charlie Layers
WRITTEN BY

Charlie Layers

Fill your life with experiences so you always have a great story to tellBio: About:

Leave a Reply

Your email address will not be published. Required fields are marked *