Does anyone remember how many holes we dug together? B: well… In fact, I don’t remember myself, but today we are going to dig a special hole, this hole can be said to be dug at the root of metaprogramming.

Metaprogramming is an important concept in the programming field. It allows programs to modify or replace code at run time, using code as data. If you’re familiar with Java, are you thinking of Java’s reflection mechanism? Yes, it is a kind of metaprogramming.

reflection

Rust also supports reflection, which is supported by the STD ::any:: any package in the standard library.

The following methods are provided in this package

TypeId is a type in Rust that is used to represent a unique identifier for a type. Type_id (&self) This method returns the TypeId of the variable.

The is() method is used to determine the type of a function.

You can take a look at its source code implementation

pub fn is<T: Any>(&self) - >bool {
  let t = TypeId::of::<T>();

  let concrete = self.type_id();

  t == concrete
}
Copy the code

As you can see, the implementation is very simple, just comparing TypeId.

Downcast_ref () and downcast_mut() are a pair of methods for converting generic T to a concrete type. The types returned are Option<&T> and Option<&mut T>, that is, downcast_ref() converts type T to an immutable reference and downcast_mut() converts T to a mutable reference.

Finally, let’s take a look at the specific use of these functions through an example.

use std::any::{Any, TypeId};

fn main() {
    let v1 = "Jackey";
    let mut a: &Any;
    a = &v1;
    println!("{:? }", a.type_id());
    assert!(a.is::<&str> ()); print_any(&v1);let v2: u32 = 33;
    print_any(&v2);
}

fn print_any(any: &Any) {
    if let Some(v) = any.downcast_ref::<u32> () {println!("u32 {:x}", v);
    } else if let Some(v) = any.downcast_ref::<&str> () {println!("str {:? }", v);
    } else {
        println!("else"); }}Copy the code

The macro

Rust’s reflection mechanism provides limited functionality, but Rust also provides macros to support metaprogramming.

So far, macros have been both a familiar and a strange concept to us, familiar because we’ve been using println! Macro, unfamiliar because we’ve never covered it in detail.

For println! Macros, the intuitive way we use them is they’re almost like functions. But there is a difference.

We know that for a function, the number of arguments it takes is fixed, and was fixed when the function was defined. The number of parameters received by macros is not fixed.

The macros we are talking about here are function-like macros, but Rust also has property-like macros. It is similar to annotations in Java, usually written as a tag above function names.

#[route(GET, "/")]
fn index() {
Copy the code

Route is used to specify the interface method. For the service, GET requests from the root path are routed to the index function. Such macros are defined by the #[proc_macro_attribute] annotation that belongs to procedure macros. Function-like procedure macros are defined with a #[proc_macro] annotation.

In addition to procedural macros, another large category of macros is called declarative macros. Macros are declared via macro_rules! To declare defined macros, which are more widely used than procedure macros. Vec we’ve touched before! It’s a kind of declarative macro. It is defined as follows:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new(); $( temp_vec.push($x); ) * temp_vec } }; }Copy the code

Let’s define a macro of our own.

Custom macros need to use derive annotations. (Example from the Book)

Let’s start by creating a lib library called hello_macro that defines only a trait.

pub trait HelloMacro {
    fn hello_macro(a); }Copy the code

Create a subdirectory hello_macro_DERIVE and add dependencies to the hello_macro_derive/Cargo

[lib]
proc-macro = true

[dependencies]
syn = "0.14.4"
quote = "0.6.3"
Copy the code

Then you can define the functionality implementation of our custom macro in the hello_macro_derive/lib.rs file.

extern crate proc_macro;

use crate::proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}".stringify!(#name)); }}}; gen.into() }Copy the code

Two Crate are used: SYN, which converts Rust code into a special operable data structure, and Quote, which does the opposite.

As you can see, the annotation we use for our custom macro is #[proc_macro_derive(HelloMacro)], where HelloMacro is the name of the macro, and we only need to use the annotation #[derive(HelloMacro)].

These two dependencies should be introduced first

hello_macro = { path = ".. /hello_macro" }
hello_macro_derive = { path = ".. /hello_macro/hello_macro_derive" }
Copy the code

And then use it again

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Copy the code

The run results show that we were able to successfully capture the name of the structure in the implementation.

conclusion

In this article, we have introduced two types of metaprogramming in Rust: reflection and macros. Reflection is weak, but macros are very powerful. We’ve only scratched the surface of what macros are about, and it takes a lot more time to really understand macros.