深入瞭解 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   }}
 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}

在上面的代碼中,我們做了如下操作:

現在宏可以這樣調用:

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 通知編譯器,這個特定的代碼片段應該映射到宏展開的那個代碼片段。通過這種方式,編譯器可以顯示在編譯擴展代碼期間發生的錯誤。

現在總結一下,程序宏結構是由以下模塊構建的:

本文翻譯自:

https://blog.jetbrains.com/rust/2022/03/18/procedural-macros-under-the-hood-part-i/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/h6VByElVgwcdx0OyHhchkQ