原文标题:Macros in Rust: A tutorial with examples
原文链接:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
公众号: Rust 碎碎念
翻译 by: Praying
知乎:https://www.zhihu.com/column/c_1186237256184029184
感谢Praying大佬的翻译。上一篇文章讲解了Rust声明式宏 ,这篇文章将继续带你深入了解更复杂、也更强大的Rust过程宏。
Rust 中的过程宏
过程宏(Procedural macros)是一种更为高级的宏。过程宏能够扩展 Rust 的现有语法。它接收任意输入并产生有效的 Rust 代码。
过程宏接收一个TokenStream
作为参数并返回另一个TokenStream
。过程宏对输入的TokenStream
进行操作并产生一个输出。有三种类型的过程宏:
- 属性式宏(Attribute-like macros)
- 派生宏(Derive macros)
- 函数式宏(Function-like macros)
接下来我们将会对它们进行详细讨论。
属性式宏
属性式宏能够让你创建一个自定义的属性,该属性将其自身关联一个项(item),并允许对该项进行操作。它也可以接收参数。
#[some_attribute_macro(some_argument)]
fn perform_task(){
// some code
}
在上面的代码中,some_attribute_macros
是一个属性宏,它对函数perform_task
进行操作。
为了编写一个属性式宏,我们先用cargo new macro-demo --lib
来创建一个项目。创建完成后,修改Cargo.toml
来通知 cargo,该项目将会创建过程宏。
# Cargo.toml
[lib]
proc-macro = true
现在,我们可以开始过程宏学习之旅了。
过程宏是公开的函数,接收TokenStream
作为参数并返回另一个TokenStream
。要想写一个过程宏,我们需要先实现能够解析TokenStream
的解析器。Rust 社区已经有了很好的 crate——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"
现在我们可以使用proc_macro
在lib.rs
中写一个属性式宏,proc_macro
是编译器提供的用于写过程宏的一个 crate。对于一个过程宏 crate,除了过程宏外,不能导出其他任何东西,crate 中定义的过程宏不能在 crate 自身中使用。
// 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。我们只需要修改与我们想要操作的 token 类型所对应的 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
在我们解析的代码中注入 print 语句。
#[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 中的自定义派生宏能够对 trait 进行自动实现。这些宏通过使用#[derive(Trait)]
自动实现 trait。
syn
对derive
宏有很好的支持。
#[derive(Trait)]
struct MyStruct{}
要想在 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 - 比声明式宏更为强大
总结
在这篇 Rust 教程中,我们涵盖了 Rust 中关于宏的基本内容,声明式宏和过程宏的定义,以及如果使用各种语法和社区的 crate 来编写这两种类型的宏。我们还总结了每种类型的 Rust 宏所具有优势。