Bugs that the Rust compiler catches for you

45
[favorite_button]
Bugs that the Rust compiler catches for you
Advertisements

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.

Advertisements

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:

Advertisements

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:

Advertisements

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:

Advertisements

#[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:

Advertisements

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.

Advertisements

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…

Advertisements

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.

Advertisements

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?

Advertisements

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.

Advertisements
Charlie
WRITEN BY

Charlie

Fill your life with experiences so you always have a great story to tell
Get Connected!
One of the Biggest Social Platform for Entrepreneurs, College Students and all. Come and join our community. Expand your network and get to know new people!

Discussion(s)

No comments yet
Knowasiak We would like to show you notifications so you don't miss chats & status updates.
Dismiss
Allow Notifications