输入语法能引发对输出的联想 (C-EVOCATIVE)

Rust 宏允许你设计出几乎任何形式的输入语法。尽量保持输入语法与用户代码中其他部分一致,通过尽可能地模仿现有的 Rust 语法,使其更具亲和力和连贯性。注意关键词和标点符号的选择与放置。

一个好的指引是使用类似于宏输出结果的语法,特别是关键词和标点符号。

例如,如果你的宏在输入中声明了一个具有特定名称的结构体,请在该名称前加上关键词 struct,以向读者标明正在定义一个具有给定名称的结构体。

#![allow(unused)]
fn main() {
// 优先这样写...
bitflags! {
    struct S: u32 { /* ... */ }
}

// ...而不是省略关键词...
bitflags! {
    S: u32 { /* ... */ }
}

// ...或使用随意的词。
bitflags! {
    flags S: u32 { /* ... */ }
}
}

另一个例子是分号 vs 逗号。在 Rust 中,常量后用分号,所以如果你的宏声明了一连串常量,即使语法略有不同,也应当使用分号。

#![allow(unused)]
fn main() {
// 普通常量使用分号。
const A: u32 = 0b000001;
const B: u32 = 0b000010;

// 所以更建议这样写...
bitflags! {
    struct S: u32 {
        const C = 0b000100;
        const D = 0b001000;
    }
}

// ...而不是这样。
bitflags! {
    struct S: u32 {
        const E = 0b010000,
        const F = 0b100000,
    }
}
}

宏的多样性使得这些具体例子可能不适用,但请考虑如何在你的情况下运用相同的原则。

项目宏与属性很好地组合 (C-MACRO-ATTR)

生成多个输出项目的宏应支持为任何一个输出项目添加属性。一个常见的用法是通过 cfg 置于单个项目后。

#![allow(unused)]
fn main() {
bitflags! {
    struct Flags: u8 {
        #[cfg(windows)]
        const ControlCenter = 0b001;
        #[cfg(unix)]
        const Terminal = 0b010;
    }
}
}

生成结构体或枚举作为输出的宏应该支持属性,以便输出可以与派生属性一起使用。

#![allow(unused)]
fn main() {
bitflags! {
    #[derive(Default, Serialize)]
    struct Flags: u8 {
        const ControlCenter = 0b001;
        const Terminal = 0b010;
    }
}
}

项目宏可在任何允许放置项目的地方运行 (C-ANYWHERE)

Rust 允许项目放置在模块级别或更紧凑的范围内,如函数内。项目宏应该在这些地方与普通项目一样正常工作。测试套件应包括在至少模块范围和函数范围内调用宏的情况。

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    test_your_macro_in_a!(module);

    #[test]
    fn anywhere() {
        test_your_macro_in_a!(function);
    }
}
}

一个简单的错误示例是,这个宏在模块范围内工作良好,但在函数范围内失败。

#![allow(unused)]
fn main() {
macro_rules! broken {
    ($m:ident :: $t:ident) => {
        pub struct $t;
        pub mod $m {
            pub use super::$t;
        }
    }
}

broken!(m::T); // 好的,展开为 T 和 m::T

fn g() {
    broken!(m::U); // 编译失败,super::U 指向的是包含的模块而不是 g
}
}

项目宏支持可见性说明符 (C-MACRO-VIS)

遵循 Rust 语法,宏生成的项目默认是私有的,如果指定 pub 则是公有的。

#![allow(unused)]
fn main() {
bitflags! {
    struct PrivateFlags: u8 {
        const A = 0b0001;
        const B = 0b0010;
    }
}

bitflags! {
    pub struct PublicFlags: u8 {
        const C = 0b0100;
        const D = 0b1000;
    }
}
}

类型片段是灵活的 (C-MACRO-TY)

如果你的宏在输入中接受类型片段如 $t:ty,它应能与以下所有内容一起使用:

  • 基本类型:u8&str
  • 相对路径:m::Data
  • 绝对路径:::base::Data
  • 向上相对路径:super::Data
  • 泛型:Vec<String>

一个简单的错误示例是,这个宏在使用基本类型和绝对路径时工作良好,但在使用相对路径时失败。

#![allow(unused)]
fn main() {
macro_rules! broken {
    ($m:ident => $t:ty) => {
        pub mod $m {
            pub struct Wrapper($t);
        }
    }
}

broken!(a => u8); // 好的

broken!(b => ::std::marker::PhantomData<()>); // 好的

struct S;
broken!(c => S); // 编译失败
}