<
<
en
fi CN
en
fi CN

Catching bugs before even running the code – PART 1: How Rust allows programmers to write high performance code with safety guarantees

In this blog I will go through a couple of features of the Rust programming language and explain how the features encourage writing safe code and avoid some pitfalls that cause headaches even to experienced programmers in other programming languages.

ABOUT THE AUTHOR

My name is Otto, and I am a software developer who has worked different areas of the field such as mobile phone software development, back-end development and embedded systems development. In addition to Rust, I have mostly been programming in C and different flavors of C++ on different operating systems and hardware platforms. I joined Symbio in January 2021.

ABOUT THIS BLOG

In this blog I will go through a couple of features of the Rust programming language and explain how the features encourage writing safe code and avoid some pitfalls that cause headaches even to experienced programmers in other programming languages.

In the next part of the blog, I will present some suggestions what to consider when adopting Rust in a professional software engineering organization that already has code to maintain and new products already underway.

FIRST PART: ERROR HANDLING AND PATTERN MATCHING

Rust has many features to help detecting and avoiding programming errors that in many other languages would go unnoticed until they cause problems in production.

The language has a wide variety of use cases from embedded software development on bare metal, to web development with WebAssembly. In order to work with existing code, it is possible to use C language libraries from Rust code and it is possible to generate Rust code from C headers. Bindings to other languages exist as well. In this blog the scope is on how some of the Rust’s features make it possible to detect or avoid errors that would require more attention from the programmer in other programming languages. If the programmer follows the rules of the language, it is of course possible to write error free code in any language. I want to place emphasis on this blog how Rust’s rules in particular make the task easier for humans.

STRUCTURED ERROR HANDLING WITH RUST

One common source of errors is that when a part of the program raises an unexpected condition such as missing file or network unavailability, another part of the program that is supposed to handle the condition ignores it or terminates the operation instead of handling it gracefully. Rust has a dedicated operator “?” that propagates a possible error value from the function that is currently being run to the caller. Resources such as memory that are reserved when the error occurs are released before returning from the function. This is automatically handled by the Drop trait that works similarly to the “Resource Access Is Initialization” (RAII) pattern. 

Errors are strongly typed in Rust just like all other types as well. This means that sometimes it is needed to map error types from one type to another. For example, if your own function calls a standard library function that might return an “std::io::Error” but your own function returns “CustomError”, you need to provide mapping from “std::io::Error” to “CustomError”. This can be quite easily done with the map_err() function.

Error handling in Rust is built around enumerations, or enums, which are a powerful language feature when used in conjunction with pattern matching. In Rust the individual variants of the enum can have structure to them so that a single instance of the enum includes all the associated data. 

Result and Option types are enum types in the Rust Standard Library and very often used as return values of functions. The variants themselves include the associated data so that the “Ok” or “Err” variants itself contain the data about the result or the error. The types of the “Ok” or “Err” values are generic, resembling templates in C++ or generics in Java. Option is another enum that has two separate values, “Some” for values that exist and “None” representing absence of value. 

An important distinction to many other languages is that safe Rust has no null or undefined value for variables at all. In C an often-used idiom is to use a pointer to a value to represent an existing value and a null pointer to represent the absence of one. This means that unless unsafe code is used, null pointer related crashes are not possible in Rust.

In order to work with existing C code, it is possible to create instances of a null pointer, but that code will need to be placed inside an unsafe block.

The approach in safe Rust is less error prone: when an Option value is used it always needs to be explicitly checked whether the value is “Some” or “None”. To accomplish this, a match statement is often used. 

You can view the code in GitHub.

PATTERN MATCHING AND HOW TO USE IT

Pattern matching syntax in Rust is a powerful, concise and human-readable form of expression to handle results, optional types, other types of enumerations, complex numerical ranges and nested structures, just to mention a few typical use cases.

The match statement is used to compare a value to several different possible patterns and to branch the execution based on which pattern has matched. The compiler requires that in a match statement every possible value must be covered.

This is an important difference to constructs like switch-case in C and C++, where a default case can be omitted, and a value can pass through the construct without being handled at all. This may be intended by the programmer, but in Rust the compiler requires the programmer to be explicit about it and any value that has not been matched by any of the other patterns must be eventually handled in the “_” case.

SAFEGUARDS AND UNSAFE ACCESS

For lower-level access or performance reasons the standard safeguards in Rust can sometimes be too strict. Safeguards can be disabled when it is necessary, but the language requires the programmer to be explicit about it.

The inner value from an Option can be forced out, but the function calls that do that, unwrap() or expect(), make it explicit that if the value is not present, the program will terminate immediately with information about the cause. This is called a “panic” in Rust. Unsafe methods to handle system resources such as raw memory without safeguards is allowed, but these operations must be placed in unsafe blocks explicitly. If a serious error occurs while running a Rust program, the probable causes can be usually narrowed down to those parts of code that use methods that can panic or use unsafe language features.

NEXT EPISODE

In the next part of this blog, I will go through how memory and resource management, multithreading, and what good resources there are to learn Rust. I will also express my thoughts about how to take Rust in use in a software engineering organization.

Otto Harju, Software Engineer

18.05.2021 | Articles