输入语法与输出语法一致

Input syntax is evocative of the output (C-EVOCATIVE)

Rust 宏系统允许你创造出你想要的输入语法。 尽量让输入的语法是让大多数人感到熟悉的, 仿照现存的 Rust 语法以便让使用者的代码风格统一。 注意对关键字和标点符号的选择与更改。

好的标准是:宏输入的语法与输出的结果不会相差太大,尤其在关键字和标点符号方面。

比如你写的宏要根据输入来声明一个具体的结构体,那么就以 struct 关键字开头, 后接结构体的名称。

// 这种方式更好 bitflags! { struct S: u32 { /* ... */ } } // 这种方式就不太好 bitflags! { S: u32 { /* ... */ } } // 这种方式也不太好 bitflags! { flags S: u32 { /* ... */ } }

另一个例子是分号和逗号之间如何做出选择。 Rust 的常量 (constants) 以分号作为结尾,所以如果你写的宏要定义一系列常量, 那么应该在输入语法里书写分号,即使语法看起来与 Rust 的语法有些不同。

// 原始的常量定义语法后面接的是分号 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, } }

宏是多样化的,以致于这里举的例子不会适合所有场景。 但是的确要仔细思考如何应用同一套规则。

宏与属性形成有机的整体

Item macros compose well with attributes (C-MACRO-ATTR)

生成超过一个条目 (item) 的宏,应该支持对每个条目添加属性。 一个常见的例子是,把单独的条目放在 cfg 后面:

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

生成结构体或枚举体的宏,应该支持添加宏属性, 方便让输出结果使用 derive 属性。

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

生成条目的宏可以在条目被允许的地方使用

Item macros work anywhere that items are allowed (C-ANYWHERE)

Rust 允许条目 (item) 有模块级别那样的作用域或者出现在类似函数这样小的作用域里。 生成条目的宏应该和普通的条目一样,在这些地方可以正常使用。 测试这些宏的时,应该至少在模块和函数作用域里都调用宏。

#[cfg(test)] mod tests { test_your_macro_in_a!(module); #[test] fn anywhere() { test_your_macro_in_a!(function); } }

下面这个例子展示了错误的宏: 这个宏能在模块作用域里运行,但不能在函数作用域里运行。

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 }

生成条目的宏应支持可见性分类符

Item macros support visibility specifiers (C-MACRO-VIS)

宏应遵循 Rust 对条目可见性 (visibility) 的语法要求: 默认是私有的,如果使用 pub 则表明条目是公有的。

bitflags! { struct PrivateFlags: u8 { const A = 0b0001; const B = 0b0010; } } bitflags! { pub struct PublicFlags: u8 { const C = 0b0100; const D = 0b1000; } }

类型分类符 $t:ty 是灵活的

Type fragments are flexible (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); // okay broken!(b => ::std::marker::PhantomData<()>); // okay struct S; broken!(c => S); // 编译失败: `S` not found in this scope }