Creating your first macro with macro_rules!

Let's start with declarative macros first by building one using the macro_rules! macro. Rust already has the println! macro, which is used to print things to the standard output. However, it doesn't have an equivalent macro for reading input from the standard input. To read from the standard input, you have to write something like the following:

let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();

These lines of code can be easily abstracted away with a macro. We'll name our macro scanline!. Here's the code that shows us how we want to use this macro:

// first_macro.rs

fn main() {
let mut input = String::new();
scanline!(input);
println!("{:?}", input);
}

We want to be able to create a String instance and just pass it to scanline!, which handles all the details of reading from standard input. If we compile the preceding code by running rustc first_macro.rs, we get the following error:

error: cannot find macro `scanline!` in this scope
--> first_macro.rs:5:5
|
5 | scanline!(input);
| ^^^^^^^^

error: aborting due to previous error

rustc cannot find the scanline! macro, because we haven't defined it yet, so let's do that:

// first_macro.rs

use std::io::stdin;

// A convenient macro to read input as string into a buffer
macro_rules! scanline {
($x:expr) => ({
stdin().read_line(&mut $x).unwrap();
$x.trim();
});
}

To create the scanline! macro, we use the macro_rules! macro, followed by the macro name scanline!, followed by a pair of braces. Within the braces, we have things that look similar to match arms. These are called matching rules. Every matching rule consists of three parts. The first is the pattern matcher, that is, the ($x:expr) part, followed by a =>, and then the code generation block, which can be delimited either with (), {}, or even []. A matching rule has to end with a semicolon when there is more than one rule to match.

In the preceding code, the notation on the left, ($x:expr), within parentheses is the rules, where $x is a token tree variable that needs to have a type specified after the colon :, which is an expr token tree type. Their syntax is similar to how we specify parameters in functions. When we invoke the scanline! macro with any token sequence as input, it gets captured in $x and is referred to by the same variable within the code generation block on the right. The expr token type means that this macro can only accept things that are expressions. We'll cover other kinds of token types that are accepted by macro_rules! in a moment. In the code generation block, we have multi-line code to generate, so we have a pair of braces, which are there to account for multi-line expressions. The matching rule ends with a semicolon. We can also omit braces if we have a single line of code that needs to be generated. The generated code we want is as follows:

io::stdin().read_line(&mut $x).unwrap();

Notice that read_line accepts something that doesn't look like a proper mutable reference to some identifier, that is, it's a &mut $x . The $x gets substituted with an actual expression that we pass to our macro on invocation. That's it; we just wrote our first macro! The complete code is as follows:

// first_macro.rs

use std::io;

// A convenient macro to read input as string into a buffer
macro_rules! scanline {
($x:expr) => ({
io::stdin().read_line(&mut $x).unwrap();
});
}

fn main() {
let mut input = String::new();
scanline!(input);
println!("I read: {:?}", input);
}

In main, we first create our input string, which will store our input from the user. Next, our scanline! macro is invoked where we pass the input variable. Within this macro, this is then referred to as $x, as we saw in the preceding definition. With the invocation of scanline, when the compiler sees the invocation, it replaces that with the following:

io::stdin().read_line(&mut input).unwrap();

Here's the output on running the preceding code with an input string of Alice from the standard input:

$ Alice
I read: "Alice "

Following code generation, the compiler also checks whether the generated code makes any sense. For example, if we were to invoke scanline! with some other item that is not accounted for in the matching rules (say, passing an fn keyword, such as scanline!(fn)), we would get the following error:

Also, even if we pass an expression (say, 2), which is valid to pass (as it's also an expr ) to this macro but doesn't make sense in this context, Rust will catch this and report as follows:

This is neat! Now, we can also add multiple matching rules to our macro. So, let's add an empty rule that covers the case where we just want scanline! to allocate the String for us, read from stdin, and return the string back. To add a new rule, we modify the code like so:

// first_macro.rs

macro_rules! scanline {
($x:expr) => ({
io::stdin().read_line(&mut $x).unwrap();
});
() => ({
let mut s = String::new();
stdin().read_line(&mut s).unwrap();
s
});
}

We added an empty match rule, () => {}. Within the braces, we generate a bunch of code where we first create a String instance in s, call read_line, and pass &mut s. Finally, we return s to the caller. Now, we can call our scanline! without a pre-allocated String buffer:

// first_macro.rs

fn main() {
let mut input = String::new();
scanline!(input);
println!("Hi {}",input);
let a = scanline!();
println!("Hi {}", a);
}

It's also important to note that we cannot invoke this macro anywhere outside functions. For instance, the scanline! invocation at the root of a module will fail, as it is invalid to write a let statement within a mod {} declaration.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset