我的 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 完全不使用宏。这就是不同设计下带来的不同结果。