1. 程式人生 > 其它 >Rust中的過程巨集

Rust中的過程巨集

前言

過程巨集(Procedural macros)允許你操作給定 Rust 程式碼的抽象語法樹(abstract syntax tree, AST)。過程巨集是從一個(或者兩個)TokenStream到另一個TokenStream的函式,用輸出的結果來替換巨集呼叫。

工具準備

安裝cargo-expand

安裝後,使用cargo expand命令可以打印出應用於當前 crate 的巨集擴充套件結果。

// 安裝nightly
rustup toolchain install nightly
// 並設定為預設,或者直接執行該語句會自動下載並設定為預設
rustup default nightly

// 安裝cargo-expand
cargo install cargo-expand

// 如果提示link.exe,按提示安裝C++

專案準備

專案目錄

// 建立新專案
cargo new rust_macro_test

// 建立巨集目錄
mkdir proc_macro_crate

cd proc_macro_crate

// 初始化專案並建立lib.rs檔案
cargo init --lib


專案結構為:

rust_macro_test
├─proc_macro_crate
│  └─src
└─src

注意:

1、過程巨集必須定義在一個獨立的crate中。不能在一個crate中既定義過程巨集,又使用過程巨集。

原理:過程巨集是在編譯一個crate之前,對crate的程式碼進行加工的一段程式,這段程式也是需要編譯後執行的。如果定義過程巨集和使用過程巨集的程式碼寫在一個crate中,那就陷入了死鎖:

  • 要編譯的程式碼首先需要執行過程巨集來展開,否則程式碼是不完整的,沒法編譯crate。
  • 不能編譯crate,crate中的過程巨集程式碼就沒法執行,就不能展開被過程巨集裝飾的程式碼

2、過程巨集必須定義定義在lib目標中,不能定義在bin目標中

引入過程巨集三件套

在proc_macro_crate下Cargo.toml新增:

# 表示這個crate是過程巨集
[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.7"
quote = "1"
syn = { version = "1.0.56", features = ["full","extra-traits"] } # "extra-traits"方便後續列印除錯資訊
  • proc_macro2:是對proc_macro的封裝,是由 Rust 官方提供的。
  • syn:是基於proc_macro2中暴露的 TokenStream API 來生成 AST,提供方便的操作AST的介面。
  • quote:配合syn,將AST轉回TokenSteam,迴歸到普通文字程式碼生成的TokenSteam中。

新增過程巨集crate依賴

在rust_macro_test下Cargo.toml新增:

[dependencies]
proc_macro_crate = {path="./proc_macro_crate"}

Rust過程巨集

屬性式過程巨集

Attribute macros,用在結構體、欄位、函式等地方,為其指定屬性等功能。

定義

在lib.rs中加入:

// 屬性式過程巨集 custom_proc_macro_attribute 為巨集的名字
#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(
    attrs: proc_macro::TokenStream, // 過程巨集傳的屬性
    input: proc_macro::TokenStream, // 作用的程式碼
) -> proc_macro::TokenStream {
    eprintln!("attrs:{:#?}", attrs); // 注意輸出要用epringln!
    eprintln!("input:{:#?}", input);
    input

    // 如果返回空物件,相當於去除方法
    // proc_macro::TokenStream::new()
}

使用

在main.rs加入:

use proc_macro_crate::custom_proc_macro_attribute;

fn main() {
    custom_proc_macro_attribute_fn();
    custom_proc_macro_attribute_fn_ha_attribute();
}

// 屬性過程巨集:空屬性
#[custom_proc_macro_attribute]
fn custom_proc_macro_attribute_fn() {
    println!("this is custom_proc_macro_attribute_fn()");
}

// 屬性過程巨集:自定義屬性
#[custom_proc_macro_attribute("this is custom_proc_macro_attribute")]
fn custom_proc_macro_attribute_fn_ha_attribute() {
    println!("this is custom_proc_macro_attribute_fn()");
}


檢視巨集擴充套件結果

執行cargo expand

fn main() {
    custom_proc_macro_attribute_fn();
    custom_proc_macro_attribute_fn_ha_attribute();
}
fn custom_proc_macro_attribute_fn() {
    {
        ::std::io::_print(
            ::core::fmt::Arguments::new_v1(
                &["this is custom_proc_macro_attribute_fn()\n"],
                &[],
            ),
        );
    };
}
fn custom_proc_macro_attribute_fn_ha_attribute() {
    {
        ::std::io::_print(
            ::core::fmt::Arguments::new_v1(
                &["this is custom_proc_macro_attribute_fn() with attribute\n"],
                &[],
            ),
        );
    };
}

檢視TokenStream輸出

執行cargo check或cargo run。

可以看到attrs為屬性巨集傳遞的自定義屬性,input為方法內容。

// 空屬性輸出
attrs:TokenStream []
input:TokenStream [
    Ident {
        ident: "fn",
        span: #0 bytes(218..220),
    },
    Ident {
        ident: "custom_proc_macro_attribute_fn",
        span: #0 bytes(221..251),
    },
    Group {
        delimiter: Parenthesis,
        stream: TokenStream [],
        span: #0 bytes(251..253),
    },
    Group {
        delimiter: Brace,
        stream: TokenStream [
            Ident {
                ident: "println",
                span: #0 bytes(260..267),
            },
            Punct {
                ch: '!',
                spacing: Alone,
                span: #0 bytes(267..268),
            },
            Group {
                delimiter: Parenthesis,
                stream: TokenStream [
                    Literal {
                        kind: Str,
                        symbol: "this is custom_proc_macro_attribute_fn()",
                        suffix: None,
                        span: #0 bytes(269..311),
                    },
                ],
                span: #0 bytes(268..312),
            },
            Punct {
                ch: ';',
                spacing: Alone,
                span: #0 bytes(312..313),
            },
        ],
        span: #0 bytes(254..315),
    },
]

// 自定義屬性輸出
attrs:TokenStream [
    Literal {
        kind: Str,
        symbol: "this is custom_proc_macro_attribute",
        suffix: None,
        span: #0 bytes(384..421),
    },
]
input:TokenStream [
    Ident {
        ident: "fn",
        span: #0 bytes(424..426),
    },
    Ident {
        ident: "custom_proc_macro_attribute_fn_ha_attribute",
        span: #0 bytes(427..470),
    },
    Group {
        delimiter: Parenthesis,
        stream: TokenStream [],
        span: #0 bytes(470..472),
    },
    Group {
        delimiter: Brace,
        stream: TokenStream [
            Ident {
                ident: "println",
                span: #0 bytes(479..486),
            },
            Punct {
                ch: '!',
                spacing: Alone,
                span: #0 bytes(486..487),
            },
            Group {
                delimiter: Parenthesis,
                stream: TokenStream [
                    Literal {
                        kind: Str,
                        symbol: "this is custom_proc_macro_attribute_fn() with attribute",
                        suffix: None,
                        span: #0 bytes(488..545),
                    },
                ],
                span: #0 bytes(487..546),
            },
            Punct {
                ch: ';',
                spacing: Alone,
                span: #0 bytes(546..547),
            },
        ],
        span: #0 bytes(473..549),
    },
]

進階

修改定義中的程式碼,使用synquote庫來將TokenStream轉化為語法樹,再對語法樹進行處理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, Item};

#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 使用parse_macro_input!巨集將attr轉換為語法樹
    eprintln!("{:#?}", parse_macro_input!(attr as AttributeArgs));
    // 轉換成語法樹
    let body_ast = parse_macro_input!(item as Item);
    // eprintln!("{:#?}", body_ast);

    match body_ast {
        // 獲取到函式及相關屬性
        Item::Fn(ref o) => {
            eprintln!("o:{:#?}", o);
            eprintln!("vis:{:#?}", o.vis); // 可見性
            eprintln!("attrs:{:#?}", o.attrs); // 特性
            eprintln!("sig:{:#?}", o.sig); // 方法簽名
            eprintln!("block:{:#?}", o.block); // 方法體
        }
        _ => {}
    }

    // quote!巨集將語法樹重新轉換為TokenStream
    // 返回的實際結果為proc_macro2::TokenStream,要into轉換為proc_macro::TokenStream
    quote!(#body_ast).into()

    // 返回空內容,將清楚函式內容
    // quote!({}).into()
}

以上列出了部分屬性的獲取,更多屬性可以點選型別進去檢視。

派生式過程巨集

Derive macro,用於結構體(struct)、列舉(enum)、聯合(union)型別,可為其實現函式或特徵(Trait)。

定義

// 派生式過程巨集 CuctomProcMacroDerive 為巨集名稱
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    eprint!("cuctom_proc_macro_derive:{:#?}", input);   
    
    // 派生一個叫cuctom_proc_macro_derive_fn的方法
    "fn cuctom_proc_macro_derive_fn() -> i32 {100}"
        .parse()
        .unwrap()
    
    // 返回空代表沒派生內容
    // proc_macro::TokenStream::new()
}

使用

#[derive(CuctomProcMacroDerive)]
struct Student;

fn main() {
    // 派生巨集中派生的新方法,可以直接使用。
    cuctom_proc_macro_derive_fn();
}

檢視巨集擴充套件結果

可以看到展開後有了自定義的方法cuctom_proc_macro_derive_fn,這個方法我們可以直接使用。

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
struct Student;
fn cuctom_proc_macro_derive_fn() -> i32 {
    100
}
fn main() {
    cuctom_proc_macro_derive_fn();
}

檢視TokenStream輸出結果

cuctom_proc_macro_derive:TokenStream [
    Ident {
        ident: "struct",
        span: #0 bytes(59..65),
    },
    Ident {
        ident: "Student",
        span: #0 bytes(66..73),
    },
    Punct {
        ch: ';',
        spacing: Alone,
        span: #0 bytes(73..74),
    },
]

進階

修改定義中的程式碼,使用synquote庫來將TokenStream轉化為語法樹,再對語法樹進行處理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

// 派生式過程巨集 CuctomProcMacroDerive 為巨集名稱
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // 轉換成語法樹,注意這裡的型別為DeriveInput
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident; // 名稱
    let attrs = input.attrs; // 特性
    let vis = input.vis; // 可見性
    let generics = input.generics; // 泛型
    let data = input.data; // 資料

    // 生成輸出
    let expanded = quote! {
      fn cuctom_proc_macro_derive_fn() -> i32 {100}

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

    // 返回TokenStream
    expanded.into()
    // 等同於
    // proc_macro::TokenStream::from(expanded)
}

以上列出了部分屬性的獲取,更多屬性可以點選型別進去檢視。

函式式過程巨集

Function-like macro,用法與普通的規則巨集類似,但功能更加強大,可實現任意語法樹層面的轉換功能。

定義

// 函式式過程巨集 custom_proc_macro 為巨集名稱
#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    eprint!("custom_proc_macro:{:#?}", input);
    "fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()

    // proc_macro::TokenStream::new()
}

使用

fn main() {
    custom_proc_macro!();
    custom_proc_macro!(123);    

    // 巨集中定義的函式可直接使用
    custom_proc_macro_fn();
}

檢視巨集擴充套件結果

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
fn main() {
    fn custom_proc_macro_fn() -> i32 {
        100
    }
    custom_proc_macro_fn();
}

檢視TokenStream輸出結果

可以看到input內容為使用時傳入的內容。

custom_proc_macro:TokenStream []
custom_proc_macro:TokenStream [
    Literal {
        kind: Integer,
        symbol: "123",
        suffix: None,
        span: #0 bytes(233..236),
    },
]

進階

修改定義中的程式碼,使用synquote庫來將TokenStream轉化為語法樹,再對語法樹進行處理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // eprint!("custom_proc_macro:{:#?}", input);
    // "fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()
    quote!({
        fn custom_proc_macro_fn() -> i32 {
            100
        }
    })
    .into()
}

總結

本文先介紹了專案搭建以及過程巨集的定義和簡單使用及進階使用。可以看出原始程式碼都被解析成TokenStream型別的樹形結構資料,但如果我們想對TokenStream做一些處理,去直接解析、修改這樣的結構是非常複雜的。我們可以使用syn庫來將TokenStream轉化為具有語義資訊、抽象程度更高的語法樹,再對語法樹進行處理,處理完成後再使用quote返回TokenStream