Tech News

Rust Programming Language: Macros – Introduction to a must-have tool

In Rust, macros are a powerful tool that cannot be compared to the simple text replacements in C/C++ macros. A plea for Rust macros.

 

Anyone who takes their first steps in the Rust programming language and generates the classic “Hello World” program in the process will see the first and only line of the main-Function println!("Hello, world!"). The exclamation mark after the is striking println-Command. It marks a macro in Rust, to be precise: a function-like declarative macro. There are other forms of macros in Rust. However, this article focuses on the function-like, declarative macros, which can be recognized by the exclamation mark after the macro name.

 

 

 

 

Young professionals write for young professionals

 


This post is part of a series of articles to which voonze Developer invites young developers – to provide information about current trends, developments and personal experiences. The “Young Professionals” series is published monthly. Are you a “Young Professional” yourself and want to write a (first) article? Send your suggestion to the editors: developer@voonze.de. We’re here to help you write.

  • Alpine.js: The Swiss army knife for dynamic web interfaces
  • Developer Experience: Happy developers write better code
  • Ethics and artificial intelligence: A new approach to AI systems
  • All articles in the series can be found in the “Young Professionals” section

 

 


In this column, the two Rust experts Rainer Stropek and Stefan Baumgartner would like to take turns reporting regularly on innovations and background information in the Rust area. It helps teams already using Rust stay current. Beginners get deeper insights into how Rust works through the Ferris Talks.

  • Ferris Talk #1: Iterators in Rust
  • Ferris Talk #2: Abstractions Without Overhead – Traits in Rust
  • Ferris Talk #3: New Rust 2021 Edition is here – Closures, the Cinderella feature
  • Ferris Talk #4: Asynchronous Programming in Rust
  • Ferris Talk #5: Tokyo as an asynchronous runtime environment is an almost all-rounder
  • Ferris Talk #6: A new trick for the format strings in Rust
  • Ferris Talk #7: From Behemoth to Gold Rose – a little Rust refactoring story
  • Ferris Talk #8: Wasm loves Rust – WebAssembly and Rust beyond the browser
  • Ferris Talk #9: The Builder Pattern and Other Typestate Adventures
  • Ferris Talk #10: Constant Fun with Rust
  • Ferris Talk #11: Memory Management – Memory management in Rust with ownership

 

The fact that the “Hello World” program in Rust already contains a macro call is characteristic of Rust. Macros are ubiquitous in this programming language. You can find them in the Rust standard library as well as in virtually all application frameworks. Macros save developers a lot of typing by generating code. Used well, they improve development productivity and lead to more readable and maintainable code.

Developers who have programming experience in C or C++ are often not happy when they hear that Rust relies heavily on macros. The reason for this is that while macros in C can be useful in certain cases, they are error-prone. However, concerns about macros are unfounded with Rust. To make this clear, here’s a quick look at the history of macros in C. Macros in C are preprocessor directives. Before the compiler translates the code, macros are expanded with simple search-replace logic, as shown in Listing 1:

 

#define PI 3.14159265358979323846
#define CIRCLE_AREA(r) PI * r * r

int main(void) {
    int radius = 5;
    int area = CIRCLE_AREA(radius);
    printf("Radius: %d\nArea: %d\n", radius, area);
    return 0;
}

 

Listing 1: First, simple C macros

In the code example in Listing 1, the constant PI is first defined as a macro (meaning the number π here). The second line is followed by a macro that is already a bit more complex. It takes one parameter to calculate the area of ​​a circle using the constant PI defined earlier. the mainmethod contains the call to the macro. The C preprocessor will resolve the macros by simple text replacement before compiling.

At first glance, there is no apparent problem in the code example above. But what happens when you call the macro CIRCLE_AREA changed on double area = CIRCLE_AREA(radius + 2)? Since the C compiler resolves the macros by text replacement, it becomes double area = PI * radius + 2 * radius + 2. It is now clear that the formula for calculating the area of ​​a circle is no longer correct.

C developers solve the problem by using parentheses. In the present example, the macro could be changed to: #define CIRCLE_AREA(r) PI * (r) * (r). The parentheses are taken into account when resolving the macro that is the result after running the preprocessor double area = PI * (radius + 2) * (radius + 2) – and that’s right. Listing 2 provides another example to illustrate the problem.

 

#define SUM(a, b) (a) + (b)

int main(void) {
    printf("%d\n", SUM(1, 2) * 2); // Result is 5 instead of 6
    return 0;
}

 

Listing 2: C macro leads to incorrect calculation

If you run the C program in Listing 2, you might be surprised to find that the result is 5 when you expected 6. This is again due to the brackets. The formula SUM(1, 2) * 2 will be expanded to (1) + (2) * 2. The problem can be solved again by adding parentheses: #define SUM(a, b) ((a) + (b)) leads to the right result.

Here at the latest it becomes clear that there can be subtle sources of error hidden in C macros. A macro is always intended for a specific purpose and also fulfills this task. However, if the macro is used in a different context by someone who hasn’t given much thought to the exact structure of the macro, it can easily lead to incorrect results. That’s why macros in C don’t have the best reputation.

The good news in Rust is that macros work fundamentally differently than in C. The Rust compiler does not perform simple text replacement on macros. Macros work at the Abstract Syntax Tree (AST) level in Rust. Bracket errors like in C are therefore a thing of the past. The functionality of Rust macros goes far beyond that of macros in C: The areas of application of macros in Rust range from simple helper constructs that save a little typing and structure the code better, to domain-specific language constructs (domain-specific languages, DSL for short), which can be seamlessly integrated into the Rust code using macros.

Listing 3 shows a first, small example to illustrate the principle of declarative macros. In the Rust documentation, such macros are often referred to as “Macros by Example”, because the macro developer gives an example of how the macro is to be resolved.

 

macro_rules! say_hi_to {
    ($name:expr) => {
        println!("Hi, {}!", $name);
    }
}

say_hi_to!("Rust");

 

Listing 3: First, simple Rust macro

The macro definition starts with macro_rules and specifying the macro name say\_hi\_to. The identifier \$name declares a metavariable that can be used in the macro. In our case, the metavariable \$name as an argument when calling println! passed to another macro.

The special thing about Rust macros is the fragment specifier expr, which follows after the macro name. In the example, the authors specify that Rust as the value for the variable \$name may accept an expression. So you set the macro parameter type based on syntax elements from the Rust AST. Here it becomes clear that Rust macros are not text replacements, but an AST-level transformation rule. With this semantic information, the Rust compiler can resolve the macros better than was possible with the preprocessor directives of the C macros. Rust macros do not have parenthesis errors as in the C examples shown above. Similarly, Listing 4, which is a Rust translation of the C macro SUM shown earlier, returns the correct result without having to insert parentheses.

 

macro_rules! sum {
    ($a:expr, $b:expr) => {
        $a + $b
    };
}

println!("{}", sum!(1, 2) * 2); // Result is 6
println!("{}", sum!(2 * 2, 3 * 3) * 2); // Result is 26

 

Listing 4: sum macro in Rust

In the example shown above, this takes sum-Macro similar to a function opposed to two parameters. However, the call signature of a Rust macro can also contain literals. This allows domain-specific language constructs to be implemented in Rust. Listing 5 defines a variant of the sum-Macros with a very special syntax.

 

macro_rules! sum {
    // +---------------+-- Note literals "rechne" and "plus"
    // 
    // v               v
    (rechne $a:literal plus $b:literal) => {
        $a + $b
    };
}

//                  +--------+-- Note literals here
//                  
//                  v        v
println!("{}", sum!(rechne 1 plus 2) * 2); // Result is 6

let _x = 42;
//                     +-- Does not work as eval requires literal,
//                     |   not an identifier or an expression.
//                              v
// println!("{}", sum! { rechne _x plus 2 } * 2); // Result is 6

 

Listing 5: Literals in Rust macros

The Rust lexer must be able to parse the macro call successfully, but the call does not have to conform to the Rust parser’s rules. The syntax is determined by the macro rules and the Rust compiler checks it at compile time. Syntax errors when calling the macro do not lead to runtime errors. If you’re curious and want to see how far you can go with domain-specific languages ​​with Rust macros, you can take a look at experiments like macro-lisp, which implement a Lisp-like DSL using macros.

Related Articles

Leave a Reply

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

Back to top button