Procedural Macros, pt. 1: An Introduction

Macros, declarative or procedural, are mechanisms in rust that use a few input keywords to generate blocks of code. In this series of posts, we are going to build a derive macro (a type of procedural macro), which will generate functions that get the value of a member variable in a struct. These functions are commonly known as getters (and are generally accompanied by setters).

#[derive(propertese)]
struct Person {
  name: String,
  age: u16,
}

Given the struct declared above, our derive macro propertese will generate the code below.

impl Person {
  pub fn name(&self) -> &str {
    self.name.as_str()
  }
  
  pub fn age(&self) -> u16 {
    self.age
  }
}

While the macro invocation #[derive(propertese)] is simple, generation of the target code is more involved. As we work our way through this, we will find that it has to

  1. Identify the name of the struct.
  2. Detect the types of the variables in the struct.
  3. Determine whether it has to return values by copy (Numerics), or as a slice (String)
  4. and as we get into it, a lot more…

Functional Specs

Before we begin writing the code for our macro, let’s establish the basic rules with which the macro should operate. These rules will form the basis for the functional requirements of the macro.

Getters

  1. The return type of the getters should be specialized by the type of the member variable. We are going to keep things simple here, and only consider String and Numeric types. By default, the following mapping will be used.

    Property TypeReturn Type
    String&str
    T where T: NumericT
  2. We should be able to skip the generation of a getter for any of the member variables.

For our derive macro to be really useful, we’d have to expand our rules to handle a variety of additional types. We are, however, going to keep things simple and focus on the details behind writing a derive macro - in our next post.