我的 B 站上正兒八經的和軟體開發相關的視訊,已經有半年沒有更新了。最後一期《程式君的 Rust 教育訓練 (2)》還是去年 6 月出品的,我記得肝那期時,正趕上西雅圖百年一遇的酷暑,晚上十點多還有 39 度的高溫,以至于我的 mbp 那幾天經常會被熱到關機自保。
之後,我業餘時間基本上都在更新我在極客時間上的《Rust 第一課》。上個月終于結課,才慢慢有更多業餘時間得以繼續更新公衆号,以及做些視訊。做視訊是很耗時耗力的一件事情,需要花費比寫稿更多的時間和精力,是以我必然無法像更新公衆号那樣頻繁。不過,和文字呈現最後的結果相比,視訊天然适合把整個過程暴露給大家。而軟體開發又是一個過程及其重要的活動,如果我把做一個項目的完整曆程,包括經曆的問題,做出的選擇展現出來,相信對大家會有很多幫助。
如果說日子是問題疊着問題過的,那麼代碼就是選擇疊着選擇寫的。我非常希望通過視訊,不僅介紹知識本身,還能把我在 live coding 過程中做出的選擇,無論是思路上的選擇,設計上的選擇,還是重構時的選擇給表現出來,這樣對我自己,對讀者朋友們都更加有幫助。
這個長周末,Tubi Holiday 加馬丁路德金日,一下子整出來四天假期,于是我有了大段的時間開始構思在《Rust 第一課》中,讀者們呼聲很高的宏程式設計,打算搞個加餐。寫文字的時候,我突然想到,何不就要寫的代碼做個 live coding,錄成視訊,一魚多吃?于是,就有了這個「Rust 過程宏」系列的三期視訊。
第一期,我不用 syn/quote 徒手寫了個通過 JsonSchema 生成 Rust struct 的函數宏,從最底層的邏輯出發,讓大家了解 Rust 的 TokenStream,以及如何把包含代碼的字元串轉換成 TokenStream。感謝 Rust 的 FromStr trait,這個從字元串到 TokenStream 的動作簡單到就是一個
s.parse().unwrap()
。
第二期,我使用 syn/quote 做了一個派生宏 Builder,可以為資料結構生成 builder 方法,讓資料結構可以用非常簡單直覺的方法初始化。這個 Builder 宏的需求來自于 dtolnay 的 proc-macro-workshop 中的一個練習,Jon Gjengset 在他的 Procedural Macros in Rust 視訊中,也使用它作為 proc macro 教學的素材。我大概一年前看的那個視訊,它讓我受益匪淺。不過,我不喜歡在宏處理的上下文中做所有的事情,而更加傾向于通過建構良好的資料結構,從 TokenStream 中擷取我需要使用的資料,然後在自己的資料結構做進一步的處理,而非直接和TokenStream 或者 DeriveInput 打交道。
第三期,我為 Builder 宏添加了 attributes 的支援。由于 syn 的 DeriveInput 結構對 attribute 沒有比較好的支援,你從文法樹中拿到的就是 attributes 的 TokenStream,處理起來會非常繁瑣。好在有 darling 這個第三方庫,可以把 attributes 用資料結構捕獲下來,就像 clap 3 / structopt 做的那樣。
其實宏還有很多其他可講的内容,我也在考慮哪些放在加餐中。比如 syn 的 Parse trait,就非常值得聊一聊 —— 我們通過
parse_macro_input!(input as DeriveInput)
之是以能把 TokenStream 轉換成 DeriveInput,就是因為 DeriveInput 實作了 Parse trait。再比如,sqlx 裡 query 宏,就為其内部資料結構 QueryMacroInput 也實作了它。是以才可以這麼用:
#[proc_macro]
pub fn expand_query(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as query::QueryMacroInput);
...
}
複制
我對宏的态度一直是這樣的:宏程式設計并沒有什麼神秘的,它就跟我們平日裡寫的代碼一樣,隻不過操作的資料結構是文法樹,輸出的資料結構也是文法樹。在這個過程中,你要做的不過是從輸入的文法樹中抽取必要的元素,放入你自己的資料結構中,然後在通過你的資料結構生成新的文法樹。是以,宏程式設計不過是一系列資料結構的轉換而已。
這三個講座,雖然我提供的例子非常簡單,但已經涵蓋了宏程式設計中你會遇到的主要情況。大家如果對宏程式設計感興趣的話,可以在看完之後繼續完成 proc-macro-workshop 裡其它的例子。如果你耐心地把它們全做一遍,一定會有很大的收獲。我希望通過這個系列,可以讓你對宏程式設計不再畏懼。
不過凡事有兩面。大家需要注意的是,宏程式設計是你撰寫代碼最後的手段。當一個功能可以用函數表達時,不要用宏。不要過分迷信于編譯時的處理,不要把它當成提高性能的手段。雖然撰寫宏并不困難,但宏會為别人了解你的代碼,使用你的代碼帶來額外的負擔。由于宏會生成代碼,是以大量使用宏會讓你的代碼在不知不覺中膨脹,導緻二進制很大。此外,正如我們看到的那樣,目前 IDE 對宏的支援還不夠好,這也是大量使用宏的一個問題。我們看到,像 nom 這樣的工具,一開始大量使用宏,後來也都逐漸用函數取代。是以我們在開發的時候,要非常謹慎地建構宏。多問自己:我非用宏不可麼?可以使用别的設計來避免使用宏麼?就像同樣是 web 架構,rocket 使用宏做路由,axum 完全不使用宏。這就是不同設計下帶來的不同結果。