柚子快報(bào)激活碼778899分享:2311rust過程宏的示例
柚子快報(bào)激活碼778899分享:2311rust過程宏的示例
原文
Rust2018中的過程宏
在Rust2018版本中,我最喜歡的功能是過程宏.在Rust中,過程宏有著悠久而傳奇的歷史(并繼續(xù)擁有傳奇的未來!) 因?yàn)?018年版極大改善了定義和使用它們的體驗(yàn).
什么是過程宏
過程宏是,在編譯時(shí)用一段語法,生成新語法的函數(shù).Rust2018中的過程宏有三個(gè)風(fēng)格:
1,自Rust1.15以來,#[derive]模式宏一直很穩(wěn)定,并把#[derive(Debug)]的所有優(yōu)點(diǎn)和易用性也帶到了用戶定義的特征中,如Serde的#[derive(Deserialize)]. 2,函數(shù)式宏,在2018版中是新的穩(wěn)定版本,并允許在基于crates.io的庫中定義:
env!("FOO")
format_args!("...")
宏.類似macro_rules!宏.
3,我最喜歡的屬性宏,也是2018版中的新功能,它允許在Rust函數(shù)上提供輕量注解,來編譯時(shí)語法轉(zhuǎn)換代碼.
可在清單中用proc-macro=true指定宏.使用時(shí),Rust編譯器會(huì)加載過程宏,并在展開調(diào)用時(shí)執(zhí)行它. 即Cargo可控制過程宏的版本,且可像其他Cargo依賴項(xiàng)一樣輕松使用它們!
定義過程宏
這三類的定義方式略微不同,在此以屬性宏為例.首先,標(biāo)記Cargo.toml:
[lib]
proc-macro = true
然后在src/lib.rs中,可編寫宏:
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
//...
}
然后可在tests/smoke.rs中編寫單元測(cè)試:
#[my_crate::hello]
fn wrapped_function() {}
#[test]
fn works() {
wrapped_function();
}
…就這樣!執(zhí)行cargo test的測(cè)試時(shí),Cargo編譯過程宏.之后,它編譯編譯時(shí)加載宏的單元測(cè)試,執(zhí)行hello函數(shù)并編譯生成的語法.
可見過程宏的幾個(gè)重要屬性:
1,輸入/輸出是TokenStream類型 2,編譯時(shí)可執(zhí)行任意代碼,即幾乎不受限! 3,過程宏與模塊系統(tǒng)整合,即可像其他名字一樣導(dǎo)入.
先深入了解其中的一些要點(diǎn).
宏和模塊系統(tǒng)
宏現(xiàn)在與Rust中的模塊系統(tǒng)整合.表明導(dǎo)入宏時(shí)不再需要笨拙的#[macro_use]屬性!不是:
#[macro_use]
extern crate log;
fn main() {
debug!("hello, ");
info!("world!");
}
你可以如下:
use log::info;
fn main() {
log::debug!("hello, ");
info!("world!");
}
好處不僅限于!風(fēng)格的macro_rules宏,因?yàn)楝F(xiàn)在可轉(zhuǎn)換如下代碼:
#[macro_use]
extern crate serde_derive;
#[derive(Deserialize)]
struct Foo {
//...
}
//為
use serde::Deserialize;
#[derive(Deserialize)]
struct Foo {
//...
}
甚至不需要顯式依賴Cargo.toml中的serde_derive!,只需要:
[dependencies]
serde = { version = '1.0.82', features = ['derive'] }
TokenStream內(nèi)部
神秘的TokenStream類型,來自編譯器提供的proc_macro倉庫. 首次添加TokenStream時(shí),只能調(diào)用to_string()或parse()來回來轉(zhuǎn)換其為串或從串轉(zhuǎn)換. 從Rust2018開始,可直接操作TokenStream中的令牌.
TokenStream"只是"TokenTree上的一個(gè)迭代器.Rust中的所有語法都分四類,即TokenTree的四種變體: 1,Ident是如foo或bar的標(biāo)識(shí).它還包含如self和super的關(guān)鍵字. 2,字面(Literal)包括像1,"foo"和"b"等內(nèi)容.所有字面都是表示程序中常量值的一個(gè)令牌. 3,Punct表示標(biāo)點(diǎn)符號(hào),而不是分隔符.
如.是foo.bar字段訪問中的Punct令牌.像=>此多符標(biāo)點(diǎn)符號(hào)表示為兩個(gè)Punct標(biāo)記,一個(gè)表示=,一個(gè)表示>,Spacing枚舉表示=與>相鄰.
4,Group是"樹"項(xiàng)最相關(guān)的地方,因?yàn)镚roup代表一個(gè)分隔的子令牌流.如,(a,b)是以括號(hào)作為分隔符的Group,內(nèi)部令牌流是a,b.
最小化TokenTree對(duì)穩(wěn)定性至關(guān)重要. 穩(wěn)定Rust的AST是不可行的,因?yàn)槟潜硎静荒芨淖兯?(想像假如如果不能添加?符號(hào)).
用TokenStream與過程宏通信,在同時(shí)可編譯和處理較舊過程宏時(shí),編譯器可添加新的語言語法.不過,先看看如何從TokenStream中取有用的信息.
解析TokenStream
但,只需要看看syn倉庫. 使用syn倉庫,可用單行代碼解析RustAST:
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let name = &input.ident;
let abi = &input.abi;
//...
}
syn倉庫不僅可解析內(nèi)置語法,且還可輕松地為自己的語法編寫遞歸下降解析器.更多.
生成TokenStream
不僅要以TokenStream作為過程宏的輸入,還要生成TokenStream作為輸出.一般要求輸出是有效的Rust語法,但與輸入一樣,它只是要構(gòu)建的令牌列表. 創(chuàng)建TokenStream的唯一方法是通過其FromIterator實(shí)現(xiàn),即必須逐個(gè)創(chuàng)建每個(gè)令牌并聚集它到TokenStream中. 不過,這很乏味,所以看看syn的quote兄弟倉庫. quote倉庫是Rust的準(zhǔn)引用實(shí)現(xiàn),主要提供了一個(gè)方便的宏:
use quote::quote;
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let name = &input.ident;
//輸入函數(shù)總是等價(jià)于返回`42`,對(duì)不?
let result = quote! {
fn #name() -> u32 { 42 }
};
result.into()
}
quote!宏這里允許你編寫大部分Rust語法,并用#foo從環(huán)境中快速插值變量.
令牌和跨度(Span)
也許Rust2018中過程宏的最大特性是可自定義和使用每個(gè)令牌上的Span信息,這樣可從過程宏中取得驚人的語法錯(cuò)誤消息:
error: expected `fn`
--> src/main.rs:3:14
|
3 | my_annotate!(not_fn foo() {});
| ^^^^^^
及完全自定義的錯(cuò)誤消息:
錯(cuò)誤:`導(dǎo)入`方法必須至少`有一個(gè)`參數(shù)
--> invalid-imports.rs:12:5
|
12 | fn f1();
| ^^^^^^^^
Span可看作是原始源文件的指針,一般表示,foo,"Ident令牌來自文件bar.rs,第4行第5列,長(zhǎng)度為3個(gè)字節(jié)". 此信息主要由包含警告和錯(cuò)誤消息的編譯器診斷使用.
在Rust2018中,每個(gè)TokenTree都有個(gè)與之關(guān)聯(lián)的Span.即,如果把所有輸入令牌的Span保留到輸出中,則即使生成全新語法,編譯器的錯(cuò)誤消息仍是準(zhǔn)確的! 如,如下一個(gè)小宏:
#[proc_macro]
pub fn make_pub(item: TokenStream) -> TokenStream {
let result = quote! {
pub #item
};
result.into()
}
按如下調(diào)用:
my_macro::make_pub! {
static X: u32 = "foo";
}
是無效的,因?yàn)閺膽?yīng)該返回u32的函數(shù)返回一個(gè)串,編譯器幫助診斷問題為:
`error[E0308]`:`類型`不匹配
--> src/main.rs:1:37
|
1 | my_macro::make_pub!(static X: u32 = "foo");
| ^^^^^ expected u32, found reference
|
=注意:期望類型為`"U32"`
找到類型`'&'staticstr'`
錯(cuò)誤:因?yàn)樯弦粋€(gè)錯(cuò)誤而中止
在此可見,盡管正在生成全新語法,但編譯器可保留span信息,以繼續(xù)為編寫代碼提供針對(duì)性的診斷.
生態(tài)中的過程宏
syn,quote和proc-macro2是編寫過程宏的首選庫.方便自定義解析器,解析現(xiàn)有語法,創(chuàng)建新語法,使用舊版本的Rust等等!
Serde這里及Serialize和Deserialize的繼承宏可能是生態(tài)中最常用的宏.它們有令人印象深刻的配置量,是小注解但強(qiáng)大的很好示例.
wasm-bindgen項(xiàng)目在Rust中,使用屬性宏輕松定義接口,并從JS導(dǎo)入接口. #[wasm_bindgen]輕量注解方便理解傳入和傳出內(nèi)容,并刪除了大量轉(zhuǎn)換樣板.
gobject_gen!宏是GNOME項(xiàng)目的實(shí)驗(yàn)性IDL,來在Rust中安全地定義GObject對(duì)象,避免手寫來與C語言通信,并用Rust寫其他GObject實(shí)例交互期望的所有膠水.
Rocket框架最近切換到了過程宏,并展示了過程宏的一些最新功能,如自定義診斷,自定義跨度創(chuàng)建等.
柚子快報(bào)激活碼778899分享:2311rust過程宏的示例
相關(guān)文章
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。