Rust中的过程宏
前言
过程宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)TokenStream
到另一个TokenStream
的函数,用输出的结果来替换宏调用。
工具准备
安装cargo-expand
安装后,使用cargo expand
命令可以打印出应用于当前 crate 的宏扩展结果。
// 安装nightly
rustup toolchain install nightly
// 并设置为默认,或者直接执行该语句会自动下载并设置为默认
rustup default nightly
// 安装cargo-expand
cargo install cargo-expand
// 如果提示link.exe,按提示安装C++
项目准备
项目目录
// 创建新项目
cargo new rust_macro_test
// 创建宏目录
mkdir proc_macro_crate
cd proc_macro_crate
// 初始化项目并创建lib.rs文件
cargo init --lib
项目结构为:
rust_macro_test
├─proc_macro_crate
│ └─src
└─src
注意:
1、过程宏必须定义在一个独立的crate中。不能在一个crate中既定义过程宏,又使用过程宏。
原理:过程宏是在编译一个crate之前,对crate的代码进行加工的一段程序,这段程序也是需要编译后执行的。如果定义过程宏和使用过程宏的代码写在一个crate中,那就陷入了死锁:
- 要编译的代码首先需要运行过程宏来展开,否则代码是不完整的,没法编译crate。
- 不能编译crate,crate中的过程宏代码就没法执行,就不能展开被过程宏装饰的代码
2、过程宏必须定义定义在lib
目标中,不能定义在bin
目标中
引入过程宏三件套
在proc_macro_crate下Cargo.toml添加:
# 表示这个crate是过程宏
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.7"
quote = "1"
syn = { version = "1.0.56", features = ["full","extra-traits"] } # "extra-traits"方便后续打印调试信息
- proc_macro2:是对proc_macro的封装,是由 Rust 官方提供的。
- syn:是基于proc_macro2中暴露的 TokenStream API 来生成 AST,提供方便的操作AST的接口。
- quote:配合syn,将AST转回TokenSteam,回归到普通文本代码生成的TokenSteam中。
添加过程宏crate依赖
在rust_macro_test下Cargo.toml添加:
[dependencies]
proc_macro_crate = {path="./proc_macro_crate"}
Rust过程宏
属性式过程宏
Attribute macros,用在结构体、字段、函数等地方,为其指定属性等功能。
定义
在lib.rs中加入:
// 属性式过程宏 custom_proc_macro_attribute 为宏的名字
#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(
attrs: proc_macro::TokenStream, // 过程宏传的属性
input: proc_macro::TokenStream, // 作用的代码
) -> proc_macro::TokenStream {
eprintln!("attrs:{:#?}", attrs); // 注意输出要用epringln!
eprintln!("input:{:#?}", input);
input
// 如果返回空对象,相当于去除方法
// proc_macro::TokenStream::new()
}
使用
在main.rs加入:
use proc_macro_crate::custom_proc_macro_attribute;
fn main() {
custom_proc_macro_attribute_fn();
custom_proc_macro_attribute_fn_ha_attribute();
}
// 属性过程宏:空属性
#[custom_proc_macro_attribute]
fn custom_proc_macro_attribute_fn() {
println!("this is custom_proc_macro_attribute_fn()");
}
// 属性过程宏:自定义属性
#[custom_proc_macro_attribute("this is custom_proc_macro_attribute")]
fn custom_proc_macro_attribute_fn_ha_attribute() {
println!("this is custom_proc_macro_attribute_fn()");
}
查看宏扩展结果
运行cargo expand
fn main() {
custom_proc_macro_attribute_fn();
custom_proc_macro_attribute_fn_ha_attribute();
}
fn custom_proc_macro_attribute_fn() {
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["this is custom_proc_macro_attribute_fn()\n"],
&[],
),
);
};
}
fn custom_proc_macro_attribute_fn_ha_attribute() {
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["this is custom_proc_macro_attribute_fn() with attribute\n"],
&[],
),
);
};
}
查看TokenStream输出
运行cargo check或cargo run。
可以看到attrs为属性宏传递的自定义属性,input为方法内容。
// 空属性输出
attrs:TokenStream []
input:TokenStream [
Ident {
ident: "fn",
span: #0 bytes(218..220),
},
Ident {
ident: "custom_proc_macro_attribute_fn",
span: #0 bytes(221..251),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: #0 bytes(251..253),
},
Group {
delimiter: Brace,
stream: TokenStream [
Ident {
ident: "println",
span: #0 bytes(260..267),
},
Punct {
ch: '!',
spacing: Alone,
span: #0 bytes(267..268),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
kind: Str,
symbol: "this is custom_proc_macro_attribute_fn()",
suffix: None,
span: #0 bytes(269..311),
},
],
span: #0 bytes(268..312),
},
Punct {
ch: ';',
spacing: Alone,
span: #0 bytes(312..313),
},
],
span: #0 bytes(254..315),
},
]
// 自定义属性输出
attrs:TokenStream [
Literal {
kind: Str,
symbol: "this is custom_proc_macro_attribute",
suffix: None,
span: #0 bytes(384..421),
},
]
input:TokenStream [
Ident {
ident: "fn",
span: #0 bytes(424..426),
},
Ident {
ident: "custom_proc_macro_attribute_fn_ha_attribute",
span: #0 bytes(427..470),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [],
span: #0 bytes(470..472),
},
Group {
delimiter: Brace,
stream: TokenStream [
Ident {
ident: "println",
span: #0 bytes(479..486),
},
Punct {
ch: '!',
spacing: Alone,
span: #0 bytes(486..487),
},
Group {
delimiter: Parenthesis,
stream: TokenStream [
Literal {
kind: Str,
symbol: "this is custom_proc_macro_attribute_fn() with attribute",
suffix: None,
span: #0 bytes(488..545),
},
],
span: #0 bytes(487..546),
},
Punct {
ch: ';',
spacing: Alone,
span: #0 bytes(546..547),
},
],
span: #0 bytes(473..549),
},
]
进阶
修改定义中的代码,使用syn
和quote
库来将TokenStream
转化为语法树,再对语法树进行处理。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, Item};
#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {
// 使用parse_macro_input!宏将attr转换为语法树
eprintln!("{:#?}", parse_macro_input!(attr as AttributeArgs));
// 转换成语法树
let body_ast = parse_macro_input!(item as Item);
// eprintln!("{:#?}", body_ast);
match body_ast {
// 获取到函数及相关属性
Item::Fn(ref o) => {
eprintln!("o:{:#?}", o);
eprintln!("vis:{:#?}", o.vis); // 可见性
eprintln!("attrs:{:#?}", o.attrs); // 特性
eprintln!("sig:{:#?}", o.sig); // 方法签名
eprintln!("block:{:#?}", o.block); // 方法体
}
_ => {}
}
// quote!宏将语法树重新转换为TokenStream
// 返回的实际结果为proc_macro2::TokenStream,要into转换为proc_macro::TokenStream
quote!(#body_ast).into()
// 返回空内容,将清楚函数内容
// quote!({}).into()
}
以上列出了部分属性的获取,更多属性可以点击类型进去查看。
派生式过程宏
Derive macro,用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
定义
// 派生式过程宏 CuctomProcMacroDerive 为宏名称
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
eprint!("cuctom_proc_macro_derive:{:#?}", input);
// 派生一个叫cuctom_proc_macro_derive_fn的方法
"fn cuctom_proc_macro_derive_fn() -> i32 {100}"
.parse()
.unwrap()
// 返回空代表没派生内容
// proc_macro::TokenStream::new()
}
使用
#[derive(CuctomProcMacroDerive)]
struct Student;
fn main() {
// 派生宏中派生的新方法,可以直接使用。
cuctom_proc_macro_derive_fn();
}
查看宏扩展结果
可以看到展开后有了自定义的方法cuctom_proc_macro_derive_fn,这个方法我们可以直接使用。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
struct Student;
fn cuctom_proc_macro_derive_fn() -> i32 {
100
}
fn main() {
cuctom_proc_macro_derive_fn();
}
查看TokenStream输出结果
cuctom_proc_macro_derive:TokenStream [
Ident {
ident: "struct",
span: #0 bytes(59..65),
},
Ident {
ident: "Student",
span: #0 bytes(66..73),
},
Punct {
ch: ';',
spacing: Alone,
span: #0 bytes(73..74),
},
]
进阶
修改定义中的代码,使用syn
和quote
库来将TokenStream
转化为语法树,再对语法树进行处理。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
// 派生式过程宏 CuctomProcMacroDerive 为宏名称
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// 转换成语法树,注意这里的类型为DeriveInput
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident; // 名称
let attrs = input.attrs; // 特性
let vis = input.vis; // 可见性
let generics = input.generics; // 泛型
let data = input.data; // 数据
// 生成输出
let expanded = quote! {
fn cuctom_proc_macro_derive_fn() -> i32 {100}
impl Trait for #name {
fn print(&self) -> usize {
println!("{}","hello from #name")
}
}
};
// 返回TokenStream
expanded.into()
// 等同于
// proc_macro::TokenStream::from(expanded)
}
以上列出了部分属性的获取,更多属性可以点击类型进去查看。
函数式过程宏
Function-like macro,用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。
定义
// 函数式过程宏 custom_proc_macro 为宏名称
#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
eprint!("custom_proc_macro:{:#?}", input);
"fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()
// proc_macro::TokenStream::new()
}
使用
fn main() {
custom_proc_macro!();
custom_proc_macro!(123);
// 宏中定义的函数可直接使用
custom_proc_macro_fn();
}
查看宏扩展结果
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
fn main() {
fn custom_proc_macro_fn() -> i32 {
100
}
custom_proc_macro_fn();
}
查看TokenStream输出结果
可以看到input内容为使用时传入的内容。
custom_proc_macro:TokenStream []
custom_proc_macro:TokenStream [
Literal {
kind: Integer,
symbol: "123",
suffix: None,
span: #0 bytes(233..236),
},
]
进阶
修改定义中的代码,使用syn
和quote
库来将TokenStream
转化为语法树,再对语法树进行处理。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
// eprint!("custom_proc_macro:{:#?}", input);
// "fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()
quote!({
fn custom_proc_macro_fn() -> i32 {
100
}
})
.into()
}
总结
本文先介绍了项目搭建以及过程宏的定义和简单使用及进阶使用。可以看出原始代码都被解析成TokenStream类型的树形结构数据,但如果我们想对TokenStream
做一些处理,去直接解析、修改这样的结构是非常复杂的。我们可以使用syn
库来将TokenStream
转化为具有语义信息、抽象程度更高的语法树,再对语法树进行处理,处理完成后再使用quote
返回TokenStream
。