Appearance
9.3 过程宏
对于一些简单的宏,使用 macro_rules! 声明宏就足够使用了。
过程宏(procedural macros),它更加强大和复杂,它的行为更像函数(是一种程序)。 过程宏以代码作为输入,以此执行一些操作,然后产生一些代码作为输出,而不是像声明性宏那样匹配模式然后替换相应的代码。
过程宏的工作原理非常简单: 取一段称为输入TokenStream
的代码,将其转换为抽象语法树(ast), 从输入处获得的内容构建一个新的TokenStream
(使用syn::parse()
方法),并将其作为输出代码注入编译器。
过程宏的三种类型:
类函数过程宏(Function-like macros):类函数宏类似于声明性宏,因为它们是用宏调用操作符调用的
!
看起来像函数调用。它们对括号内的代码进行操作。 它们在函数上使用#[proc_macro]
属性。lazy_static 程序库中的lazy_static!
宏就采用了类函数过程宏。类属性过程宏(Attribute-like macros):属性宏定义新的外部属性,这些属性可以附加到项上。它们类似于派生宏,但可以附加到更多的项目,如 trait 定义和函数。 它们在函数上使用
#[proc_macro_attribute]
属性。wasm-bindgen 程序库中 的#[wasm-bindgen]
属性就采用类过程宏。 自定义属性宏则可以用于为 Rust 中的各种元素添加额外的元数据,从而实现一些自定义的功能。例如,我们可以使用 #[route] 属性宏来标记一个函数作为 Web 应用程序中的路由处理器。派生过程宏(Derive macros):派生宏用于结构体、枚举和联合,并使用
#[derive(MyMacro)]
声明进行注释。 它们还可以声明辅助属性,这些属性可以附加到项目的成员(例如枚举变量或结构字段)。 它们使用#[proc_macro_derive]
属性。这些是大多数 Rust 软件包中最常见的宏,例如 serde 程序库。
由于引入了它们的 RFC 的名称,它们有时也被称为派生宏或 macro 1.1。
它是直接用 Rust 语言写出来的,相当于一个编译器插件。但是编译器插件的最大问题是,它依赖于编译器的内部实现方式。 一旦编译器内部有所变化,那么对应的宏就有可能出现编译错误,需要修改。因此,Rust 中的“宏”一直难以稳定。 这么做是出于复杂的技术原因,我们希望将来能够消除这个限制。
所以,Rust 设计者希望提供一套相对稳定一点的 API,它基本跟 rustc 的内部数据结构解耦。 这个设计就是 macro 2.0。这个功能目前暂时还没完成。但是,Rust 提前推出了一个 macro 1.1 版本。 我们在 1.15 正式版中可以体验一下这个功能的概貌。 所谓的 macro 1.1 是按照 2.0 的思路专门为自动 derive 功能设计的,是一个缩微版的 macro 2.0。
在 Rust 中,attribute 也是一种特殊的宏。在编译器内部,attribute 和 macro 并没有本质的区别,它们都是所谓的编译器扩展。 在以后的 macro 2.0 中,我们也可以用类似的 API 设计自定义 attribute。 目前有一个叫作 derive 的 attribute 是最常用的,最需要支持自定义扩展。专门为支持自定义 derive 的功能,就是 macro 1.1。 derive 功能我们在 trait 一章中已经讲过了,attribute 可以让编译器帮我们自动 impl 某些 trait。
9.3.1 类函数过程宏
首先,创建两个项目:一个是实现宏的库,一个使用宏。
sh
cargo new hello-macro
cd hello-world
cargo new macro-lib --lib
macro-lib 中进行:
Cargo.toml
新增:
toml
[lib]
proc-macro = true # 开启过程宏
lib.rs
:
rust
#![crate_type = "proc-macro"]
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
完成无误后运行:cargo build
。
hello-macro 目录下进行:
Cargo.toml
新增:
toml
[dependencies]
macro-lib = { path = "macro-lib" }
main.rs
:
rust
use macro_lib::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}
运行:
sh
cargo install --path .
cargo run
输出:42
。
9.3.2 类过程宏
可以按上面的类函数过程宏方式来构建个项目:
macro-lib 中lib.rs
更改为:
rust
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{}\"", attr.to_string());
println!("item: \"{}\"", item.to_string());
item
}
hello-macro 目录下 main.rs
更改为:
rust
extern crate macro_lib;
use macro_lib::show_streams;
// Example: Basic function
#[show_streams]
fn invoke1() {}
// Example: Attribute with input
#[show_streams(bar)]
fn invoke2() {}
// Example: Multiple tokens in the input
#[show_streams(multiple => tokens)]
fn invoke3() {}
// Example:
#[show_streams { delimiters }]
fn invoke4() {}
fn main(){
invoke1();
invoke2() ;
invoke3() ;
invoke4() ;
}
运行后输出:
txt
attr: ""
item: "fn invoke1() {}"
attr: "bar"
item: "fn invoke2() {}"
attr: "multiple => tokens"
item: "fn invoke3() {}"
attr: "delimiters"
item: "fn invoke4() {}"
9.3.3 派生过程宏
目前,编译器的 derive 只支持一小部分固定的 trait。但我们可以通过自定义宏实现扩展 derive。 下面,我们用一个示例来演示一下如何使用 macro 1.1 完成自定义#[derive(HelloWorld)]
功能。
可以参考上面的类函数过程宏的项目创建。
宏库项目编译完成后,会生成一个动态链接库。这个库会被编译器在编译主项目的过程中调用。在主项目代码中写上如下测试代码:
rust
#[macro_use]
extern crate hello_world_derive;
trait THelloWorld {
fn hello();
}
#[derive(HelloWorld)]
struct FrenchToast;
fn main() {
FrenchToast::hello();
}
接下来,我们来实现这个宏。它的代码骨架如下所示:
rust
extern crate proc_macro;
use proc_macro::TokenStream;
use std::str::FromStr;
#[proc_macro_derive(HelloWorld)]
pub fn hello_world(input: TokenStream) -> TokenStream {
// Construct a string representation of the type definition
let s = input.to_string();
TokenStream::from_str("").unwrap()
}
我们的主要逻辑就写在hello_world
函数中,它需要用proc_macro_derive
修饰。它的签名是,输入一个TokenStream
,输出一个TokenStream
。 目前这个TokenStream
类型还没实现什么有用的成员方法,暂时只提供了和字符串类型之间的转换方式。我们在函数中把input
的值打印出来:
rust
let s = input.to_string();
println!("{}", s);
编译可见,输出值为struct FrenchToast;
。由此可见,编译器将#[derive()]
宏修饰的部分作为参数,传递给了我们这个编译器扩展函数。 我们需要对这个参数进行分析,然后将希望自动生成的代码作为返回值传递出去。
在这里,我们引入 regex 库来辅助实现逻辑。在项目文件中,加入以下代码:
toml
[dependencies]
regex = "0.2"
然后写一个函数,把类型名字从输入参数中提取出来:
rust
fn parse_struct_name(s: &str) -> String {
let r = Regex::new(r"(?:struct\s+)([\w\d_]+)").unwrap();
let caps = r.captures(s).unwrap();
caps[1].to_string()
}
#[test]
fn test_parse_struct_name() {
let input = "struct Foo(i32);";
let name = parse_struct_name(input);
assert_eq!(&name, "Foo");
}
接下来,就可以自动生成我们的 impl 代码了:
rust
#[proc_macro_derive(HelloWorld)]
pub fn hello_world(input: TokenStream) -> TokenStream {
let s = input.to_string();
let name = parse_struct_name(&s);
let output = format!(r#"
impl THelloWorld for {0} {{
fn hello() {{ println!(" {0} says hello "); }}
}}"#, name);
TokenStream::from_str(&output).unwrap()
}
我们构造了一个字符串,然后将这个字符串转化为TokenStream
类型返回。
编译主项目可见,FrenchToast
类型已经有了一个hello()
方法,执行结果为:
txt
FrenchToast says hello
在 macro 1.1 版本中,只提供了这么一点简单的 API。 在接下来的 macro 2.0 版本中,会为TokenStream
添加一些更有用的方法,或许那时候就没必要把 TokenStream 转成字符串再自己解析一遍了。
9.3.4 常用的过程宏软件包
由于过程宏可以作为独立的软件包进行分发,因此可以在 crates.io 上找到许多实用的宏软件包。 通过它们可以大大减少为生成 Rust 代码而手动编写模板的工作量。其中一些如下所示。
derive-new:该软件包为结构体提供了默认的全字段构造函数,并且支持自定义。
derive-more:该软件包可以绕过这样的限制,即我们已经为类型包装了许多自动实 现的特征,但是失去了为其创建自定义类型包装的能力。 该软件包可以帮助我们提 供相同的特征集,即使是在这种包装器类型上也是如此。
lazy_static:该软件包提供了一个类函数的过程宏,其名为
lazy_static!
,你可以在其 中声明需要动态初始化类型的静态值。 例如,你可以将配置对象声明为 HashMap, 并可以跨代码库全局访问它。