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 过程宏

Download history 2806/week @ 2024-04-29 2093/week @ 2024-05-06 2838/week @ 2024-05-13 3447/week @ 2024-05-20 3859/week @ 2024-05-27 3160/week @ 2024-06-03 2833/week @ 2024-06-10 3161/week @ 2024-06-17 3584/week @ 2024-06-24 2743/week @ 2024-07-01 3646/week @ 2024-07-08 3431/week @ 2024-07-15 3466/week @ 2024-07-22 3684/week @ 2024-07-29 3820/week @ 2024-08-05 3659/week @ 2024-08-12

每月 15,058 次下载
45 库中使用 (17 个直接使用)

MIT 许可证

195KB
4.5K SLoC

crates.io docs.rs license

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