Meta Programming

Metaprogramming can be seen as a way of writing code that writes/generates other code.

Roslyn is providing a feature for metaprogramming in C#, available since .NET 5, and called Source Generators. Source generators can create new C# source files at build-time that are added to the user's compilation. Before Source Generators were introduced, Visual Studio has been providing a code generation tool via T4 Text Templates. An example on how T4 works is the following template or its concretization.

Rust is also providing a feature for metaprogramming: macros. There are declarative macros and procedural macros.

Declarative macros allow you to write control structures that take an expression, compare the resulting value of the expression to patterns, and then run the code associated with the matching pattern.

The following example is the definition of the println! macro that it is possible to call for printing some text println!("Some text")

macro_rules! println {
    () => {
        $crate::print!("\n")
    };
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}

To learn more about writing declarative macros, refer to the Rust reference chapter macros by example or The Little Book of Rust Macros.

Procedural macros are different than declarative macros. Those accept some code as an input, operate on that code, and produce some code as an output.

Another technique used in C# for metaprogramming is reflection. Rust does not support reflection.

Function-like macros

Function-like macros are in the following form: function!(...)

The following code snippet defines a function-like macro named print_something, which is generating a print_it method for printing the "Something" string.

In the lib.rs:

extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro]
pub fn print_something(_item: TokenStream) -> TokenStream {
    "fn print_it() { println!(\"Something\") }".parse().unwrap()
}

In the main.rs:

use replace_crate_name_here::print_something;
print_something!();

fn main() {
    print_it();
}

Derive macros

Derive macros can create new items given the token stream of a struct, enum, or union. An example of a derive macro is the #[derive(Clone)] one, which is generating the needed code for making the input struct/enum/union implement the Clone trait.

In order to understand how to define a custom derive macro, it is possible to read the rust reference for derive macros

Attribute macros

Attribute macros define new attributes which can be attached to rust items. While working with asynchronous code, if making use of Tokio, the first step will be to decorate the new asynchronous main with an attribute macro like the following example:

#[tokio::main]
async fn main() {
    println!("Hello world");
}

In order to understand how to define a custom derive macro, it is possible to read the rust reference for attribute macros