【譯】Rust巨集:教程與示例(二)

Praying發表於2021-03-11

原文標題:Macros in Rust: A tutorial with examples
原文連結:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
公眾號: Rust 碎碎念
翻譯 by: Praying

Rust 中的過程巨集

過程巨集(Procedural macros)[1]是一種更為高階的巨集。過程巨集能夠擴充套件 Rust 的現有語法。它接收任意輸入併產生有效的 Rust 程式碼。

過程巨集接收一個TokenStream作為引數並返回另一個TokenStream。過程巨集對輸入的TokenStream進行操作併產生一個輸出。有三種型別的過程巨集:

  1. 屬性式巨集(Attribute-like macros)
  2. 繼承巨集(Derive macros)
  3. 函式式巨集(Function-like macros)

接下來我們將會對它們進行詳細討論。

屬性式巨集

屬性式巨集能夠讓你建立一個自定義的屬性,該屬性將其自身關聯一個項(item),並允許對該項進行操作。它也可以接收引數。

#[some_attribute_macro(some_argument)]
fn perform_task(){
// some code
}

在上面的程式碼中,some_attribute_macros是一個屬性巨集,它對函式perform_task進行操作。

為了編寫一個屬性式巨集,我們先用cargo new macro-demo --lib來建立一個專案。建立完成後,修改Cargo.toml來通知 cargo,該專案將會建立過程巨集。

# Cargo.toml
[lib]
proc-macro = true

現在,我們可以開始過程巨集學習之旅了。

過程巨集是公開的函式,接收TokenStream作為引數並返回另一個TokenStream。要想寫一個過程巨集,我們需要先實現能夠解析TokenStream的解析器。Rust 社群已經有了很好的 crate——syn[2],用於解析TokenStream

syn提供了一個現成的 Rust 語法解析器能夠用於解析TokenStream。你可以通過組合syn提供的底層解析器來解析你自己的語法、

synquote[3]新增到Cargo.toml

# Cargo.toml
[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

現在我們可以使用proc_macrolib.rs中寫一個屬性式巨集,proc_macro是編譯器提供的用於寫過程巨集的一個 crate。對於一個過程巨集 crate,除了過程巨集外,不能匯出其他任何東西,crate 中定義的過程巨集不能在 crate 自身中使用。

// lib.rs
extern crate proc_macro;
use proc_macro::{TokenStream};
use quote::{quote};

// using proc_macro_attribute to declare an attribute like procedural macro
#[proc_macro_attribute]
// _metadata is argument provided to macro call and _input is code to which attribute like macro attaches
pub fn my_custom_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    // returing a simple TokenStream for Struct
    TokenStream::from(quote!{struct H{}})
}

為了測試我們新增的巨集,我們需要建立一個測試。建立一個名為tests的資料夾然後在該資料夾新增檔案attribute_macro.rs。在這個檔案中,我們可以測試我們的屬性式巨集。

// tests/attribute_macro.rs

use macro_demo::*;

// macro converts struct S to struct H
#[my_custom_attribute]
struct S{}

#[test]
fn test_macro(){
// due to macro we have struct H in scope
    let demo=H{};
}

使用命令cargo test來執行上面的測試。

現在,我們理解了過程巨集的基本使用,讓我們用syn來對TokenStream進行一些高階操作和解析。

為了理解syn是如何用來解析和操作的,讓我們來看syn Github 倉庫[4]上的一個示例。這個示例建立了一個 Rust 巨集,這個巨集可以追蹤變數值的變化。

首先,我們需要去驗證,我們的巨集是如何操作與其所關聯的程式碼的

#[trace_vars(a)]
fn do_something(){
  let a=9;
  a=6;
  a=0;
}

trace_vars巨集獲取它所要追蹤的變數名,然後每當輸入變數(也就是a)的值發生變化時注入一條列印語句。這樣它就可以追蹤輸入變數的值了。

首先,解析屬性式巨集所關聯的程式碼。syn提供了一個適用於 Rust 函式語法的內建解析器。ItemFn將會解析函式,並且如果語法無效,它會丟擲一個錯誤。

#[proc_macro_attribute]
pub fn trace_vars(_metadata: TokenStream, input: TokenStream) -> TokenStream {
// parsing rust function to easy to use struct
    let input_fn = parse_macro_input!(input as ItemFn);
    TokenStream::from(quote!{fn dummy(){}})
}

現在我們已經解析了input,讓我們開始轉移到metadata。對於metadata,沒有適用的內建解析器,所以我們必須自己使用synparse模組寫一個解析器。

#[trace_vars(a,c,b)] // we need to parse a "," seperated list of tokens
// code

要想syn能夠工作,我們需要實現syn提供的Parse trait。Punctuated用於建立一個由,分割Indentvector

struct Args{
    vars:HashSet<Ident>
}

impl Parse for Args{
    fn parse(input: ParseStream) -> Result<Self> {
        // parses a,b,c, or a,b,c where a,b and c are Indent
        let vars = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;
        Ok(Args {
            vars: vars.into_iter().collect(),
        })
    }
}

一旦我們實現Parse trait,我們就可以使用parse_macro_input巨集來解析metadata

#[proc_macro_attribute]
pub fn trace_vars(metadata: TokenStream, input: TokenStream) -> TokenStream {
    let input_fn = parse_macro_input!(input as ItemFn);
    // using newly created struct Args
    let args= parse_macro_input!(metadata as Args);
    TokenStream::from(quote!{fn dummy(){}})
}

現在,我們準備修改input_fn以便於在當變數值變化時新增println!。為了完成這項修改,我們需要過濾出有複製語句的程式碼,並在那行程式碼之後插入一個 print 語句。

impl Args {
    fn should_print_expr(&self, e: &Expr) -> bool {
        match *e {
            Expr::Path(ref e) => {
 // variable shouldn't start wiht ::
                if e.path.leading_colon.is_some() {
                    false
// should be a single variable like `x=8` not n::x=0
                } else if e.path.segments.len() != 1 {
                    false
                } else {
// get the first part
                    let first = e.path.segments.first().unwrap();
// check if the variable name is in the Args.vars hashset
                    self.vars.contains(&first.ident) && first.arguments.is_empty()
                }
            }
            _ => false,
        }
    }

// used for checking if to print let i=0 etc or not
    fn should_print_pat(&self, p: &Pat) -> bool {
        match p {
// check if variable name is present in set
            Pat::Ident(ref p) => self.vars.contains(&p.ident),
            _ => false,
        }
    }

// manipulate tree to insert print statement
    fn assign_and_print(&mut self, left: Expr, op: &dyn ToTokens, right: Expr) -> Expr {
 // recurive call on right of the assigment statement
        let right = fold::fold_expr(self, right);
// returning manipulated sub-tree
        parse_quote!({
            #left #op #right;
            println!(concat!(stringify!(#left), " = {:?}"), #left);
        })
    }

// manipulating let statement
    fn let_and_print(&mut self, local: Local) -> Stmt {
        let Local { pat, init, .. } = local;
        let init = self.fold_expr(*init.unwrap().1);
// get the variable name of assigned variable
        let ident = match pat {
            Pat::Ident(ref p) => &p.ident,
            _ => unreachable!(),
        };
// new sub tree
        parse_quote! {
            let #pat = {
                #[allow(unused_mut)]
                let #pat = #init;
                println!(concat!(stringify!(#ident), " = {:?}"), #ident);
                #ident
            };
        }
    }
}

在上面的示例中,quote巨集用於模板化和生成 Rust 程式碼。#用於注入變數的值。

現在,我們將會在input_fn上進行 DFS,並插入 print 語句。syn提供了一個Foldtrait 可以用來對任意Item實現 DFS。我們只需要修改與我們想要操作的 token 型別所對應的 trait 方法。

impl Fold for Args {
    fn fold_expr(&mut self, e: Expr) -> Expr {
        match e {
// for changing assignment like a=5
            Expr::Assign(e) => {
// check should print
                if self.should_print_expr(&e.left) {
                    self.assign_and_print(*e.left, &e.eq_token, *e.right)
                } else {
// continue with default travesal using default methods
                    Expr::Assign(fold::fold_expr_assign(self, e))
                }
            }
// for changing assigment and operation like a+=1
            Expr::AssignOp(e) => {
// check should print
                if self.should_print_expr(&e.left) {
                    self.assign_and_print(*e.left, &e.op, *e.right)
                } else {
// continue with default behaviour
                    Expr::AssignOp(fold::fold_expr_assign_op(self, e))
                }
            }
// continue with default behaviour for rest of expressions
            _ => fold::fold_expr(self, e),
        }
    }

// for let statements like let d=9
    fn fold_stmt(&mut self, s: Stmt) -> Stmt {
        match s {
            Stmt::Local(s) => {
                if s.init.is_some() && self.should_print_pat(&s.pat) {
                    self.let_and_print(s)
                } else {
                    Stmt::Local(fold::fold_local(self, s))
                }
            }
            _ => fold::fold_stmt(self, s),
        }
    }
}

Fold trait 用於對一個Item進行 DFS。它使得你能夠針對不同的 token 型別採取不同的行為。

現在我們可以使用fold_item_fn在我們解析的程式碼中注入 print 語句。

#[proc_macro_attribute]
pub fn trace_var(args: TokenStream, input: TokenStream) -> TokenStream {
// parse the input
    let input = parse_macro_input!(input as ItemFn);
// parse the arguments
    let mut args = parse_macro_input!(args as Args);
// create the ouput
    let output = args.fold_item_fn(input);
// return the TokenStream
    TokenStream::from(quote!(#output))
}

這個程式碼示例來自於syn 示例倉庫[5],該倉庫也是關於過程巨集的一個非常好的學習資源。

自定義繼承巨集

Rust 中的自定義繼承巨集能夠對 trait 進行自動實現。這些巨集通過使用#[derive(Trait)]自動實現 trait。

synderive巨集有很好的支援。

#[derive(Trait)]
struct MyStruct{}

要想在 Rust 中寫一個自定義繼承巨集,我們可以使用DeriveInput來解析繼承巨集的輸入。我們還將使用proc_macro_derive巨集來定義一個自定義繼承巨集。

#[proc_macro_derive(Trait)]
pub fn derive_trait(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident;

    let expanded = quote! {
        impl Trait for #name {
            fn print(&self) -> usize {
                println!("{}","hello from #name")
           }
        }
    };

    proc_macro::TokenStream::from(expanded)
}

使用syn可以編寫更為高階的過程巨集,請查閱syn倉庫中的這個示例[6]

函式式巨集

函式式巨集類似於宣告式巨集,因為他們都通過巨集呼叫操作符!來執行,並且看起來都像是函式呼叫。它們都作用於圓括號裡的程式碼。

下面是如何在 Rust 中寫一個函式式巨集:

#[proc_macro]
pub fn a_proc_macro(_input: TokenStream) -> TokenStream {
    TokenStream::from(quote!(
            fn anwser()->i32{
                5
            }
))
}

函式式巨集在編譯期而非在執行時執行。它們可以在 Rust 程式碼的任何地方被使用。函式式巨集同樣也接收一個TokenStream並返回一個TokenStream

使用過程巨集的優勢包括:

  • 使用span獲得更好的錯誤處理
  • 更好的控制輸出
  • 社群已有synquote兩個 crate
  • 比宣告式巨集更為強大

總結

在這篇 Rust 教程中,我們涵蓋了 Rust 中關於巨集的基本內容,宣告式巨集和過程巨集的定義,以及如果使用各種語法和社群的 crate 來編寫這兩種型別的巨集。我們還總結了每種型別的 Rust 巨集所具有優勢。

參考資料

[1]

過程巨集(Procedural macros): https://blog.logrocket.com/procedural-macros-in-rust/

[2]

syn: https://crates.io/crates/syn

[3]

quote: https://crates.io/crates/quote

[4]

syn Github 倉庫: https://github.com/dtolnay/syn/blob/master/examples/trace-var/trace-var/src/lib.rs

[5]

syn 示例倉庫: https://github.com/dtolnay/syn/blob/master/examples/trace-var/trace-var/src/lib.rs

[6]

這個示例: https://github.com/dtolnay/syn/blob/master/examples/heapsize/heapsize_derive/src/lib.rs

相關文章