bigywater.tech
Rust · 78

宏卫生性

为什么宏里的 let tmp 不会弄坏调用处的 tmp?这个叫「卫生性」。这篇不靠概念,而是让编译器把每个标识符的语法上下文编号打印出来,再用四个编译与运行实验划清卫生的边界。

#0 对 #4
同名变量 v 在调用处与宏内部的语法上下文编号
E0425
宏里引用未传入的调用处变量,或调用处引用宏里写死的变量
2 类
不卫生的:item(fn 等);卫生的:局部变量与标签

局部变量不会冲突

实测 · 本机执行输出
  源码:
      1 | macro_rules! swap { ($a:expr, $b:expr) => { let tmp = $a; $a = $b; $b = tmp; }; }
      2 | fn main() {
      3 |     let mut x = 1; let mut y = 2; let tmp = 100;
      4 |     swap!(x, y);
      5 |     println!("x = {} y = {} tmp = {}(调用处的 tmp 没有被宏里的 tmp 覆盖)", x, y, tmp);
      6 |     let mut tmp2 = 5; let mut other = 6;
      7 |     swap!(tmp2, other);
      8 |     println!("tmp2 = {} other = {}", tmp2, other);
      9 | }
  编译:成功
  运行(退出码 0):
    x = 2 y = 1 tmp = 100(调用处的 tmp 没有被宏里的 tmp 覆盖)
    tmp2 = 6 other = 5

macro_rules 基础见声明宏 macro_rules!。

编译器如何区分

实测 · 本机执行输出
  源码:
    macro_rules! twice { ($e:expr) => {{ let v = $e; v + v }}; }
    pub fn f() -> i32 { let v = 10; twice!(v + 1) }
  展开(带 hygiene 标记 名字#上下文编号):
    pub fn f /* 2332#0 */() -> i32 /* 985#0 */ {
        let v /* 2348#0 */ = 10;
        {
            let v /* 2348#4 */ = v /* 2348#0 */ + 1;
            v /* 2348#4 */ + v /* 2348#4 */
        }
    }
    crate0::{{expn2}}: parent: crate0::{{expn0}}, call_site_ctxt: #0, def_site_ctxt: #0, kind: Macro(Bang, "twice")
    #4: parent: #0, outer_mark: (crate0::{{expn2}}, SemiOpaque)

hygiene 打印用 RUSTC_BOOTSTRAP=1 -Z unpretty=expanded,hygiene;编号是该次编译的内部值,会随编译器版本变化。

反方向的限制

实测 · 本机执行输出
  源码:
      1 | macro_rules! show_y { () => { println!("y = {}", y) }; }
      2 | fn main() { let y = 5; show_y!(); }
  编译:失败
    error[E0425]: cannot find value `y` in this scope
     --> <实验目录>/src/u78c.rs:1:50
      |
    1 | macro_rules! show_y { () => { println!("y = {}", y) }; }
      |                                                  ^ not found in this scope
    2 | fn main() { let y = 5; show_y!(); }
      |                        --------- in this macro invocation
      |
    help: an identifier with the same name exists, but is not accessible due to macro hygiene
     --> <实验目录>/src/u78c.rs:2:17
      |
    2 | fn main() { let y = 5; show_y!(); }
      |                 ^
      = note: this error originates in the macro `show_y` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0425`.
  编译:成功
  运行(退出码 0):
    y = 5
    宏里 let 传入的名字 z 在调用处可见: z = 9
  源码:
      1 | macro_rules! decl_hidden { ($val:expr) => { let hidden = $val; }; }
      2 | fn main() { decl_hidden!(1); println!("{}", hidden); }
  编译:失败
    error[E0425]: cannot find value `hidden` in this scope
     --> <实验目录>/src/u78e.rs:2:45
      |
    2 | fn main() { decl_hidden!(1); println!("{}", hidden); }
      |                                             ^^^^^^ not found in this scope
      |
    help: an identifier with the same name is defined here, but is not accessible due to macro hygiene
     --> <实验目录>/src/u78e.rs:1:49
      |
    1 | macro_rules! decl_hidden { ($val:expr) => { let hidden = $val; }; }
      |                                                 ^^^^^^
    2 | fn main() { decl_hidden!(1); println!("{}", hidden); }
      |             --------------- in this macro invocation
      = note: this error originates in the macro `decl_hidden` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0425`.

item 与标签

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    宏里定义的 helper

标签的卫生性

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    i = 0 宏里的 'outer 与调用处的 'outer 不冲突,找到 Some(8)
    i = 1 宏里的 'outer 与调用处的 'outer 不冲突,找到 Some(8)

分步演示

宏卫生性
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。展开用 RUSTC_BOOTSTRAP=1 -Z unpretty=expanded,hygiene
    • 没有验证:过程宏的 Span 与卫生性(call_site / mixed_site)、$crate、泛型参数与生命周期名的卫生性、宏 2.0 的 item 卫生
    京ICP备2026067502号-1京公网安备11010502063487号