Skip to content

元编程

rust的宏

Rust 的宏可以清晰地分为两大类,其中过程宏又可以进一步细分:

  1. 声明式宏 (macro_rules!)

    • 描述: 这是 Rust 开发者通常首先接触到的宏类型。它们通过匹配字面上的 token 模式,然后将这些模式转写(替换)成其他代码。相比过程宏,它们在更高的抽象层级上工作。
    • 语法: 使用 macro_rules! 宏名称 { (模式) => { 展开代码 }; ... } 的语法定义。
    • 调用: 通过 宏名称!(...)宏名称!{...}宏名称![...] 的方式调用。
    • 材料中的关键特性:
      • 模式匹配和重复 (例如 $($元素:expr),*)。
      • 捕获类型 (例如 :expr, :ty, :ident, :tt 等)。
      • 卫生性 (主要针对标识符,有助于防止名称冲突)。
      • 递归 (可以自我调用)。
      • 可用于简单的代码生成和创建 DSL (领域特定语言)。
      • 在 "The Little Book of Rust Macros" (文件1) 和 "Write Powerful Rust Macros" (文件4) 的第2章中有详细介绍。
  2. 过程宏 (Procedural Macros)

    • 描述: 这类宏直接操作 Rust 代码的抽象语法树 (AST)。它们接收一个 TokenStream (令牌流) 作为输入,并产生一个 TokenStream 作为输出。它们比声明式宏更强大也更复杂,允许进行任意的代码分析和生成。它们需要定义在自己的 crate 中,并在 Cargo.toml 中设置 proc-macro = true
    • 常用库: 通常使用 syn 库来解析输入的 TokenStream 转换成 AST,使用 quote 库来生成输出的 TokenStream
    • 子类型 (源自文件2和文件4):
      • a. 派生宏 (Derive Macros)
        • 描述: 用于为结构体和枚举自动实现 trait。它们会 添加 新的代码 (例如一个 impl 块),而不会修改原始的条目。
        • 调用: #[derive(MyTrait)] struct Foo;
        • 材料中的示例: #[derive(Builder)], #[derive(CustomDebug)], 标准库的 #[derive(Debug)], #[derive(Clone)]
        • 关键特性: 不能改变输入的条目,只能向其添加内容。它们的属性是“惰性的”(inert),即除非被宏消费,否则会保留在输出中。
      • b. 属性宏 (Attribute Macros)
        • 描述: 可以应用于几乎任何 Rust 条目 (函数、结构体、枚举、impl 块等)。它们可以将附加到的条目 替换 为新的代码,从而允许进行显著的代码转换。它们会定义一个新的自定义属性。
        • 调用: #[my_attribute]#[my_attribute(with_args)]
        • 材料中的示例: #[sorted], #[bitfield], #[tokio::main], #[public(exclude(...))] (来自 "Write Powerful Rust Macros")。
        • 关键特性: 它们的输出会 替换 输入的条目。它们的属性(以及条目上的任何辅助属性)是“活性的”(active),即在处理过程中会被消费和移除,除非被重新发出。
      • c. 类函数宏 (Function-like Macros)
        • 描述: 它们看起来像普通的函数调用,但以感叹号结尾。它们可以接收几乎任何 Rust 代码 (甚至非 Rust 风格的语法) 作为输入,并产生任意的 Rust 代码作为输出。
        • 调用: my_macro!(...), my_macro!{...}, 或 my_macro![...]
        • 材料中的示例: seq!, println!, format!, iac! (用于 AWS 基础设施)。
        • 关键特性: 输入最为灵活;它们的输出会替换调用本身。

何时使用宏及其效果

宏是一种元编程 (Metaprogramming) 的形式——即编写能够编写其他代码的代码。它们是非常强大的工具,但应当谨慎使用。文件4 ("Write Powerful Rust Macros" 的第1章) 在这方面给出了很好的指导。

以下是常见的用例和效果的概要,这些都得到了你提供材料的支持:

  1. 减少样板代码和重复 (DRY - Don't Repeat Yourself)

    • 何时使用: 当你发现自己需要重复编写相似的代码模式时。
    • 效果:
      • 减少需要编写和维护的代码量。
      • 提高一致性,减少在重复代码中手动出错的几率。
    • 材料中的示例:
      • 声明式宏: 生成 newtype 包装器及其 From 实现 (文件4, 第2章)。lazy_static 用于初始化静态变量 (文件4, 第2章)。
      • 派生宏: derive(Builder) 避免编写构建器模式的样板代码 (文件2, 文件4 第6章)。derive(Debug)derive(CustomDebug) 避免手动实现 Debug trait (文件2, 文件4 第1、4章)。Serde 的派生宏用于序列化/反序列化 (文件4, 第1章)。
      • 类函数宏: seq! 用于生成重复的、带索引的代码 (文件2)。
  2. 扩展语言能力 / 模拟特性

    • 何时使用: 当 Rust 的核心语言或标准库不直接支持某个期望的特性或语法时。
    • 效果:
      • 通过添加感觉像是原生支持的特性,使代码更具表达力或更方便。
    • 材料中的示例:
      • 声明式宏: 为函数实现可变参数 (varargs) 或默认参数 (文件4, 第2章)。
      • 属性宏: #[tokio::main] 用于异步 main 函数 (文件4, 第1、4章)。#[bitfield] 用于类似 C 语言的位域操作 (文件2)。“带类型参数的单元结构体”案例研究 (文件3) 用于克服语言限制。“用户定义的可调用类型”案例研究 (文件3)。
      • 类函数宏: println!, format! (自定义格式化 DSL)。Yew/Leptos 的 html!view! 宏用于在 Rust 中编写 HTML (文件4, 第5、9章)。
  3. 创建领域特定语言 (DSLs)

    • 何时使用: 当你想为特定的问题领域提供一种专门的、通常更简洁或更易读的语法时。
    • 效果:
      • 代码可以变得更具声明性,更容易被领域专家(或在该领域工作的开发者)理解。
      • 可以在编译时强制执行特定领域的规则。
    • 材料中的示例:
      • 声明式宏: 用于银行账户转账的简单 DSL (文件4, 第2章)。
      • 类函数宏: iac! 用于描述 AWS 基础设施 (文件4, 第9章)。SQLx 的 query! 宏 (文件4, 第5章)。Yew 的 html! 宏 (文件4, 第5章)。
  4. 编译时检查和验证

    • 何时使用: 当你想在编译时而不是运行时强制执行不变量或规则时。
    • 效果:
      • 在开发周期的早期捕获错误,从而产生更健壮的代码。
      • 可以通过将检查从运行时移到编译时来提高性能。
    • 材料中的示例:
      • 属性宏: #[sorted] 确保枚举变体或 match 分支按字典序排列 (文件2)。“8的倍数常量断言”案例研究 (文件3)。
      • 类函数宏: Yew/Leptos 的 html! 宏检查 HTML 结构 (文件4, 第5章)。
  5. 代码生成和转换 (核心元编程)

    • 何时使用: 当你需要根据某些输入或定义以编程方式生成或修改代码时。
    • 效果:
      • 自动化复杂或重复的代码生成。
      • 允许强大的抽象。
    • 材料中的示例:
      • 派生宏: 所有的派生宏都会生成 impl 块。
      • 属性宏: #[tokio::main] 将标准的 async fn main 转换为 Tokio 运行时可以执行的形式。“公开字段”宏 (文件4, 第4章) 修改结构体使其字段公开。
      • 类函数宏: lazy_static 生成必要的静态项和初始化逻辑。
  6. 抽象和封装 / 隐藏复杂性

    • 何时使用: 当你想在一个更复杂的底层实现之上提供一个更简单的接口时。
    • 效果:
      • 更易于使用的 API。
      • 实现细节可以被隐藏,并且可以在不破坏用户代码的情况下进行更改。
    • 材料中的示例:
      • Serde 隐藏了序列化/反序列化的复杂性。Tokio 隐藏了异步运行时的管理 (文件4, 第1章)。
      • “函数尾声”案例研究 (文件3) 演示了以一种有些隐蔽的方式修改函数行为。
      • config! 宏 (文件4, 第10章) 隐藏了 YAML 解析和结构体生成。

从文件4 (第1章) 总结的一般效果/好处:

  • 安全性: Rust 宏 (特别是使用 synquote 的过程宏) 生成的代码随后会由 Rust 编译器检查,从而继承了 Rust 的安全保证。声明式宏具有卫生性。
  • 性能: 工作在编译时完成,因此通常没有运行时开销;通常,由于生成的代码经过优化,它甚至可以带来 更好 的运行时性能。
  • 可读性/人体工程学 (如果做得好): 可以使代码更简洁、更具表达力。
  • 强大性: 可以完成函数无法完成的事情 (例如,操作任意的 token 流、生成条目、根据结构实现 trait)。

何时 使用宏 (源自文件4, 第1章):

  • 如果常规函数就足够了: 函数更易于理解、测试,并且有更好的 IDE 支持。
  • 如果它使代码 更难 理解: 目标是降低复杂性,而不是增加复杂性。
  • 通常不用于核心业务逻辑: 除非是一个定义非常明确的 DSL 或用于消除罕见且复杂的样板代码。通过宏在微服务之间共享业务逻辑通常不是一个好主意。
  • 过度工程化: 不要仅仅因为可以使用宏就使用宏。
  • IDE 支持: 可能不如常规 Rust 代码那么完善,可能会使调试或自动补全更具挑战性。
  • 编译时间: 复杂的宏可能会增加编译时间。

与其他概念的关联

Python 装饰器 (Decorator)

  • 相似之处:

    • 代码包装和增强: Python 装饰器的核心功能是包装一个函数(或其他可调用对象),并在不修改原函数代码的情况下为其添加额外的功能(例如计时、日志、权限检查等)。Rust 的属性宏 (Attribute Macros) 在概念上非常相似,它们可以附加到函数、结构体等条目上,并修改或包裹它们以实现类似的功能。
    • 语法糖: 装饰器本身是一种语法糖,使得包装函数的行为更加优雅和声明式。Rust 的属性宏也提供了类似的声明式方式来应用代码转换。
    • 元编程: 两者都是元编程的一种形式,即在运行时(Python 装饰器在函数定义时)或编译时(Rust 宏)操纵或生成代码。
  • 不同之处:

    • 执行时机: Python 装饰器主要在函数定义时(可以看作是运行时的元编程,尽管发生在加载模块时)起作用。而 Rust 宏则是在编译时展开和执行的。这意味着 Rust 宏的开销在编译阶段,而 Python 装饰器可能会引入一些运行时开销(尽管通常很小)。
    • 能力范围: Python 装饰器主要作用于函数和类。Rust 的过程宏(特别是属性宏和类函数宏)可以操作更广泛的 Rust 语法结构,并且可以直接生成或修改 AST,其能力更为强大和底层。
    • 类型系统: Rust 的宏展开后的代码仍然会受到 Rust 严格的类型系统检查。Python 作为动态类型语言,其装饰器在这方面有更大的灵活性,但也缺少编译时的类型保证。

编译器 (Compiler)

  • 相似之处:

    • 代码处理: 编译器读取源代码,进行词法分析、语法分析、语义分析,最终生成目标代码。Rust 的过程宏系统也接收 Rust 代码(以 TokenStream 的形式),对其进行解析(通常用 syn 库),然后生成新的 Rust 代码(通常用 quote 库)。
    • AST 操作: 编译器内部会构建和操作抽象语法树 (AST)。过程宏的核心能力之一就是能够访问和修改(或基于其生成新的)AST 片段。
    • 编译时执行: 宏是在编译阶段执行的,它们是编译过程的一部分。
  • 不同之处:

    • 范围和职责: 编译器负责整个编译流程,从源代码到最终的可执行文件或库。宏只是编译流程中的一个特定阶段,专注于代码生成和转换。宏生成的代码仍然需要经过编译器后续阶段的处理(类型检查、优化、代码生成等)。
    • 语言定义 vs. 语言扩展: 编译器的核心功能是根据语言规范来实现语言。宏系统则允许开发者在一定程度上扩展语言的语法和功能,但这些扩展最终还是会被转换成符合核心语言规范的代码。

    LLVM Pass

  • 相似之处:

    • 代码转换和分析: LLVM Pass 是在 LLVM 的中间表示 (IR) 层面上对代码进行分析、转换和优化的模块。过程宏也是对代码(Rust 源代码的表示)进行分析和转换的模块。
    • 目的驱动: LLVM Pass 通常有特定的目的,比如优化特定模式、进行静态分析、插桩 (instrumentation) 等。你实现的 profiling pass 就是一个很好的例子。宏也通常是为了解决特定问题而设计的(如代码生成、DSL 实现等)。
    • 流水线操作: 编译过程(包括 LLVM 的处理)通常是一个流水线,代码在不同阶段被不同的 Pass 处理。宏展开也可以看作是编译流水线中的一个阶段。
  • 不同之处:

    • 抽象层级: LLVM Pass 工作在比 Rust 源代码更低级的 LLVM IR 上。而 Rust 的过程宏工作在 Rust 源代码的抽象语法树 (TokenStream/AST) 层面上。这意味着过程宏开发者通常不需要关心机器指令或非常底层的优化细节,而是专注于 Rust 语言结构。
    • 语言相关性: LLVM Pass 理论上可以应用于任何编译到 LLVM IR 的语言。Rust 宏则是 Rust 语言特有的机制。
    • 可移植性/复杂性: 编写 LLVM Pass 通常需要对 LLVM 的内部结构有深入了解,并且可能涉及到 C++ 编程,其复杂度和学习曲线通常比编写 Rust 过程宏要高。
  • Python 装饰器Rust 属性宏 在“声明式地增强代码功能”方面高度相似。

  • 尾递归优化 的共同点在于它们都是一种“代码转换”机制,但目的和层面不同。

  • 编译器宏系统 的关系可以理解为,宏系统是编译器能力的一种可编程扩展,允许开发者在编译期介入代码生成。

  • LLVM Pass过程宏 都代表了在编译流程中对代码表示进行分析和转换的模块化方法,只是工作的抽象层级不同。

知识在于积累