---
tags: Rust
---
# Rust: Procedural Macros
:::warning
此文章是對 Procedural Macros 相關文章閱讀後的記錄,若有發現內容錯誤或者用字上不精準之處,歡迎指正。
:::
Procedural Macros 是 Rust 語言中一個強大的語法。Procedural Macros 可以被視為是一種將 AST 轉成另一個 AST 的函數,允許在編譯時期將 Rust 語法解析並轉換成另一段 Rust 語法,它又可以被分為三種類型:
* [Function-like macros](https://doc.rust-lang.org/reference/procedural-macros.html#function-like-procedural-macros)
* [Derive macros](https://doc.rust-lang.org/reference/procedural-macros.html#derive-macros)
* [Attribute macros](https://doc.rust-lang.org/reference/procedural-macros.html)
作為解析語法的函數,Procedural Macros 會返回 syntax、panic 或者進入無限迴圈。返回 syntax 即根據 Procedural Macros 的類型替換或添加新的 syntax,panic 則被 compiler 轉化為 compiler error,而無限迴圈導致編譯時期的停滯。
## 如何定義 procedural macro?
procedure macro 必須作為 library 存在,在 Cargo.toml 中寫入下面兩行可以啟用:
```
[lib]
proc-macro = true
```
定義 procedural macro 的語法如下
```rust
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn hello(attr: TokenStream, item: TokenStream) -> TokenStream {
// ...
}
```
我們可以看到 procedural macro 的輸入/ 輸出是 [`TokenStream`](https://doc.rust-lang.org/proc_macro/struct.TokenStream.html) 類型,後續我們會探討其內容為何。
## Macros 與 module system
procedural macro 與 module system 是可以相互合作的,這意味著它們可以像函式名稱等 public symbol 被 import。
簡單來說,相對於使用 `#[macro_use]` 使被註解的 module 中之 macros 應用到當前作用域中。
```rust
#[macro_use]
extern crate serde_derive;
#[derive(Deserialize)]
struct Foo {
// ...
}
```
取而代之的是透過 Rust 的 module system 來導入。
```rust
use serde::Deserialize;
#[derive(Deserialize)]
struct Foo {
// ...
}
```
這使得可以更好的管理作用域中 symbol(避免 import 無用的 symbol 而導致 symbol 容易 conflict)
> 參考 [Defining Modules to Control Scope and Privacy](https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html) 可以理解 Rust 的作用域和 module system 之基本觀念
## `TokenStream` 是什麼?
`TokenStream` 可以視為是 [`TokenTree`](https://doc.rust-lang.org/stable/proc_macro/enum.TokenTree.html) 為元素的陣列(功能上可以想像成是 `Vec<TokenTree>`,實際上複製成本比 `Vec` 更低)。一段 Rust 的程式碼可以被解析成一堆 token 的集合,則每個 token 都屬於 `TokenTree` 的四個類別之一:
* `Ident`: identifier like foo or bar. This also contains keywords such as self and super(變數名稱、函數名稱 )
* `Literal`: include things like 1, "foo", and 'b'. All literals are one token and represent constant values in a program(恆定的值)
* `Punct`: some form of punctuation that's not a delimiter(非分號的標點符號)
* `Group`: a delimited sub-token-stream(括號符號包圍的一段 token stream)
由於 procedural macros 使用的輸入是 `TokenStream` 而非 Rust AST tree,這提高了穩定性,允許編譯器能夠添加新的語言語法的同時,還能夠編譯和使用舊的 procedural macros。
如果 `TokenStream` 只是一個簡單的 vector,意味著我們首先需要撰寫 parser 來將語法變成理想的輸出。我們可以透過 [syn](https://crates.io/crates/syn) 這個 crate 來將解析的流程簡單化。解析完輸入的 `TokenStream` 之後,我們需要產生相對應的輸出,這時候則可以加上 [quote](https://crates.io/crates/quote) crate。
此外,每個 token 具有一個相關聯的 `Span` 結構,`Span` 代表了 procedural macros 處理前的原始程式碼的區間,攜帶這些程式碼的信息可以幫助 compiler 更好的提示錯誤的地方。
## Function-like macros
Function-like macros 是使用 macro invocation operator (`!`) 調用的 procdural macros。使用方法就像一般的 macro 類似,只是 parsing 的流程和輸出是撰寫程式者自行定義的。該類型 macro 的 prototype 為 `(TokenStream) -> TokenStream`,將輸入的 `TokenStream` 進行分析後,輸出 `TokenStream` 替換 macro 的調用。
如下的語法案例,輸入的 `TokenStream` 會被忽略,並且調用 macro 後會建立一個
名為 `answer` 的 function:
```rust
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
```
調用方式如下:
```rust
extern crate proc_macro_examples;
use proc_macro_examples::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
```
## Derive macros
Derive macros 定義如何解析 struct, enum, union 結構並添加新的 [item](https://doc.rust-lang.org/reference/items.html)。該類型 macro 的 prototype 為 `(TokenStream) -> TokenStream`,輸入的 `TokenStream` 是一個結構體 item,輸出的 `TokenStream` 則會添加新的程式碼在該輸入的 item 之後。
如下的語法案例,輸入的 `TokenStream` 會被忽略,並且調用 macro 後會建立一個
名為 `answer` 的 function,接續在輸入的 struct 之後:
```rust
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
```
調用的方式如下:
```rust
extern crate proc_macro_examples;
use proc_macro_examples::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
assert_eq!(42, answer());
}
```
## Attribute macros
attribute macros 可以附加 attribute(參數) 到原始程式碼的 item 之中。該類型 macro 的 prototype 為 `(TokenStream, TokenStream) -> TokenStream` 輸入的第一個 `TokenStream` 是自定義的 attribute,第二個 `TokenStream` 是給定的輸入 item。輸出的 `TokenStream` 會取代兩個 `TokenStream` 成為新的 item。
如下的語法案例,這個 macro 沒有輸入 attribute,而直接將輸入的第二個 `TokenStream` 返回:
```rust
#[proc_macro_attribute]
pub fn return_as_is(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
```
如下的案例則展示將兩個輸入的 `TokenStream` 字串化的內容。由於 procedural macros 是在編譯時期執行的,輸出會在編譯時期產生。下面將輸出內容寫成註解展示在處理目標的函式之後(以 `out:` 為前綴)
```rust
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{}\"", attr.to_string());
println!("item: \"{}\"", item.to_string());
item
}
```
調用的方式如下:
```rust
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() {}"
// Example:
#[show_streams { delimiters }]
fn invoke4() {}
// out: attr: "delimiters"
// out: item: "fn invoke4() {}"
```
## Reference
> * [Procedural Macros
](https://doc.rust-lang.org/reference/procedural-macros.html)
> * [Procedural Macros in Rust 2018](https://blog.rust-lang.org/2018/12/21/Procedural-Macros-in-Rust-2018.html)
> * [如何编写一个过程宏(proc-macro)](https://dengjianping.github.io/2019/02/28/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99%E4%B8%80%E4%B8%AA%E8%BF%87%E7%A8%8B%E5%AE%8F(proc-macro).html)
> * [Rust 學習之路─第十九章:巨集](https://magiclen.org/rust-macro/)