--- 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/)