一、概述
為了解決rust語法元素的擴展纪蜒,并能復用現(xiàn)有的代碼袁稽,在rust編寫的程序中普遍使用宏
.
通過宏定義和宏調(diào)用或宏引用來簡化代碼的編寫侈沪,以復用已有的代碼來擴展語法元素:
-
自定義語法元素
有時語言層面定義的語法元素有“缺陷”或“不足”欲虚,甚至用已有的語法元素來描述一段邏輯看起來比較復雜或不夠靈活等場景集灌;我們可以通過宏
來幫忙我們
# 比如rust使用print宏來實現(xiàn)c語言中printf函數(shù)的功能
print!("hello world");
# 使用vec宏來簡化vector的定義
let v = vec!["a", "b", "c"];
-
簡化復用代碼
宏本身是具有自定義語法元素的“能力”,通過使用它能夠簡化和復用代碼复哆。
# 比如給自定義的struct添加Clone和Debug trait, 在編譯時會自動為其提供Clone和Debug的缺省實現(xiàn)代碼
#[derive(Clone, Debug)]
struct StructDemo {
// 省略代碼
}
二欣喧、宏
1.定義
在rust中,宏就是一種代碼復用的機制梯找,它提供了基礎語法唆阿,并允許開發(fā)者根據(jù)需要使用這些語法基礎來自定義新的語法,然后方便其他的開發(fā)者調(diào)用或引用這些自定義的語法元素初肉。
宏由兩部分構(gòu)成:宏定義酷鸦、宏調(diào)用或宏引用饰躲。
2.分類
主要是因為涉及到自定義語法及其定義和調(diào)用的復雜性牙咏,再加上復用代碼的使用場景不同,將宏分為:聲明宏和過程宏嘹裂;
不過這兩類宏的定義方式妄壶、調(diào)用或引用方式可能各不相同,但其擴展語法元素和復用代碼的目的還是一致的寄狼。
3.宏對比(聲明宏 vs 過程宏)
定義
- 聲明宏
在rust中丁寄,聲明宏本質(zhì)就是匹配規(guī)則 + 轉(zhuǎn)譯替換規(guī)則; 或者就是“代碼模版按照匹配規(guī)則進行代碼化替換”;
聲明宏通過給定一個宏名稱泊愧,并為其指定多組輸入匹配規(guī)則及對應的轉(zhuǎn)譯替換規(guī)則伊磺;
調(diào)用聲明宏時,就是傳入一串代碼片段删咱,在編譯期由編譯期根據(jù)傳入代碼片段來匹配宏自身定義的匹配規(guī)則屑埋,再經(jīng)過轉(zhuǎn)譯替換規(guī)則,將宏調(diào)用代碼替換為轉(zhuǎn)譯后的代碼痰滋;
自定義的聲明宏未必是一個有效的rust函數(shù)摘能,但是需要確保其名稱在宏擴展階段能夠被標識并被調(diào)用续崖,匹配時是按照逐個規(guī)則“深度匹配的”,一旦不匹配就會出現(xiàn)“異惩鸥悖”严望;
匹配規(guī)則和轉(zhuǎn)移替換規(guī)則在調(diào)用時會進行分詞和解析的,其內(nèi)容是字面上可見的文字流逻恐。
# 參數(shù)“hello world”作為一個分詞后的文字量像吻,傳給println宏,并將其轉(zhuǎn)移替換為對其他函數(shù)調(diào)用
println!("hello world");
- 過程宏
相對來說過程宏就比較特殊: 定義一個過程宏首先確定其crate為proc-macro=true
复隆,然后定義一個傳統(tǒng)的rust函數(shù)萧豆,并指定它的屬性及其輸入和輸出;
需要使用提供的proc_macro
昏名、quote
涮雷、syn
等crate來實現(xiàn)過程宏的函數(shù)邏輯;
過程宏根據(jù)定義的屬性和輸入及輸出參數(shù)的不同轻局,可分為:類似函數(shù)的過程宏洪鸭、繼承過程宏、屬性過程宏仑扑, 不過他們的使用場景各不相同览爵。
聲明宏和過程宏的區(qū)別
- 1、定義方式
聲明宏通過macro_rules! macro_name {}來進行定義的镇饮,并實現(xiàn)匹配轉(zhuǎn)譯替換字面上的代碼輸入蜓竹,宏定義的邏輯編譯后存于當前的crate中;當前編譯器在編譯其他crate時使用到該crate的聲明宏時储藐,編譯期會自動加載被調(diào)用的crate中宏邏輯來實現(xiàn)轉(zhuǎn)譯替換邏輯俱济;
過程宏則是通過定義一個特殊類型的crate<帶有[lib] proc-macro=true標識>,在這個crate中定義符合特定屬性和輸入輸出參數(shù)的傳統(tǒng)rust函數(shù)钙勃,其中繼承過程宏的函數(shù)名與過程宏名可不一樣蛛碌;當編譯器編譯這個特殊的proc-macro crate時,它會生成一個動態(tài)庫辖源,并導出相關過程宏對應的rust函數(shù)蔚携;
2、調(diào)用方式
當聲明宏被定義和調(diào)用時克饶,編譯器會使用類型正則表達式的方式酝蜒,來匹配字面上的代碼并替換字面上的代碼,生成新的字面上的代碼矾湃,再進行解析生成語法樹節(jié)點亡脑;
當過程宏被編譯時,編譯器會將字面上的代碼生成TokenStream對象,并動態(tài)搜索和加載過程宏對應的動態(tài)庫远豺,然后調(diào)用對應的過程宏rust函數(shù)奈偏,并將其輸出的TokenStream對象轉(zhuǎn)換為編譯器內(nèi)部的AST語法樹節(jié)點;過程宏在定義和引用時躯护,編譯器會以調(diào)用動態(tài)庫函數(shù)的方式來實現(xiàn)語法樹節(jié)點的替換或新增的;與crate中其他語法元素的關系
包含過程宏定義的crate不會被鏈接到使用它的crate中惊来,往往只會通過被編譯器動態(tài)調(diào)用,而這個crate往往不要包括需要鏈接開發(fā)者的lib或bin等其他rust語法元素中棺滞,比如fn/trait等裁蚁;
包含聲明宏定義的crate可被包含在調(diào)用它的crate中,它可包含需要鏈接開發(fā)者的lib或bin中的所有rust語法元素,比如fn/trait等;
4继准、宏與函數(shù)的區(qū)別
聲明宏沒有對應Rust函數(shù)枉证,語法上 MacroDef
、MacCall
分別屬于不同的ItemKind
移必,類似Mod室谚、Fn、Struct等也是一種ItemKind;
過程宏則對應一個傳統(tǒng)Rust函數(shù)崔泵,并帶上不同的屬性#[proc_macro]秒赤、#[proc_macro_derive(name_xx)]、#[proc_macro_attribute]憎瘸,不過其調(diào)用者往往來自于編譯器等特定用途的程序入篮,不會是普通開發(fā)者開發(fā)的程序;
另外不管是聲明宏調(diào)用還是過程宏的調(diào)用幌甘,往往發(fā)生在編譯器編譯階段
潮售,而傳統(tǒng)Rust函數(shù)的調(diào)用則發(fā)生在用戶程序的運行階段
;
由于定義和實現(xiàn)過程宏的過程比較復雜锅风,往往涉及到對proc_macro酥诽、quote、syn crate的了解和使用遏弱,所以定義一個過程宏盆均,相對來講比較復雜塞弊,但如能掌握它們抽象出來的概念的話漱逸,使用起來也會非常直接和明了
三、示例
1游沿、聲明宏
- 語法定義
//
macro_rules! macro_name {
// 省略規(guī)則
匹配規(guī)則 => {}
.....
}
定義語法(部分)
匹配規(guī)則是除了$饰抒、{}、()诀黍、[]之外的token組成的序列;
轉(zhuǎn)譯替換規(guī)則是一個分隔的TokenTree;
Syntax
MacroRulesDefinition :
macro_rules ! IDENTIFIER MacroRulesDef
MacroRulesDef :
( MacroRules ) ;
| [ MacroRules ] ;
| { MacroRules }
MacroRules :
MacroRule ( ; MacroRule )* ;?
MacroRule :
MacroMatcher => MacroTranscriber
MacroMatcher :
( MacroMatch* )
| [ MacroMatch* ]
| { MacroMatch* }
MacroMatch :
Token except $ and delimiters
| MacroMatcher
| $ IDENTIFIER : MacroFragSpec
| $ ( MacroMatch+ ) MacroRepSep? MacroRepOp
MacroFragSpec :
block | expr | ident | item | lifetime | literal
| meta | pat | path | stmt | tt | ty | vis
MacroRepSep :
Token except delimiters and repetition operators
MacroRepOp :
* | + | ?
MacroTranscriber :
DelimTokenTree
匹配規(guī)則中包含meta變量用$標識來標示袋坑,其類型包括block、expr眯勾、ident枣宫、item婆誓、lifetime、literal也颤、meta洋幻、pat、path翅娶、stmt文留、tt、ty竭沫、vis;
簡單示例如下:
/// test宏定義了兩組匹配和轉(zhuǎn)譯替換規(guī)則
macro_rules! test_macro {
($left:expr; and $right:expr) => {
println!("{:?} and {:?} is {:?}",
// $left變量的內(nèi)容對應匹配上的語法片段的內(nèi)容
stringify!($left),
// $right變量的內(nèi)容對應匹配上的語法片段的內(nèi)容
stringify!($right),
$left && $right)
};
($left:expr; or $right:expr) => {
println!("{:?} or {:?} is {:?}",
stringify!($left),
stringify!($right),
$left || $right)
};
}
測試
/// 傳入的字面上的代碼片段燥翅,解析后生成的語法片段,
/// - 在解析過程中進行簡易分詞和解析后生成一個語法片段(包含解析出來的不同類型及其對應的值)
/// - 與聲明宏中定義的匹配規(guī)則包含的字面量token和meta變量類型等蜕提,按照從左到右一對一的方式進行匹配(匹配都是進行深度匹配的森书,一旦當前規(guī)則匹配過程出現(xiàn)問題,則不會再進行后續(xù)的規(guī)則匹配)
/// - 一旦提供的語法片段和某個聲明宏定義的規(guī)則匹配了谎势,那么對應類型的值綁定到meta變量中拄氯,即用$標示來代替;
/// 再匹配后,進入轉(zhuǎn)譯替換階段它浅,直接讀取對應的轉(zhuǎn)譯替換規(guī)則的內(nèi)容译柏,將其meta變量的內(nèi)容用上一階段綁定過來的值替換,完成處理后輸出即可;
/// 正好能匹配上第一個匹配規(guī)則姐霍;
/// - 第一個匹配規(guī)則為
/// 一個表達式類型語法片段和; and 和另一個表達式類型語法片段
/// 其中;和and需要字面上一對一匹配鄙麦;
test_macro!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
/// 下面?zhèn)魅氲淖置嫔系拇a片段,解析后生成的語法片段镊折,
/// 正好能匹配上第二個匹配規(guī)則胯府;
/// - 第二個匹配規(guī)則為:
/// 一個表達式類型語法片段和; or 和另一個表達式類型語法片段
/// 其中;和or需要字面上一對一匹配;
test_macro!(true; or false);
在聲明宏中為了簡化表達重復具有相同類型的meta變量恨胚,就使用特別符號來描述相關規(guī)則
- 代表任何數(shù)量的重復骂因,數(shù)量可以是0個;
- +代表任何數(shù)據(jù)的重復, 數(shù)量至少有1個赃泡;
- 寒波?代表可選的一個變量, 0或最多一個;
大概的樣式:var: metatype),*升熊,其中,可省略俄烁; $()代表一個分組;
macro_rules! find_min {
($x:expr) => ($x);
// $x語法表達式,后面跟上至少一個語法表達式$y
($x:expr, $($y:expr),+) => (
// 將重復的匹配上的語法表達式$y至少一個或多個
// 遞歸傳給find_min宏,$x直接傳給方法min
std::cmp::min($x, find_min!($($y),+))
)
}
樣例驗證
println!("{}", find_min!(1u32));
println!("{}", find_min!(1u32 + 2, 2u32));
println!("{}", find_min!(5u32, 2u32 * 3, 4u32));
當需要在聲明宏的輸出內(nèi)容中引用宏定義所在的crate的標識级野,則需要使用$crate::ident_name來輸出页屠;
聲明宏調(diào)用時傳入的字面上代碼片段,分詞解析后就不會有crate類型;而聲明宏輸出的內(nèi)容包括的各種標識符,應在調(diào)用該聲明宏的crate中找到其定義辰企,否則宏輸出編譯會出錯;
// 宏thd_name會使用當前宏定義crate中的get_tag_from_thread_name方法
#[macro_export]
macro_rules! thd_name {
($name:expr) => {{
$crate::get_tag_from_thread_name()
.map(|tag| format!("{}::{}", $name, tag))
.unwrap_or_else(|| $name.to_owned())
}};
}
接下來看看聲明宏的可見范圍:
聲明宏的定義屬于一個Item风纠,其宏的定義可以在一個crate,而調(diào)用宏可以在另一個不同的crate中牢贸; 那么理論上可以存在于crate的mod中议忽,或任何crate中可以出現(xiàn)Item的地方并被使用;但是由于歷史原因和聲明宏沒有象其他Item的可見屬性pub等十减,聲明宏的可見范圍及調(diào)用路徑方式與傳統(tǒng)Item不一樣栈幸;
其規(guī)則如下
:
- 1.若沒有使用帶路徑的方式來調(diào)用聲明宏,則直接在當前代碼塊范圍來匹配相關宏的名稱帮辟,并進行調(diào)用速址,如果沒有找到則從帶有路徑的范圍中查找;
- 2.若使用帶路徑的方式來調(diào)用聲明宏由驹,則直接從帶路徑的范圍來查找芍锚,而不從當前字面范圍來匹配查找;
- 3.聲明宏定義后的可見范圍與let變量的可見范圍類似蔓榄,在它定義的代碼塊范圍及子范圍中可直接引用它或覆蓋定義它并炮;
- 4.如想在大于定義它的代碼塊范圍中使用它,則需要使用宏導出導入甥郑;
use lazy_static::lazy_static;//帶路徑方式導入的宏
macro_rules! lazy_static { //當前代碼塊范圍定義的宏
(lazy) => {};
}
// 沒有帶路徑的調(diào)用方式逃魄,直接從當前代碼塊范圍來找到當前定義的宏
lazy_static!{lazy}
// 帶路徑的調(diào)用方式,忽略當前代碼塊定義的宏澜搅,找到導入的宏
self::lazy_static!{}
/// src/lib.rs
mod has_macro {
// m!{} // 錯誤:當前代碼塊中宏沒有定義.
macro_rules! m {
() => {};
}
m!{} // OK: 在當前代碼塊中已定義m宏.
mod uses_macro;
}
// 錯誤: 當前代碼塊中并沒有定義宏m伍俘,而是在其子mod has_macro塊中有定義;
// m!{}
/// 另一個src/has_macro/uses_macro.rs文件勉躺,被引用到has_macro mod中
m!{} // OK: 宏m的定義在src/lib.rs中的has_macro mod中
// 宏在mod代碼塊中的定義范圍
macro_rules! m {
(1) => {};
}
m!(1);// 當前代碼塊范圍有宏m定義
mod inner {
m!(1); // 當前代碼塊的父mod中有定義宏m癌瘾,可以直接引用
macro_rules! m { // 覆蓋父mod中定義的宏m
(2) => {};
}
// m!(1); // 錯誤: 沒有匹配'1'的規(guī)則,原來的已被覆蓋
m!(2); // 當面代碼塊有定義宏m
macro_rules! m {
(3) => {};
}
m!(3); // 當面代碼塊有定義宏m饵溅,原來的已被覆蓋
}
m!(1);//當面代碼塊有定義宏m
// 宏在函數(shù)代碼塊中的定義范圍
fn foo() {
// m!(); // 錯誤: 宏m在當前代碼塊沒有定義.
macro_rules! m {
() => {};
}
m!();// 當前代碼塊范圍有宏m定義
}
// m!(); // 錯誤: 宏m不在當前代碼塊范圍中定義.
使用導出#[macro_export]和導入#[macro_import]的用法妨退,來“放大”聲明宏的可見范圍;
一般說來蜕企,宏定義后沒有帶路徑的調(diào)用方式咬荷,只有當一個宏定義時加上宏導出屬性#[macro_export],即代表將其定義的代碼塊范圍提升到crate級別范圍糖赔;
在一個宏被導出后萍丐,當前crate中的其他mod可以使用帶路徑的方式來調(diào)用它;
在一個宏被導出后放典,其它crate可以使用宏導入屬性#[macro_use]的方式,將其中導出宏名稱導入到當前crate范圍中;
對于同一crate中不同的mod中定義的宏奋构,可以使用#[macro_use]方式來提升定義宏的可見范圍壳影,而無須使用[macro_export];
self::m!(); // OK:帶路徑的調(diào)用方式,會查找當前crate中導出的宏
m!(); // OK: 不帶路徑的調(diào)用方式弥臼,會查找當前crate中導出的宏
mod inner {
// 子mod塊范圍使用帶路徑方式調(diào)用宴咧,在當前crate中可找到導出的宏
super::m!();
crate::m!();
}
mod mac {
#[macro_export]
// 子mod塊范圍中定義的宏m導出到當前crate中
macro_rules! m {
() => {};
}
}
// 導入外部crate中的宏m或者使用#[macro_use]來導入其所有導出的宏.
#[macro_use(m)]
extern crate lazy_static;
m!{} // 外部crate宏已導入到當前crate
// self::m!{} // 錯誤: m沒有在`self`中定義
2、過程宏
- 類似函數(shù)的過程宏
其對應函數(shù)聲明中的輸入?yún)?shù)item是proc_macro crate中定義的TokenStream径缅,由調(diào)用時傳遞過來的字面上的代碼生成掺栅,它內(nèi)部包含結(jié)構(gòu)化的Token流,使用相關接口可以訪問指定Token等纳猪;其對應函數(shù)聲明中的輸出是proc_macro crate中定義的TokenStream氧卧,字面上的代碼串可以通過parse方法來生成;
類似函數(shù)的過程宏氏堤,使用時類似聲明宏調(diào)用方式沙绝,傳入代碼片段,調(diào)用后的輸出結(jié)果會替換調(diào)用過程宏這個語法元素耕渴,類似聲明宏調(diào)用颁股;
類似函數(shù)的過程宏的名稱與對應的函數(shù)聲明一致派昧,它可應用在任何聲明宏可被調(diào)用的地方;
其示例如下:
// 過程宏定義
extern crate proc_macro;
use proc_macro::TokenStream;
// 過程宏輸出的TokenStream中包含fn answer定義及實現(xiàn)
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
// 過程宏調(diào)用
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!(); // 類似函數(shù)簡易宏的調(diào)用
fn main() {
println!("{}", answer());// 直接調(diào)用過程宏輸出的fn answer
}
- 繼承過程宏
其對應函數(shù)聲明中的輸入?yún)?shù)item是附加有指定過程宏屬性的整個自定義類型Item對應的TokenStream粗悯,
其對應函數(shù)聲明中的輸出是一個獨立的Item對應的TokenStream,它與自定義類型Item屬于同一個mod或block中同欠;
繼承過程宏的使用是以屬性#[derive(過程宏名)]的方式出現(xiàn)在struct为黎、enum、union自定義類型聲明中行您;
繼承過程宏的名稱包含在對應函數(shù)的屬性中铭乾,可與對應函數(shù)名不同;
其示例如下:
// 過程宏定義
extern crate proc_macro;
use proc_macro::TokenStream;
// 定義一個屬性過程宏名稱為AnserFn的過程宏
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
// 過程宏引用
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
// 將過程宏AnswerFn引用到struct聲明定義中
// 編譯時觸發(fā)過程宏對應函數(shù)調(diào)用娃循,生成fn answer
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());// 直接調(diào)用過程宏輸出的fn answer
}
帶自定義屬性名稱的過程宏炕檩,過程宏的函數(shù)實現(xiàn)可對_item中是否有自定義屬性進行檢查和判斷等
/// 定義一個屬性過程宏名稱為HelperAttr的過程宏,
/// 并且支持輸入的item定義中包含名稱為helper的屬性
#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
#[derive(HelperAttr)]
struct Struct {
#[helper] // 與自定義attributes中的屬性名helper對應
field:()
}
4.屬性過程宏
其對應函數(shù)聲明中的輸入?yún)?shù)attr是指屬性的內(nèi)容對應的TokenStream捌斧;
輸入?yún)?shù)item笛质,是指附加有指定屬性過程宏的屬性的自定義類型Item對應的TokenStream,但不包括屬性部分捞蚂,屬性部分已在attr參數(shù)中體現(xiàn);
attr和item內(nèi)部包含結(jié)構(gòu)化的Token流妇押,使用相關接口可以訪問指定Token等;
其對應函數(shù)聲明中的輸出是proc_macro crate中定義的TokenStream姓迅,字面上的代碼串可以通過parse方法來生成敲霍;
屬性過程宏俊马,使用屬性#[屬性過程宏名稱]方式來引用過程宏,引用后的輸出結(jié)果會替換引用過程宏這個語法元素肩杈,類似簡易宏調(diào)用柴我;
屬性過程宏的名稱與對應的函數(shù)聲明一致;
其示例如下:
/// 定義一個名稱為show_streams的屬性過程宏
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream)
-> TokenStream {
/// 調(diào)用宏時打印輸出attr
println!("attr: \"{}\"", attr.to_string());
/// 調(diào)用宏時打印輸出item
println!("item: \"{}\"", item.to_string());
/// 調(diào)用宏時輸出原來輸出的item
item
}
/// 引用屬性過程宏
// src/lib.rs
extern crate my_macro;
use my_macro::show_streams;
// Example: Basic function
#[show_streams]
fn invoke1() {}
// out: attr: ""
// out: item: "fn invoke1() { }"
// Example: Attribute with input
#[show_streams(bar)]
fn invoke2() {}
// out: attr: "bar"
// out: item: "fn invoke2() {}"
// Example: Multiple tokens in the input
#[show_streams(multiple => tokens)]
fn invoke3() {}
// out: attr: "multiple => tokens"
// out: item: "fn invoke3() {}"
宏調(diào)試
Rust中對宏的調(diào)用或引用扩然,往往在編譯器生成完整語法樹階段完成艘儒,宏調(diào)用的結(jié)果是否正常或有效夫偶,后面還會進行嚴格的類型和借用檢查等界睁;
# 對單個 rs 文件
rustc -Z unpretty=expanded hello.rs
# 對項目里的二進制 rs 文件
cargo rustc --bin hello -- -Z unpretty=expanded