7 个版本 (重大更新)
0.6.0 | 2024 年 3 月 28 日 |
---|---|
0.5.0 | 2022 年 10 月 7 日 |
0.4.0 | 2022 年 5 月 25 日 |
0.3.0 | 2022 年 4 月 13 日 |
0.1.1 | 2022 年 3 月 10 日 |
#20 in 过程宏
每月 15,058 次下载
在 45 个 库中使用 (17 个直接使用)
195KB
4.5K SLoC
Rust 过程宏的轻量级解析
Venial 是 Rust 的一个用于过程宏的轻量级解析器。
当编写需要解析 Rust 代码的过程宏(如属性和 derive 宏)时,最常用的解决方案是使用 syn 库。Syn 可以解析任意有效的 Rust 代码,甚至基于 Rust 的 DSL,并返回可以以强大方式检查和修改的通用数据结构。
但它也非常重。在 一次分析 中,作者估计在 lqd 的 2022 年早期基准测试数据集中,syn 负责了基准测试 8% 的编译时间,这占 Rust 最受欢迎的库的比例。这里有一些细微差别(例如,这并不一定是关键路径时间,但 syn 通常位于关键路径上),但总体结论是明确的:syn 成本很高。
然而,syn 的许多功能通常是不必要的。如果我们查看依赖于 syn 的 库,我们可以看到下载量最大的 5 个是
- serde_derive
- proc-macro-hack
- pin-project-internal
- anyhow
- thiserror-impl
在这些库中,proc-macro-hack 已弃用,其他四个库只需解析类型的基本信息。
syn 的其他受欢迎的反向依赖(如 futures-macro、tokios-macros、async-trait 等)确实 使用了 syn 的更高级功能,但过程宏中仍有轻量级解析器的空间。
Venial 就是那个解析器。
设计
Venial 非常简单。大部分实现都在 parse.rs
文件中,我在编写这个 README 的时候大约有 350 行。这是因为 Rust 语言的语法非常清晰,特别是类型声明。
Venial 除了 proc-macro2 和 quote 之外没有其他依赖。
为了实现这种简洁性,venial做出了几个权衡
- 它只能解析声明(例如
struct MyStruct {}
)。它不能解析表达式或语句。目前,只支持类型和函数。 - 它不会尝试解析类型表达式内部。例如,如果你的结构体包含一个如
foo_bar: &mut Foo<Bar, dyn Foobariser>
的字段,venial 将尽职地给你这个类型作为一系列标记符,并让你进行解释。 - 它不会尝试优雅地恢复错误。venial 假定你正在 derive 或 attribute 宏内部运行,因此你的输入是静态保证为有效的类型声明。如果不是,venial 将简单地恐慌。
但请注意,venial 会接受任何语法上有效的声明,即使它在语义上无效。一般规则是“如果它在 #[cfg(FALSE)]
下编译,venial 将解析它而不会恐慌”。
唯一的例外是枚举区分符。venial 只支持单个标记符或标记符组的枚举区分符。例如
enum MyEnum {
A = 42, // Ok
B = "hello", // Ok
C = CONSTANT, // Ok
D = FOO + BAR, // MACRO ERROR
E = (FOO + BAR), // Ok
}
这是因为解析复杂区分符需要任意表达式解析,而这超出了这个包的范围。
(注意:venial 当前在不受支持的声明上恐慌,例如 traits、别名等。此外,函数支持也不完整。)
示例
use venial::{parse_declaration, Declaration};
use quote::quote;
let enum_type = parse_declaration(quote!(
enum Shape {
Square(Square),
Circle(Circle),
Triangle(Triangle),
}
));
let enum_type = match enum_type {
Declaration::Enum(enum_type) => enum_type,
_ => unreachable!(),
};
assert_eq!(enum_type.variants[0].0.name, "Square");
assert_eq!(enum_type.variants[1].0.name, "Circle");
assert_eq!(enum_type.variants[2].0.name, "Triangle");
性能
我还没有进行任何正式的基准测试。话虽如此,我将使用 venial 的这个分叉与 equivalent miniserde commit 进行比较,并得到以下结果
$ cargo check -j1 # miniserde-venial, clean build
Finished dev [unoptimized + debuginfo] target(s) in 6.30s
$ cargo check -j1 # miniserde, clean build
Finished dev [unoptimized + debuginfo] target(s) in 9.52s
$ cargo check -j4 # miniserde-venial, clean build
Finished dev [unoptimized + debuginfo] target(s) in 3.17s
$ cargo check -j4 # miniserde, clean build
Finished dev [unoptimized + debuginfo] target(s) in 4.79s
我的机器是台式电脑,配备 AMD Ryzen 7 1800x(8 核心线程,16 线程),我有 32GB 的 RAM 和 2.5TB 的 SSD。
正如我们所看到的,使用 venial 而不是 syn 在单线程构建中减少了大约 3.2 秒的总构建时间,在 4 线程构建中减少了 1.6 秒。
大部分差异来自 syn 和 venial 本身:cargo check --timings
显示,syn 需要 2.11 秒来编译,venial 需要 0.58 秒在 4 线程构建中。
我没有展示代码生成构建、发布模式构建、16 线程构建等,但趋势大致相同:对于 miniserde 项目,切换到 venial 可减少约 30% 的构建时间。
那么... 它值得吗?
这是一个相当复杂的问题。在开始项目两年后,我的答案是“仅适用于特定用例”。
如果你采取最乐观的解释,这些结果非常棒!在单线程机器上,切换可以减少三秒钟,占构建时间的整整三分之一!
在现实中,有很多复杂因素
- Venial 绝对不能改善增量构建时间(因为依赖项被缓存,即使增量编译关闭)。
- 在多线程的情况下,syn 和 venial 之间的差距更小。
- 我有一台相当强大的计算机。笔记本电脑可能从 venial 中获得更多的好处。
- 在比miniserde更大的项目中,syn通常是在编译时同时编译的许多库之一。在某些情况下,这意味着syn的编译时间并不是那么重要,因为它可以与其他库并行编译。在其他情况下,syn位于关键路径上。
- 实际上,大多数清洁构建都是由CI服务器运行的。要衡量venial的有用性,您需要分析在Github Actions / Gitlab CI / whatever crater使用的服务器规格。
总的来说,是否值得将derive crate从syn迁移到venial是有疑问的(尽管到目前为止我的经验表明迁移并不难)。
还需要注意的是,这是一个非常年轻的库。到目前为止,几乎没有对其进行优化或分析的工作,未来的版本可能会提供更好的构建时间减少。
总结:您可能可以用venial减少几秒钟的清洁构建时间。增量构建看不到任何好处。
贡献
欢迎Pull requests。
我本人短期内没有打算亲自开发venial,但我仍然会合并PRs。
一些可能的改进
- 修复函数声明解析器。
- 找出并修复任何可能出现的错误。
- 将其他项目从syn迁移到venial并比较编译时间。
依赖关系
~79KB