深入瞭解 Rust 過程宏 - 2
過程宏 API
過程宏主體
首先讓我們澄清什麼是過程宏主體,在函數宏的情況下,主體就是圓括號之間的所有內容:
在派生宏的情況下,主體是整個帶屬性的 struct:
對於屬性宏,主體包含整個條目 (fn some_item(){})。屬性本身作爲附加屬性傳遞給函數:
爲了說明這一點,我們將研究一個標識宏,它只返回它接受的主體,而不做其他任何事情:
1extern crate proc_macro;
2use proc_macro::TokenStream;
3
4#[proc_macro]
5pub fn foo(body: TokenStream) -> TokenStream {
6 return body
7}
假設我們有一個調用 hello() 的程序,其中 hello 使用 foo! 宏。在這種情況下,foo 宏看起來就像沒有使用宏:
1use my_proc_macro::*;
2 foo! {
3 fn hello() {
4 println!("Hello, world!");
5 }
6 }
7
8fn main() {
9 hello();
10}
類似地,這可以用屬性宏來編寫:
1extern crate proc_macro;
2use proc_macro::TokenStream;
3
4#[proc_macro_attribute]
5pub fn baz(
6 attr: TokenStream,
7 item: TokenStream
8) -> TokenStream {
9 return item
10}
11…
12use my_proc_macro::*;
13#[baz]
14fn hello() {
15 println!("Hello, world!");
16}
17fn main() {
18 hello();
19}
Tokens,TokenStream,TokenTree
過程宏的主體被分爲多個標記:
Token 是一個特定類型的字符串,在宏主體解析期間給它賦值。有三種類型的 Token:標識符、標點符號和文字。
過程宏使用 proc_macro crate 的數據類型進行操作, 它是標準庫的一部分,在編譯過程宏時自動鏈接。TokenTree 是這些特殊類型之一,表示所有可能的 Token 類型枚舉:
1struct TokenStream(Vec<TokenTree>);
2enum TokenTree {
3 Ident(Ident),
4 Punct(Punct),
5 Literal(Literal),
6 ...
7}
另一個數據結構 TokenStream 表示 Token 列表,允許你迭代 Token 列表 (body.into_iter()):
1#[proc_macro]
2pub fn foo(body: TokenStream) -> TokenStream {
3 for tt in body.into_iter() {
4 match tt {
5 TokenTree::Ident(_) => eprintln!("Ident"),
6 TokenTree::Punct(_) => eprintln!("Punct"),
7 TokenTree::Literal(_) => eprintln!("Literal"),
8 _ => {}
9 }
10 }
11 return TokenStream::new();
12}
在 TokenTree 中還有一種枚舉變體,稱爲 Group:
1enum TokenTree {
2 Ident(Ident),
3 Punct(Punct),
4 Literal(Literal),
5 Group(Group),
6}
當解析器遇到括號時,就會出現 Group。組成 Group 的括號可以是圓的、方的或大括號。
例如,具有以下主體的宏:
foo!( foo { 2 + 2 } bar );
將被解析爲兩個標識符 foo 和 bar 和一個組 {2+2}。這裏的一組包括大括號和另一個 TokenStream(文字 2 和 2,以及一個標點符號 +):
我們可以看到 TokenStream 並不是嚴格意義上的流。這是一種樹,每個節點都由括號組成,葉子代表單個符號。
如何編寫過程宏
讓我們編寫一個簡單的過程宏,它將擴展爲一個函數調用,並傳遞所有的參數給它:
我們可以這樣寫:
1#[proc_macro]
2pub fn foo(body: TokenStream) -> TokenStream {
3 return [
4 TokenTree::Ident(Ident::new("foo", Span::mixed_site())),
5 TokenTree::Group(Group::new(Delimiter::Parenthesis, body))
6 ].into_iter().collect();
7}
在上面的代碼中,我們做了如下操作:
-
創建包含兩個元素的數組:
1) 標識符 foo: Ident(Ident::new(“foo”,Span::mixed_site()))
- 一個帶圓括號的組,在其中放置宏主體 group (group::new(Delimiter::Parenthesis, body))。注意,body 是從 foo 調用傳遞過來的:foo(body: TokenStream)。
-
將創建的數組轉換成 TokenStream 中
現在宏可以這樣調用:
fn main() {
foo!(1, 2);
}
讓我們看看如何處理宏,宏主體是 1,2。當展開時,主體將被包裝在圓括號中,並在前面加上 foo,這樣它看起來就像一個函數調用:
Spans
爲什麼過程宏 API 需要 TokenStream 和 TokenTree?爲什麼原始字符串不夠?我們可以考慮這樣的代碼 (這是行不通的):
1#[proc_macro]
2pub fn foo(body: String) -> String {// this doesn't work!
3 format!("foo({})", body)
4}
爲了理解爲什麼上面的代碼不起作用,我們需要回到 Token struct。
除了類型和實際的符號字符串,Token struct 還包括一個 Span:
Span 包含關於 Token 在原始代碼中的位置信息,這對於編譯器正確地顯示錯誤是必要的。
例如,我們可以使用相同的宏,並有意地將字符串而不是數值傳遞給調用:
因爲函數需要一個 i32 值,所以編譯器會報告一個錯誤。但是編譯器會在哪裏報告錯誤呢?如果它是一個普通的函數調用,它會在 Token 處顯示錯誤,而不是在整個宏調用處:
error[E0308]: mismatched types
--> src/main.rs:26:13
|
26 | foo!(1, "");
| ^^ expected `i32`, found `&str`
這是可能的,因爲我們將整個 TokenStream 傳遞到擴展中,每個 Token 包含一個 Span。Span 通知編譯器,這個特定的代碼片段應該映射到宏展開的那個代碼片段。通過這種方式,編譯器可以顯示在編譯擴展代碼期間發生的錯誤。
現在總結一下,程序宏結構是由以下模塊構建的:
-
TokenStream,它是 TokenTrees 的向量
-
TokenTree,是 3 個 Token 類型加上一個 Group 的枚舉
-
Group,由括號組成
-
每個 Token 都有一個 Span,用於錯誤映射。
本文翻譯自:
https://blog.jetbrains.com/rust/2022/03/18/procedural-macros-under-the-hood-part-i/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/h6VByElVgwcdx0OyHhchkQ