Study Rust Bible analysis – Rust learn-16 (advanced traits, macros)

Study the Rust Bible analysis – Rust learn-16 (advanced traits, macros)

  • advanced traits
    • Association Type
      • Why not use generics but Type
    • Operator overloading (low importance level)
    • Duplicate method disambiguation
    • never type
      • The value of continue is !
    • return closure
  • macro
    • Custom macros (declaration macros)
      • How macros work
      • Rust compilation process
      • Create new blank macro
      • macro selector
        • What is a term tree
        • Macro selector to set various input parameters
      • Implement a log macro
      • run repeat pattern matching
    • Custom derive macro (procedural macro)
      • Build the project structure (be sure to follow or it will be wrong)
        • set workspace
        • Create lib and main
      • hello_macro
        • lib.rs
      • hello_macro_derive
        • Add dependencies and activate `proc-macro`
        • lib.rs
        • Pay attention (please read it carefully, the official website says it very clearly, you must understand this place)
      • pancakes
        • add dependencies
        • main.rs
      • mistake
        • can’t find library `marco_t`, rename file to `src/lib.rs` or specify lib.path (why can’t build in single project package)
        • can’t use a procedural macro from the same crate that defines it
    • Custom class attribute macro (personally thinks the most important)
      • a simple example
        • Package structure
        • json_marco
          • add dependencies
          • write lib
        • json_test
      • some examples
        • base
          • lib
          • main
        • flaky_test
          • lib
          • main
        • json_parse
          • lib.rs
          • main.rs
        • fn_time
          • lib
          • main

Advanced trait

Association type Type

We can declare an associated type by using the type keyword. The function of the associated type is to simplify and hide the display type (in my opinion)

  • Simplification: When a very long type is always needed, it requires developers to spend energy on repeated writing, and if there is a change, it needs to be changed in multiple places
  • Hidden: hidden from external callers, external callers do not need to know what it refers to, as long as it can be used quickly
trait test {<!-- -->
    type Res = Result<i32, Box< & amp;'static str>>;
    fn test_res() -> Res{<!-- -->
        //...
    }
}

Why not generic but Type

Using generics, we need to display the annotation type every time we use the implementation, but when targeting a scene that is used in multiple places and does not need to modify the type, it is undoubtedly time-consuming and labor-intensive. In other words, Type sacrifices part of its flexibility in exchange for common use

Operator overloading (low importance level)

Rust does not allow creating custom operators or overloading arbitrary operators, but the operators and corresponding traits listed in std::ops can be overloaded by implementing operator-related traits
So we can overload such as +, /, -, *, etc.,
The following is an official example:

use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {<!-- -->
    x: i32,
    y: i32,
}

impl Add for Point {<!-- -->
    type Output = Point;

    fn add(self, other: Point) -> Point {<!-- -->
        Point {<!-- -->
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {<!-- -->
    assert_eq!(
        Point {<!-- --> x: 1, y: 0 } + Point {<!-- --> x: 2, y: 3 },
        Point {<!-- --> x: 3, y: 3 }
    );
}

Rename method disambiguation

When we implement multiple traits, if we encounter multiple traits with the same method name, then there will be ambiguity in the same name. At this time, the latest implementation will overwrite the previous one. In order to eliminate the ambiguity, we can use trait::fn( & amp;type) to declare the call

struct a {<!-- -->}

trait b {<!-- -->
    fn get( & amp;self) {<!-- -->}
}

trait c {<!-- -->
    fn get( & amp;self) {<!-- -->}
}

impl b for a {<!-- -->
    fn get( & amp;self) {<!-- -->
        todo!()
    }
}

impl c for a {<!-- -->
    fn get( & amp;self) {<!-- -->
        todo!()
    }
}

fn main() {<!-- -->
    let a_struct = a {<!-- -->};

    b::get( &a_struct);
    c::get( &a_struct);
}

never type

Rust has a special type called !, which we call never type because it means that the function never returns as a return value

fn no_feedback()->!{<!-- -->
//...
}

The value of continue is !

 let guess: u32 = match guess.trim().parse() {<!-- -->
            Ok(num) => num,
            Err(_) => continue,
        };

Return closure

The return value of a function can be a closure. There is no limit to this. Specifically, we simply understand the way of writing

fn test()->Box<dyn Fn()>{<!-- -->
//...
}

We write the return value to the heap by returning a Box
For example:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {<!-- -->
    Box::new(|x| x + 1)
}

Macro

Fundamentally, macros are a way of writing code for the sake of writing other code, known as metaprogramming. The derive attribute, which generates implementations of various traits, is explored in Appendix C. We’ve also used the println! macro and the vec! macro throughout this book. All of these macros expand to generate more code than you could write by hand.

Metaprogramming is very useful for reducing the amount of code to write and maintain, and it also plays the role that functions play. But macros have some additional capabilities that functions don’t.

A function signature must declare the number and types of function parameters. Macros, by contrast, can receive a different number of arguments: call println!(“hello”) with one argument or println!(“hello {}”, name) with two arguments. Furthermore, macros can be expanded before the code is translated by the compiler, for example, macros can implement traits on a given type. But functions can’t, because functions are called at runtime, and traits need to be implemented at compile time.

The downside of implementing macros less than implementing functions is that macro definitions are more complicated than function definitions, because you’re writing Rust code that generates Rust code. Because of this indirection, macro definitions are generally harder to read, understand, and maintain than function definitions.

The last important difference between macros and functions is that before calling a macro in a file it must be defined, or brought into scope, whereas functions can be defined and called from anywhere.

-https://kaisery.github.io/trpl-zh-cn/ch19-06-macros.html

Custom macros (declaration macros)

Next, we will directly customize the macro, stop talking nonsense, and start working directly (why learn this? Because you can even use this to write a language yourself)

How macros work

Rust compilation process

New blank macro

The way to create a blank macro is very simple, directly use macro_rules! to declare, and the internal shape is inferred by pattern matching (in fact, it is basically)

macro_rules! test {<!-- -->
    () => {<!-- -->};
}

Macro selector

  1. item: item, such as function, structure, module, etc.
  2. block: code block
  3. stmt: statement
  4. pat: pattern
  5. expr: expression
  6. ty: type
  7. ident: identifier
  8. path: path, e.g. foo, ::std::mem::replace, transmute::<_, int>, …
  9. meta: Meta information entries, such as #[…] and #![rust macro…] attributes
  10. tt: entry tree

What is a term tree

The tt entry tree refers to a data structure used by the Rust compiler, which is usually used to process macros (Macro) and code generation (Code Generation).

tt refers to “Token Tree”, which is a tree structure composed of a series of “Token”. “Token” is the most basic grammatical unit in a programming language, such as keywords, identifiers, operators, brackets, etc. And “Token Tree” is a tree that these “Token” are arranged according to a certain hierarchical structure.

In the Rust language, macros usually use the tt entry tree as input, which can make macro definitions more flexible and powerful. By recursing, traversing and transforming the tt entry tree, the macro can generate codes to realize the effect of metaprogramming.

In addition to macros, the Rust compiler also uses the tt entry tree to handle some code generation work, such as building an abstract syntax tree (AST) or generating an intermediate representation (IR) of the code, and so on.

The macro selector sets various input parameters

We can correspond to the parameters accepted by our macro by selecting the appropriate macro selector

Implement a log macro

use std::time::{<!-- -->Instant, SystemTime, UNIX_EPOCH};
macro_rules! log {<!-- -->
    ($log_name:tt)=>{<!-- -->
        let now = SystemTime::now();
        let timestamp = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
        println!("==========================start-{}=================== =======", $log_name);
        println!("----------------createTime:{:?}",timestamp);
        println!("----------------title:{}",$log_name);
        println!("==========================end-{}================== =======", $log_name);
    };
}

fn main() {<!-- -->
    log!("zhangsan");
}

Run repeated pattern matching

We need to use this when we have multiple input parameters, such as println! In this macro, we may pass in multiple contents that need to be printed. If each needs to be named, then why write a unified and simplified macro?
Repeat pattern matching syntax:

($($x:expr),*)=>{<!-- -->}
use std::time::{<!-- -->Instant, SystemTime, UNIX_EPOCH};
macro_rules! eq_judge {<!-- -->
    ($($left:expr => $right:expr),*)=>{<!-- -->{<!-- -->
        $(if $left == $right{<!-- -->
            println!("true")
        })*
    }}
}

fn main() {<!-- -->
    eq_judge!(
        "hello"=>"hi",
         "no"=>"no"
    );
}

Custom derive macro (process macro)

The difference from the above is that the position marked by the derive macro is generally on the structure and enum
for example:

#[derive(Debug)]
struct a{<!-- -->}

Build the project structure (must follow or it will be wrong)

The following is the official case. After I did it once, I rewritten the order and emphasized the mistakes. Please be sure to follow the order. If you encounter errors, check the mistakes I wrote here.

Set workspace

First create a project casually, then modify the toml file

  • hello_macro: Declare the traits that need to be implemented
  • hello_macro_derive: specific analysis, conversion, processing logic
  • pancakes: main execution package
[workspace]
members=[
    "hello_macro","hello_macro_derive","pancakes"
]

Create lib and main

cargo new hello_macro --lib
cargo new hello_macro_derive --lib
cargo new pancakes

The structure is as follows:

hello_macro

lib.rs

Write the traits that need to be implemented and expose them using pub

pub trait HelloMacro {<!-- -->
    fn hello_macro();
}

hello_macro_derive

Add dependencies and activate proc-macro

The syn crate parses Rust code in strings into a data structure that can be manipulated. quote converts the data structure parsed by syn back into Rust code. These crates make it easier to parse any Rust code we have to deal with: writing an entire parser for Rust is not trivial.
proc-macro indicates that this cratq is a proc-macro. After adding this configuration, the characteristics of this crate will undergo some changes. For example, this crate will only export internally defined process macros, but not other internally defined content. .

cargo add syn
cargo add quote
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro=true

[dependencies]
quote = "1.0.26"
syn = "2.0.15"

lib.rs

#[proc_macro_derive(HelloMacro)] mark as long as it is a structure, enum marked #[derive(HelloMacro)] will automatically implement the trait HelloMacro, the specific implementation logic Actually in the impl_hello_macro function

use 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: & amp;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()
}


Attention (please read carefully, the official website is very clear, this place must be understood)

When the user specifies #[derive(HelloMacro)] on a type, the hello_macro_derive function will be called. Because we’ve annotated the hello_macro_derive function with proc_macro_derive and its assigned name HelloMacro, which is the trait name, which is the convention followed by most procedural macros.

This function first converts the input from the TokenStream into a data structure that we can interpret and manipulate. This is where syn comes in handy. The parse function in syn takes a TokenStream and returns a DeriveInput structure representing the parsed Rust code. The following shows the relevant part of the DeriveInput structure parsed from the string struct Pancakes;:

DeriveInput {<!-- -->
    // --snip--

    ident: Ident {<!-- -->
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {<!-- -->
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

Defines the impl_hello_macro function, which is used to build new Rust code to be included. But before that, note that its output is also a TokenStream. The returned TokenStream will be added to the code written by our crate users, so when users compile their crates, they will get the extra functionality we provide through the modified TokenStream.

We use unwrap to panic the hello_macro_derive function when the call to the syn::parse function fails. Panic on error is necessary for procedural macros, because the proc_macro_derive function must return TokenStream instead of Result, in order to conform to the procedural macro API. The choice to use unwrap here simplifies the example; in production code, panic! or expect should be used to provide more explicit error messages about what went wrong

pancakes

Add dependencies

Here we need to rely on the lib we wrote ourselves, so we need to specify it with path

[package]
name = "pancakes"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hello_macro = {<!-- --> path = "../hello_macro" }
hello_macro_derive = {<!-- --> path = "../hello_macro_derive" }

main.rs

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

#[derive(HelloMacro)]
struct Pancakes;

fn main() {<!-- -->
    Pancakes::hello_macro();
}

Error

can’t find library marco_t, rename file to src/lib.rs or specify lib.path (Why can’t it be built in a single project package)

If you are only building in one package, when you add [lib]proc-macro = true you will get the following error:

Caused by:
  can't find library `marco_t`, rename file to `src/lib.rs` or specify lib.path

This shows that we cannot use the current package as lib, because it is the main execution package
Principle: Consider that the process macro is a program that processes the code of a crate before compiling a crate, and this program also needs to be compiled and executed. If the code that defines the procedural macro and uses the procedural macro is written in the same crate, it will fall into a deadlock:
The code to be compiled first needs to run the process macro to expand, otherwise the code is incomplete and the crate cannot be compiled.
If the crate cannot be compiled, the procedural macro code in the crate cannot be executed, and the code decorated by the procedural macro cannot be expanded.

can’t use a procedural macro from the same crate that defines it

Then if you remove this directly, you will see this error, which means you must build the process macro in lib

Custom class attribute macro (personally thinks the most important)

Class attribute macros are similar to custom derive macros, except that derive attributes generate code, and they (class attribute macros) allow you to create new attributes. They are also more flexible; derive can only be used on structs and enums; properties can also be used on other items, such as functions

Common in all kinds of frameworks!

A simple example

Project package structure

same as custom procedure macro
We need to put the parsing and processing logic under lib

[workspace]
members=[
"json_marco","json_test"
]

json_marco

Add dependencies
[package]
name = "json_marco"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = {<!-- --> version = "2.0.15", features = ["full"] }

Write lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{<!-- -->parse_macro_input, DeriveInput};

#[proc_macro_attribute]
pub fn my_macro(attr:TokenStream,item:TokenStream)->TokenStream{<!-- -->
    println!("test");
    println!("{:#?}",attr);
    println!("{:#?}",item);
    item
}

It’s very simple, just output

json_test

toml mapping import

[package]
name = "json_test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
json_marco={<!-- -->path= "../json_marco"}

main.rs

use json_marco::my_macro;

#[my_macro("test111")]
fn test(a: i32) {<!-- -->
    println!("{}", a);
}

fn main() {<!-- -->
    test(5);
}

Some examples

base

lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{<!-- -->parse_macro_input, DeriveInput};

#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {<!-- -->
    // parse the input type
    let input = parse_macro_input!(item as DeriveInput);
    
    // get type name
    let name = input.ident;

    // Build implementation code
    let expanded = quote! {<!-- -->
        impl #name {<!-- -->
            fn my_function( & amp;self) {<!-- -->
                println!("This is my custom function!");
            }
        }
    };
    
    // convert the generated code back to a TokenStream for return
    TokenStream::from(expanded)
}

main
#[my_macro]
struct MyStruct {<!-- -->
    field1: u32,
    field2: String,
}

fn main() {<!-- -->
    let my_instance = MyStruct {<!-- --> field1: 42, field2: "hello".to_string() };
    my_instance. my_function();
}

flaky_test

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

#[proc_macro_attribute]
pub fn flaky_test(_attr: TokenStream, input: TokenStream) -> TokenStream {<!-- -->
  let input_fn = syn::parse_macro_input!(input as syn::ItemFn);
  let name = input_fn.sig.ident.clone();
  TokenStream::from(quote! {<!-- -->
    #[test]
    fn #name() {<!-- -->
      #input_fn

      for i in 0..3 {<!-- -->
        println!("flaky_test retry {}", i);
        let r = std::panic::catch_unwind(|| {<!-- -->
          #name();
        });
        if r.is_ok() {<!-- -->
          return;
        }
        if i == 2 {<!-- -->
          std::panic::resume_unwind(r. unwrap_err());
        }
      }
    }
  })
}
main
#[flaky_test::flaky_test]
fn my_test() {<!-- -->
  assert_eq!(1, 2);
}

json_parse

lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{<!-- -->parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_attribute]
pub fn serde_json(_args: TokenStream, input: TokenStream) -> TokenStream {<!-- -->
    // Parse the input as a DeriveInput type, which is the common AST for all Rust structs and enums
    let input = parse_macro_input!(input as DeriveInput);

    // Check if this is a struct, and get its name, field list, etc.
    let struct_name = input.ident;
    let fields = match input.data {<!-- -->
        Data::Struct(data_struct) => data_struct.fields,
        _ => panic!("'serde_json' can only be used with structs!"),
    };

    // Generate code to convert the structure to a JSON string
    let output = match fields {<!-- -->
        Fields::Named(fields_named) => {<!-- -->
            let field_names = fields_named.named.iter().map(|f| & amp;f.ident);
            quote! {<!-- -->
                impl #struct_name {<!-- -->
                    pub fn to_json( & amp;self) -> String {<!-- -->
                        serde_json::to_string( &json!({<!-- -->
                            #(stringify!(#field_names): self.#field_names,)*
                        })).unwrap()
                    }
                }
            }
        }
        Fields::Unnamed(fields_unnamed) => {<!-- -->
            let field_indices = 0..fields_unnamed.unnamed.len();
            quote! {<!-- -->
                impl #struct_name {<!-- -->
                    pub fn to_json( & amp;self) -> String {<!-- -->
                        serde_json::to_string( & amp;json!([
                            #(self. #field_indices,)*
                        ])).unwrap()
                    }
                }
            }
        }
        Fields::Unit => {<!-- -->
            quote! {<!-- -->
                impl #struct_name {<!-- -->
                    pub fn to_json( & amp;self) -> String {<!-- -->
                        serde_json::to_string( &json!({<!-- -->})).unwrap()
                    }
                }
            }
        }
    };

    // Return the generated code as a TokenStream
    output. into()
}

main.rs
#[serde_json]
struct MyStruct {<!-- -->
    name: String,
    age: u32,
}

fn main() {<!-- -->
    let my_struct = MyStruct {<!-- -->
        name: "Alice".to_string(),
        age: 25,
    };
    let json_str = my_struct.to_json();
    println!("JSON string: {}", json_str);
}

fn_time

lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{<!-- -->parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn run(_args: TokenStream, input: TokenStream) -> TokenStream {<!-- -->
    // parse the input into a function node
    let input = parse_macro_input!(input as ItemFn);

    // Get the function name, parameter list and other information
    let func_name = &input.ident;
    let func_args = &input.decl.inputs;

    // Generate code to print timestamps at the beginning and end of the function
    let output = quote! {<!-- -->
        #input

        fn #func_name(#func_args) -> () {<!-- -->
            println!("{} started", stringify!(#func_name));
            let start = std::time::Instant::now();
            let result = #func_name(#func_args);
            let end = start. elapsed();
            println!("{} finished in {}ms", stringify!(#func_name), end.as_millis());
            result
        }
    };

    // Return the generated code as a TokenStream
    output. into()
}

main
#[run]
fn my_function() -> i32 {<!-- -->
    // simulate some processing time
    std::thread::sleep(std::time::Duration::from_secs(1));
    42
}

fn main() {<!-- -->
    let result = my_function();
    println!("Result = {}", result);
}