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
- item: item, such as function, structure, module, etc.
- block: code block
- stmt: statement
- pat: pattern
- expr: expression
- ty: type
- ident: identifier
- path: path, e.g. foo, ::std::mem::replace, transmute::<_, int>, …
- meta: Meta information entries, such as #[…] and #![rust macro…] attributes
- 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); }