過程宏(proc-macro)

coxer發表於2024-04-15

優點

  • 增加程式碼的複用。
  • 效能。因為是在編譯時生成,所以會得到更好的效能。沒測試過,有待商榷

過程宏的分類

  • proc-macro
  • proc-macro-derive
  • proc-macro-attribute

構建過程宏的必要設定

構建過程宏,要在cargo.toml裡面設定一些引數,這是必須的。一般來說,過程宏必須是一個庫,或者作為工程的子庫,不能單獨作為一個原始檔存在,至少目前不行。

[lib]
proc-macro = true
path = "src/lib.rs"

而編寫過程宏,在stable版本里,我們需要藉助三個crate:

  • syn,這個是用來解析語法樹(AST)的。各種語法構成
  • quote,解析語法樹,生成rust程式碼,從而實現你想要的新功能。
  • proc_macro(std)proc_macro2(3rd-party)

但在nightly版本里,以上的這些crate都不需要了,不依賴第三方crate,還有就是語法上是稍微有些不同,大部分是一樣的。但這篇文章只講stable rust裡的過程宏,如果想了解nightly rust的過程宏,可以去看maudRocket,前者是一個HTML模板引擎,大量使用了過程宏,模板都是編譯時生成,所以效能非常高,而後者是一個web framework,rust各種黑魔法使用的集大成者。

proc-macro(function-like,類函式宏)

這種過程宏和標準宏很類似,只是構建過程不太一樣,使用方式還是一樣的。標準語法是這樣的。

#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream{
    // ...
}

可以看出函式式的過程宏只接受一個形參,而且必須是pub的。 簡單寫一個例子,參照官網文件的,只是稍微改了一點點。

#[proc_macro]
pub fn my_proc_macro(ident: TokenStream) -> TokenStream {
    let new_func_name = format!("test_{}", ident.to_string());
    let concated_ident = Ident::new(&new_func_name, Span::call_site()); // 建立新的ident,函式名

    let expanded = quote! {
        // 不能直接這樣寫trait bound,T: Debug
        // 會報錯,找不到Debug trait,最好給出full path
        fn #concated_ident<T: std::fmt::Debug>(t: T) {
            println!("{:?}", t);
        }
    };
    expanded.into()
}

使用情形如下。

use your_crate_name::my_proc_macro;
// ...
my_proc_macro!(hello)!; // 函式test_hello就生成了,可見性在呼叫之後
// ...
test_hello("hello, proc-macro");
test_hello(10);

可以看出,寫一個函式式的過程宏還是不那麼複雜的。

proc_macro_derive(Derive mode macros, 繼承宏)

繼承宏的函式簽名和前者有些類似:

#[proc_macro_derive(MyDerive)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

不過不同的是,引入屬性有些不同。

proc_macro_derive表明了這是繼承宏,還定義了新的繼承宏的名字MyDerive。 熟悉rust程式設計的,都應該知道有個繼承宏,一直用得到,就是Debug。這是標準庫裡的,可以幫助除錯和顯示。所以呢,這裡就來實現一個類似功能的繼承宏,暫時命名這個過程宏名字為Show。 這個例子稍微有點複雜。當然我覺得還是先看了官方文件的例子之後再來看我的例子會比較好些。

#[proc_macro_derive(Show)]
pub fn derive_show(item: TokenStream) -> TokenStream {
    // 解析整個token tree
    let input = parse_macro_input!(item as DeriveInput);
    let struct_name = &input.ident; // 結構體名字

    // 提取結構體裡的欄位
    let expanded = match input.data {
        Data::Struct(DataStruct{ref fields,..}) => {
            if let Fields::Named(ref fields_name) = fields {
                // 結構體中可能是多個欄位
                let get_selfs: Vec<_> = fields_name.named.iter().map(|field| {
                    let field_name = field.ident.as_ref().unwrap(); // 欄位名字
                    quote! {
                        &self.#field_name
                    }
                }).collect();

            let implemented_show = quote! {
                // 下面就是Display trait的定義了
                // use std::fmt; // 不要這樣import,因為std::fmt是全域性的,無法做到衛生性(hygiene)
                // 編譯器會報錯重複import fmt當你多次使用Show之後
                impl std::fmt::Display for #struct_name {
                    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                        // #(#get_self),*,這是多重匹配,生成的樣子大概是這樣:&self.a, &self.b, &self.c, ...
                        // 用法和標準宏有點像,關於多個匹配,可以看這個文件
                        // https://docs.rs/quote/1.0.0/quote/macro.quote.html
                        write!(f, "{} {:?}", stringify!(#struct_name), (#(#get_selfs),*))
                    }
                }
            };
            implemented_show
            
            } else {
                panic!("sorry, may it's a complicated struct.");
            }
        }
        _ => panic!("sorry, Show is not implemented for union or enum type.")
    };
    expanded.into()
}

使用情形:

use your_crate_name::Show;
// ...
#[derive(Show)]
struct MySelf {
    name: String,
    age: u8,
}
// ...
let me = MySelf{name: "Jamie", age: 255};
println!("{}", me); // MySelf (Jamie, 255)

不過呢,繼承宏還可以新增額外的屬性,函式簽名類似如下

#[proc_macro_derive(MyDerive, attributes(my_attr)]
pub fn my_proc_macro_derive(input: TokenStream) -> TokenStream{
    // ...
}

這裡增加了一個關鍵字attributes,並指定了屬性的名字。詳細情況可以看官方文件。示例程式碼裡也有個例子,因為文章篇幅,我就不贅述了。

proc_macro_attribute(Attribute macros, 屬性宏)

屬性宏的函式簽名類似如下:

#[proc_macro_attribute]
pub fn my_attribute_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}

可以看到這裡的形參是兩個,使用的關鍵字是proc_macro_attribute。 關於例子,熟悉python的人應該知道修飾器吧,其實本質就是函式(閉包)可以作為一個物件來返回。 比如我需要一個修飾器來測量一個呼叫函式的執行時間。python的實現很簡單,如下:

def my_decorator(func):
    import time
    def timming_measrement(*args):
        start = time.time()
        func(*args)
        end = time.time()
        print(f"time cost: {end - start}")
    return timming_measrement
    
@my_decorator
def my_target_func(sec):
    import time
    time.sleep(sec)
    
my_target_func(2) # should print 2.00xx
my_target_func(4) # should print 4.00xx

如果要用rust來實現類似功能的程式碼,就要複雜一些了。 屬性宏接受的引數也不太一樣,這也會導致屬性宏的實現也會不太一樣:

// 可能屬性引數多種多樣
// #[my_macro_attribute]
// #[my_macro_attribute=something]
#[my_macro_attribute(post)] // 這是例子的使用情況
fn my_func() {
    // ...
}

實現過程

#[proc_macro_attribute]
pub fn rust_decorator(attr: TokenStream, func: TokenStream) -> TokenStream {
    let func = parse_macro_input!(func as ItemFn); // 我們傳入的是一個函式,所以要用到ItemFn
    let func_vis = &func.vis; // pub
    let func_block = &func.block; // 函式主體實現部分{}

    let func_decl = &func.sig; // 函式申明
    let func_name = &func_decl.ident; // 函式名
    let func_generics = &func_decl.generics; // 函式泛型
    let func_inputs = &func_decl.inputs; // 函式輸入引數
    let func_output = &func_decl.output; // 函式返回

    // 提取引數,引數可能是多個
    let params: Vec<_> = func_inputs.iter().map(|i| {
        match i {
            // 提取形參的pattern
            // https://docs.rs/syn/1.0.1/syn/struct.PatType.html
            FnArg::Typed(ref val) => &val.pat, // pat沒有辦法移出val,只能借用,或者val.pat.clone()
            _ => unreachable!("it's not gonna happen."),
        }
    }).collect();
    
    // 解析attr
    let attr = parse_macro_input!(attr as AttributeArgs);
    // 提取attr的ident,此處例子只有一個attribute
    let attr_ident = match attr.get(0).as_ref().unwrap() {
        NestedMeta::Meta(Meta::Path(ref attr_ident)) => attr_ident.clone(),
        _ => unreachable!("it not gonna happen."),
    };
    
    // 建立新的ident, 例子裡這個ident的名字是time_measure
    // let attr = Ident::new(&attr.to_string(), Span::call_site());
    let expanded = quote! { // 重新構建函式執行
        #func_vis fn #func_name #func_generics(#func_inputs) #func_output {
            // 這是沒有重新構建的函式,最開始宣告的,需要將其重建出來作為引數傳入,
            // fn time_measure<F>(func: F) -> impl Fn(u64) where F: Fn(u64)
            // fn deco(t: u64) {
            //     let secs = Duration::from_secs(t);
            //     thread::sleep(secs);
            // }
            fn rebuild_func #func_generics(#func_inputs) #func_output #func_block
            // 注意這個#attr的函式簽名:fn time_measure<F>(func: F) -> impl Fn(u64) where F: Fn(u64)
            // 形參是一個函式,就是rebuild_func
            let f = #attr_ident(rebuild_func);

            // 要修飾函式的引數,有可能是多個引數,所以這樣匹配 #(#params,) *
            f(#(#params,) *)
        }
    };
    expanded.into()
}

還有一段程式碼,這個函式相當於過程宏的屬性(引數attr)。

// use std::time;
// 該函式接受一個函式作為引數,並返回一個閉包,程式碼很簡單,就不解釋了。
// thanks for impl trait
fn runtime_measurement<F>(func: F) -> impl Fn(u64) where F: Fn(u64) {
    move |s| {
        let start = time::Instant::now();
        func(s);
        println!("time cost {:?}", start.elapsed());
    }
}

假定這是我們要修飾的目標函式。

#[rust_decorator(runtime_measurement)]
fn deco(t: u64) {
    let secs = Duration::from_secs(t);
    thread::sleep(secs);
}

// ...
deco(4);
deco(2);

上面這個例子有點複雜了,其實可以不用把測試函式作為引數傳入,所以不需要定義一個attr,也不需要解析這個attr。直接可以這樣寫,簡單明瞭,可以獲取任意函式的執行時,上面的那個還要考慮引數型別。

#[proc_macro_attribute]
pub fn run_time(_: TokenStream, func: TokenStream) -> TokenStream {
    let func = parse_macro_input!(func as ItemFn);
    let func_vis = &func.vis; // like pub
    let func_block = &func.block; // { some statement or expression here }

    let func_decl = func.sig;
    let func_name = &func.ident; // function name
    let func_generics = &func_decl.generics;
    let func_inputs = &func_decl.inputs;
    let func_output = &func_decl.output;
    
    let caller = quote!{
        // rebuild the function, add a func named is_expired to check user login session expire or not.
        #func_vis fn #func_name #func_generics(#func_inputs) #func_output {
            use std::time;
            
            let start = time::Instant::now();
            #func_block
            println!("time cost {:?}", start.elapsed());
        }
    };
    
    caller.into() 
}

使用和前面的類似。

#[run_time]
fn deco(t: u64) {
    let secs = Duration::from_secs(t);
    thread::sleep(secs);
}

// ...
deco(4);
deco(2);

from:https://dengjianping.github.io/2019/02/28/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E8%BF%87%E7%A8%8B%E5%AE%8F(proc-macro).html

相關文章