在这篇教程中,我们介绍了 Rust 中宏的基础知识,定义了声明性宏和过程宏,并介绍了如何使用各种语法和社区的 crate 来编写这两种类型的宏。我们还总结了使用每种类型的 Rust 宏的优势。
在本教程中,我们将介绍关于 Rust宏的所有知识,包括 Rust 宏的介绍,并通过实例演示如何使用 Rust 宏。
文章会介绍以下内容:
Rust 宏是什么?
Rust 宏的类型
Rust 宏的声明
Rust 中的过程宏
Rust 对宏有很好的支持。宏可以通过一种代码来生成另一种代码,这被称为元编程(metaprogramming)。
宏提供了类似于函数的功能,但没有运行时的成本。然而,有一些编译时的成本,因为宏在编译时被扩展。
Rust 的宏与 C 语言中的宏有很大不同,Rust 的宏是应用于词法树,而 C 语言的宏只是文本替换。
Rust 有 2 种类型的宏:
声明式宏 (Declarative macros)使你能够编写类似于 match 表达式的东西,对你提供的作为参数的 Rust 代码进行操作。它使用你提供的代码来生成取代宏调用的代码
过程宏 (Procedural macros)允许你对它所给的 Rust 代码的抽象语法树(AST)进行操作。一个 proc 宏是一个从一个TokenStream'(或两个)到另一个
TokenStream'的函数,其中的输出取代了宏的调用
让我们放大声明式宏和过程式宏,并探索如何在 Rust 中使用宏的示例。
这些宏使用macro_rules!
声明。声明式宏的功能稍弱一些,但提供了一个易于使用的接口,用于创建宏以删除重复的代码。
一个常见的声明式宏是println!
。声明式宏提供了一个类似于接口的match
,一旦匹配,宏就会被替换为匹配分支内的代码。
// use macro_rules! <name of macro>{<Body>}
macro_rules! add{
// macth like arm for macro
($a:expr,$b:expr)=>{
// macro expand to this code
{
// $a and $b will be templated using the value/variable provided to macro
$a+$b
}
}
}
fn main(){
// call to macro, $a=1 and $b=2
add!(1,2);
}
这段代码创建了一个宏:2 个数相加。我们使用 macro_rules!
来定义宏,宏的名称是 add
。
这个宏并没有把两个数字相加,它只是把自己替换成加两个数字的代码。宏的每个分支都需要一个函数参数,并且可以为参数指定多种类型。如果 add
只接受一个参数,我们可以添加其他分支。
macro_rules! add{
// first arm match add!(1,2), add!(2,3) etc
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// Second arm macth add!(1), add!(2) etc
($a:expr)=>{
{
$a
}
}
}
fn main(){
// call the macro
let x=0;
add!(1,2);
add!(x);
}
一个宏中可以有多个分支,根据不同的参数匹配到不同的分支。每个分支可以接受多个参数,参数以$
作为开头,后面是一个 token 类型:
item
—— 一个项(item),像一个函数,结构体,模块等。
block
—— 一个块(block)(即一个语句块或一个表达式,由花括号所包围)
stmt
—— 一个语句(statement)
pat
—— 一个模式(pattern)
expr
—— 一个表达式(expression)
ty
—— 一个类型(type)
ident
—— 一个标识符(indentfier)
path
—— 一个路径(path)(例如,foo,::std::mem::replace,transmute::<_, int>,...)
meta
—— 一个元数据项;位于#[...] 和#![...] 属性
tt
—— 一个词法树
vis
—— 一个可能为空的 Visibility 限定词
在示例中,我们使用标记类型为 ty
的 $typ
参数作为数据类型,如 u8
、u16
等。此宏在添加数字之前转换为特定类型。
macro_rules! add_as{
// using a ty token type for macthing datatypes passed to maccro
($a:expr,$b:expr,$typ:ty)=>{
$a as $typ + $b as $typ
}
}
fn main(){
println!("{}",add_as!(0,2,u8));
}
Rust 宏也支持接受非固定数量的参数。操作符与正则表达式非常相似。*
用于零个或多个标记类型,+
用于零个或一个参数。
macro_rules! add_as{
(
// repeated block
$($a:expr)
// seperator
,
// zero or more
*
)=>{
{
// to handle the case without any arguments
0
// block to be repeated
$(+$a)*
}
}
}
fn main(){
println!("{}",add_as!(1,2,3,4)); // => println!("{}",{0+1+2+3+4})
}
重复的标记类型包含在$()
中,后面跟着一个分隔符和一个*
或+
,表示该标记将重复的次数。分隔符用于区分不同的标记。$()
块后跟*
或+
用于表示重复的代码块。在上面的例子中,+$a
是一个重复的代码。
如果你仔细观察,你会发现在代码中加入了一个额外的 0,以使语法有效。为了去除这个零,使add
表达式与参数相同,我们需要创建一个新的宏,称为TT muncher。
macro_rules! add{
// first arm in case of single argument and last remaining variable/number
($a:expr)=>{
$a
};
// second arm in case of two arument are passed and stop recursion in case of odd number ofarguments
($a:expr,$b:expr)=>{
{
$a+$b
}
};
// add the number and the result of remaining arguments
($a:expr,$($b:tt)*)=>{
{
$a+add!($($b)*)
}
}
}
fn main(){
println!("{}",add!(1,2,3,4));
}
TT muncher 以递归的方式分别处理每个 token。一次只处理一个 token 是比较容易的。这个宏有三个分支:
如果传递了一个参数,则第一个分支处理这种情况
第二个函数处理传递两个参数的情况
第三个分支使用其他参数再次调用add
宏
宏参数不需要用逗号分隔。多个令牌可以用于不同的令牌类型。例如,括号可以与 ident
标记类型一起使用。Rust 编译器获取匹配的分支,并从参数字符串中提取变量。
macro_rules! ok_or_return{
// match something(q,r,t,6,7,8) etc
// compiler extracts function name and arguments. It injects the values in respective varibles.
($a:ident($($b:tt)*))=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
ok_or_return!(some_work(1,4));
ok_or_return!(some_work(1,0));
Ok(())
}
如果一个操作返回 Err
或者一个操作的值返回 Ok
,那么ok_or_return
宏就会返回该函数。它接受一个函数作为参数,并在一个 match 语句中执行它。对于传递给函数的参数,它使用重复的方式。
通常情况下,很少有宏需要组合成一个宏。在这些情况下,要使用内部宏规则。它有助于操作宏的输入,并写出干净的 TT Munchers。
要创建内部规则,需要添加以 @
开头的规则名称作为参数。现在,除非明确指定为参数,否则宏将永远不会匹配内部规则。
macro_rules! ok_or_return{
// internal rule.
(@error $a:ident,$($b:tt)* )=>{
{
match $a($($b)*) {
Ok(value)=>value,
Err(err)=>{
return Err(err);
}
}
}
};
// public rule can be called by the user.
($a:ident($($b:tt)*))=>{
ok_or_return!(@error $a,$($b)*)
};
}
fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
if i+j>2 {
Ok((i,j))
} else {
Err("error".to_owned())
}
}
fn main()->Result<(),String>{
// instead of round bracket curly brackets can also be used
ok_or_return!{some_work(1,4)};
ok_or_return!(some_work(1,0));
Ok(())
}
宏有时会执行需要解析 Rust 语言本身的任务。
把我们已经讲过的所有概念放在一起,让我们创建一个宏,通过添加 pub
关键字来使 struct 公开。
首先,我们需要解析 Rust struct 以获取 struct 的名称、struct 的字段和字段类型。
struct
声明的开头有一个可见性关键字(例如 pub
),后跟 struct
关键字,然后是 struct
的名称和 struct
的主体。
macro_rules! make_public{
(
// use vis type for visibility keyword and ident for struct name
$vis:vis struct $struct_name:ident { }
) => {
{
pub struct $struct_name{ }
}
}
}
$vis
具有可见性,$struct_name
具有一个 struct 名称。要使 struct 公开,我们只需要添加pub
关键字,并忽略$vis
变量。
一个struct
可以包含多个具有相同或不同数据类型和可见性的字段。ty
标记类型用于数据类型,vis
用于可见性,而ident
用于字段名。我们将使用*
重复来表示零个或多个字段。
macro_rules! make_public{
(
$vis:vis struct $struct_name:ident {
$(
// vis for field visibility, ident for field name and ty for field data type
$field_vis:vis $field_name:ident : $field_type:ty
),*
}
) => {
{
pub struct $struct_name{
$(
pub $field_name : $field_type,
)*
}
}
}
}
通常情况下,struct
有一些元数据或过程宏,如#[derive(Debug)]
。这些元数据需要保持原样。使用 meta
类型解析这些元数据。
macro_rules! make_public{
(
// meta data about struct
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
// meta data about field
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
{
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
}
make_public
宏现在已经准备好了。为了看看 make_public
是如何工作的,让我们用 Rust Playground 将宏扩展到实际编译的代码中。
macro_rules! make_public{
(
$(#[$meta:meta])*
$vis:vis struct $struct_name:ident {
$(
$(#[$field_meta:meta])*
$field_vis:vis $field_name:ident : $field_type:ty
),*$(,)+
}
) => {
$(#[$meta])*
pub struct $struct_name{
$(
$(#[$field_meta:meta])*
pub $field_name : $field_type,
)*
}
}
}
fn main(){
make_public!{
#[derive(Debug)]
struct Name{
n:i64,
t:i64,
g:i64,
}
}
}
扩展后的代码看起来像这样:
// some imports
macro_rules! make_public {
($ (#[$ meta : meta]) * $ vis : vis struct $ struct_name : ident
{
$
($ (#[$ field_meta : meta]) * $ field_vis : vis $ field_name : ident
: $ field_type : ty), * $ (,) +
}) =>
{
$ (#[$ meta]) * pub struct $ struct_name
{
$
($ (#[$ field_meta : meta]) * pub $ field_name : $
field_type,) *
}
}
}
fn main() {
pub struct name {
pub n: i64,
pub t: i64,
pub g: i64,
}
}
声明式宏有一些限制。有些是与 Rust 宏本身有关,有些则是声明式宏所特有的:
缺少对宏的自动完成和展开的支持
声明式宏调式困难
修改能力有限
更大的二进制
更长的编译时间(这一条对于声明式宏和过程宏都存在)
过程宏 (Procedural macros)是一种更为高级的宏。过程宏能够扩展 Rust 的现有语法。它接收任意输入并产生有效的 Rust 代码。
过程宏接收一个 TokenStream
作为参数并返回另一个 TokenStream
。过程宏对输入的 TokenStream
进行操作并产生一个输出。有三种类型的过程宏:
类属性宏使你能够创建一个自定义的属性,将其附加到一个项目上,并允许对该项目进行操作。它也可以接受参数。
#[some_attribute_macro(some_argument)]
fn perform_task(){
// some code
}
在上面代码中,some_attribute_macros
是一个类属性宏。它操纵着函数perform_task
。
要写一个类似属性的宏,先用cargo new macro-demo --lib
创建一个项目。项目准备好后,更新 Cargo.toml
,注意 [lib]
,该项目将会创建过程宏。
# Cargo.toml
[lib]
proc-macro = true
程序性宏是公共函数,接受TokenStream
作为输入,并返回另一个TokenStream
。为了编写程序性宏,我们需要编写解析器来解析TokenStream
。Rust 社区有一个非常好用的工具syn
,用于解析TokenStream
。
syn提供了一个现成的 Rust 语法解析器能够用于解析 TokenStream
。你可以通过组合 syn 提供的底层解析器来解析自己的语法。
把 syn
和 quote
添加到 Cargo.toml
:
# Cargo.toml
[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"
现在我们可以在 lib.rs
中使用编译器提供的 proc_macro
来编写程序性宏,编写一个类属性的宏。一个过程宏包不能导出除过程宏以外的任何东西,并且包中定义的过程宏不能在包本身中使用。
// lib.rs
extern crate proc_macro;
use proc_macro::{TokenStream};
use quote::{quote};
// using proc_macro_attribute to declare an attribute like procedural macro
#[proc_macro_attribute]
// _metadata is argument provided to macro call and _input is code to which attribute like macro attaches
pub fn my_custom_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
// returing a simple TokenStream for Struct
TokenStream::from(quote!{struct H{}})
}
通过创建一个名为tests
的文件夹并在该文件夹中添加文件attribute_macro.rs
来创建一个测试。在这个文件中,我们可以使用类属性宏进行测试。
// tests/attribute_macro.rs
use macro_demo::*;
// macro converts struct S to struct H
#[my_custom_attribute]
struct S{}
#[test]
fn test_macro(){
// due to macro we have struct H in scope
let demo=H{};
}
然后使用 cargo test
命令进行测试。
现在我们了解了程序性宏的基础知识,让我们使用 syn
来进行一些高级的TokenStream
操作和解析。
为了理解 syn 是如何用来解析和操作的,让我们来看 syn Github 仓库上的一个示例。这个示例创建了一个 Rust 宏,这个宏可以追踪变量值的变化。
首先,我们需要去验证,宏是如何操作与其所关联的代码的:
#[trace_vars(a)]
fn do_something(){
let a=9;
a=6;
a=0;
}
trace_vars
宏接受它需要跟踪的变量名称,并在每次输入变量的值(即 a 更改)时注入一个打印语句。它会跟踪输入变量的值。
首先,解析类属性宏所附加的代码。syn 为 Rust 函数语法提供了一个内置解析器。 ItemFn
将解析函数并在语法无效时抛出错误。
#[proc_macro_attribute]
pub fn trace_vars(_metadata: TokenStream, input: TokenStream) -> TokenStream {
// parsing rust function to easy to use struct
let input_fn = parse_macro_input!(input as ItemFn);
TokenStream::from(quote!{fn dummy(){}})
}
现在有了解析过的 input
,我们转向metadata
。对于 metadata
,没有内置的解析器,所以我们必须使用syn
的parse
模块自己写一个。
#[trace_vars(a,c,b)] // we need to parse a "," seperated list of tokens
// code
为了让 syn
工作,我们需要实现 syn
提供的 Parse
trait。 Punctuated
用于创建由,
分隔的Indent
的vector
。
struct Args{
vars:HashSet<Ident>
}
impl Parse for Args{
fn parse(input: ParseStream) -> Result<Self> {
// parses a,b,c, or a,b,c where a,b and c are Indent
let vars = Punctuated::<Ident, Token![,]>::parse_terminated(input)?;
Ok(Args {
vars: vars.into_iter().collect(),
})
}
}
一旦我们实现了 Parse
trait,我们就可以使用 parse_macro_input
宏来解析 metadata
。
#[proc_macro_attribute]
pub fn trace_vars(metadata: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
// using newly created struct Args
let args= parse_macro_input!(metadata as Args);
TokenStream::from(quote!{fn dummy(){}})
}
我们现在要修改 input_fn
,以便在变量改变值时添加println!
。为了添加这个,我们需要过滤出有复制语句的代码,并在该行后插入一个 print 语句。
impl Args {
fn should_print_expr(&self, e: &Expr) -> bool {
match *e {
Expr::Path(ref e) => {
// variable shouldn't start wiht ::
if e.path.leading_colon.is_some() {
false
// should be a single variable like `x=8` not n::x=0
} else if e.path.segments.len() != 1 {
false
} else {
// get the first part
let first = e.path.segments.first().unwrap();
// check if the variable name is in the Args.vars hashset
self.vars.contains(&first.ident) && first.arguments.is_empty()
}
}
_ => false,
}
}
// used for checking if to print let i=0 etc or not
fn should_print_pat(&self, p: &Pat) -> bool {
match p {
// check if variable name is present in set
Pat::Ident(ref p) => self.vars.contains(&p.ident),
_ => false,
}
}
// manipulate tree to insert print statement
fn assign_and_print(&mut self, left: Expr, op: &dyn ToTokens, right: Expr) -> Expr {
// recurive call on right of the assigment statement
let right = fold::fold_expr(self, right);
// returning manipulated sub-tree
parse_quote!({
#left #op #right;
println!(concat!(stringify!(#left), " = {:?}"), #left);
})
}
// manipulating let statement
fn let_and_print(&mut self, local: Local) -> Stmt {
let Local { pat, init, .. } = local;
let init = self.fold_expr(*init.unwrap().1);
// get the variable name of assigned variable
let ident = match pat {
Pat::Ident(ref p) => &p.ident,
_ => unreachable!(),
};
// new sub tree
parse_quote! {
let #pat = {
#[allow(unused_mut)]
let #pat = #init;
println!(concat!(stringify!(#ident), " = {:?}"), #ident);
#ident
};
}
}
}
在上面的例子中,quote
宏用于模板化和编写 Rust。#
用于注入变量的值。
现在我们在input_fn
上做 DFS,并插入 print。syn
提供了一个Fold
trait,可以在任何 Item
上实现 DFS。我们只需要修改与我们要操作的标记类型相对应的 trait 方法。
impl Fold for Args {
fn fold_expr(&mut self, e: Expr) -> Expr {
match e {
// for changing assignment like a=5
Expr::Assign(e) => {
// check should print
if self.should_print_expr(&e.left) {
self.assign_and_print(*e.left, &e.eq_token, *e.right)
} else {
// continue with default travesal using default methods
Expr::Assign(fold::fold_expr_assign(self, e))
}
}
// for changing assigment and operation like a+=1
Expr::AssignOp(e) => {
// check should print
if self.should_print_expr(&e.left) {
self.assign_and_print(*e.left, &e.op, *e.right)
} else {
// continue with default behaviour
Expr::AssignOp(fold::fold_expr_assign_op(self, e))
}
}
// continue with default behaviour for rest of expressions
_ => fold::fold_expr(self, e),
}
}
// for let statements like let d=9
fn fold_stmt(&mut self, s: Stmt) -> Stmt {
match s {
Stmt::Local(s) => {
if s.init.is_some() && self.should_print_pat(&s.pat) {
self.let_and_print(s)
} else {
Stmt::Local(fold::fold_local(self, s))
}
}
_ => fold::fold_stmt(self, s),
}
}
}
Fold
trait 用于对 Item
进行 DFS。它允许你为各种 token 类型使用不同的行为。
现在我们可以使用 fold_item_fn
在解析后的代码中注入打印语句。
#[proc_macro_attribute]
pub fn trace_var(args: TokenStream, input: TokenStream) -> TokenStream {
// parse the input
let input = parse_macro_input!(input as ItemFn);
// parse the arguments
let mut args = parse_macro_input!(args as Args);
// create the ouput
let output = args.fold_item_fn(input);
// return the TokenStream
TokenStream::from(quote!(#output))
}
这个代码示例来自于 syn 示例仓库,该仓库也是关于过程宏的一个非常好的学习资源。
Rust 中的自定义派生宏允许自动实现 traits。这些宏使你能够使用 #[derive(Trait)]
实现特征。
syn
对 derive
宏有很好的支持。
#[derive(Trait)]
struct MyStruct{}
To write a custom derive macro in Rust, we can use DeriveInput
for parsing input to derive macro. We’ll also use the proc_macro_derive
macro to define a custom derive macro.
要在 Rust 中编写自定义派生宏,我们可以使用 DeriveInput
来解析输入以派生宏。我们还将使用 proc_macro_derive
宏来定义一个自定义派生宏。
#[proc_macro_derive(Trait)]
pub fn derive_trait(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl Trait for #name {
fn print(&self) -> usize {
println!("{}","hello from #name")
}
}
};
proc_macro::TokenStream::from(expanded)
}
使用 syn 可以编写更为高级的过程宏,请查阅 syn 仓库中的这个示例。
类函数宏类似于声明式宏,因为他们都通过宏调用操作符!
来执行,并且看起来都像是函数调用。它们都作用于圆括号里的代码。
下面是如何在 Rust 中写一个函数式宏:
#[proc_macro]
pub fn a_proc_macro(_input: TokenStream) -> TokenStream {
TokenStream::from(quote!(
fn anwser()->i32{
5
}
))
}
类似函数的宏不是在运行时执行,而是在编译时执行。它们可以在 Rust 代码中的任何地方使用。类似函数的宏也接受一个TokenStream
并返回一个TokenStream
。
使用过程宏的优势有:
span
获得更好的错误处理syn
和 quote
两个 crate如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!