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),
},
]
進階
修改定義中的程式碼,使用syn
和quote
庫來將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),
},
]
進階
修改定義中的程式碼,使用syn
和quote
庫來將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),
},
]
進階
修改定義中的程式碼,使用syn
和quote
庫來將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
。