原文标题:Macros in Rust: A tutorial with examples


原文链接:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/

公众号: Rust 碎碎念


翻译 by: Praying

Rust 中的过程宏

过程宏(Procedural macros)[1]是一种更为高级的宏。过程宏能够扩展 Rust 的现有语法。它接收任意输入并产生有效的 Rust 代码。

过程宏接收一个TokenStream作为参数并返回另一个TokenStream。过程宏对输入的TokenStream进行操作并产生一个输出。有三种类型的过程宏:

  1. 属性式宏(Attribute-like macros)
  2. 继承宏(Derive macros)
  3. 函数式宏(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[2],用于解析TokenStream

syn提供了一个现成的 Rust 语法解析器能够用于解析TokenStream。你可以通过组合syn提供的底层解析器来解析你自己的语法、

syn和quote[3]添加到Cargo.toml

# Cargo.toml
[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

现在我们可以使用proc_macrolib.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 仓库[4]上的一个示例。这个示例创建了一个 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,没有适用的内置解析器,所以我们必须自己使用synparse模块写一个解析器。

#[trace_vars(a,c,b)] // we need to parse a "," seperated list of tokens
// code

要想syn能够工作,我们需要实现syn提供的Parse trait。Punctuated用于创建一个由,分割Indentvector

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提供了一个Foldtrait 可以用来对任意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 示例仓库[5],该仓库也是关于过程宏的一个非常好的学习资源。

自定义继承宏

Rust 中的自定义继承宏能够对 trait 进行自动实现。这些宏通过使用#[derive(Trait)]自动实现 trait。

synderive宏有很好的支持。

#[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仓库中的这个示例[6]

函数式宏

函数式宏类似于声明式宏,因为他们都通过宏调用操作符!来执行,并且看起来都像是函数调用。它们都作用于圆括号里的代码。

下面是如何在 Rust 中写一个函数式宏:

#[proc_macro]
pub fn a_proc_macro(_input: TokenStream) -> TokenStream {
    TokenStream::from(quote!(
            fn anwser()->i32{
                5
            }
))
}

函数式宏在编译期而非在运行时执行。它们可以在 Rust 代码的任何地方被使用。函数式宏同样也接收一个TokenStream并返回一个TokenStream

使用过程宏的优势包括:

  • 使用span获得更好的错误处理
  • 更好的控制输出
  • 社区已有synquote两个 crate
  • 比声明式宏更为强大

总结

在这篇 Rust 教程中,我们涵盖了 Rust 中关于宏的基本内容,声明式宏和过程宏的定义,以及如果使用各种语法和社区的 crate 来编写这两种类型的宏。我们还总结了每种类型的 Rust 宏所具有优势。

参考资料

[1]

过程宏(Procedural macros): https://blog.logrocket.com/procedural-macros-in-rust/

[2]

syn: https://crates.io/crates/syn

[3]

quote: https://crates.io/crates/quote

[4]

syn Github 仓库: https://github.com/dtolnay/syn/blob/master/examples/trace-var/trace-var/src/lib.rs

[5]

syn 示例仓库: https://github.com/dtolnay/syn/blob/master/examples/trace-var/trace-var/src/lib.rs

[6]

这个示例: https://github.com/dtolnay/syn/blob/master/examples/heapsize/heapsize_derive/src/lib.rs

【译】Rust宏:教程与示例(二)的更多相关文章

  1. 【译】Rust宏:教程与示例(一)

    原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...

  2. Senparc.Weixin.MP SDK 微信公众平台开发教程(十二):OAuth2.0说明

    紧接上一篇<Senparc.Weixin.MP SDK 微信公众平台开发教程(十一):高级接口说明>,这里专讲OAuth2.0. 理解OAuth2.0 首先我们通过一张图片来了解一下OAu ...

  3. 微信小程序 教程及示例

    作者:初雪链接:https://www.zhihu.com/question/50907897/answer/128494332来源:知乎著作权归作者所有,转载请联系作者获得授权.微信小程序正式公测, ...

  4. [OpenCV入门教程之十二】OpenCV边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑

    http://blog.csdn.net/poem_qianmo/article/details/25560901 本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog ...

  5. Github团队开发示例(二)

    Github团队开发示例(二) 作者:Grey 原文地址:http://www.cnblogs.com/greyzeng/p/6063765.html 接之前讲的Github团队开发示例(一),本文主 ...

  6. 【OpenCV新手教程之十二】OpenCV边缘检測:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/25560901 作者:毛星云(浅墨) ...

  7. WPF命中测试示例(二)——几何区域命中测试

    原文:WPF命中测试示例(二)--几何区域命中测试 接续上次的命中测试,这次来做几何区域测试示例. 示例 首先新建一个WPF项目,在主界面中拖入一个按钮控件,并修改代码中的以下高亮位置: 当前设计视图 ...

  8. 【转】抓包工具Fiddler的使用教程(十二)下:Fiddler抓取HTTPS

    在教程十二(上),我们也了解了HTTPS协议,该教程就和大家分享Fiddler如何抓取HTTPS 抓包工具Fiddler的使用教程(十二):[转载]HTTPS协议 再次回忆一下关键内容: iddler ...

  9. pyspider 示例二 升级完整版绕过懒加载,直接读取图片

    pyspider 示例二 升级完整版绕过懒加载,直接读取图片,见[升级写法处] #!/usr/bin/env python # -*- encoding: utf-8 -*- # Created on ...

随机推荐

  1. CPU饥饿与线程饥饿

    线程饥饿: 进程无法得到资源,(cpu或者io资源或者别的什么资源),所以无法进行下去 比如说读者写者问题,如果读者优先,那么写者可能会饿死. 又比如操作系统概念的一道习题. 用broadcast可能 ...

  2. C++ part6

    C++源文件从文本到可执行文件经历的过程 references: 一个C++源文件从文本到可执行文件经历的过程 C++源文件从文本到可执行文件经历的过程 gcc程序编译的静态链接和动态链接 保留中间过 ...

  3. 在线可视化设计网站 & 在线编辑器

    在线可视化设计网站 在线编辑器:海报编辑器.H5 编辑器.视频编辑器.音频编辑器.抠图编辑器 在线 拖拽 可视化 编辑器 Canvas WebGL Canva With Canva, anyone c ...

  4. ServerLess & MongoDB Atlas & REST API

    ServerLess & MongoDB Atlas & REST API ServerLess, Nodejs, MongoDB Atlas cloud 构建 REST API ht ...

  5. 电信悦 me 智能网关

    电信悦 me 智能网关 悦 me 智能网关 Q1:什么是电信悦 me 智能网关? 悦me网关是智慧家庭的核心终端,作为"光猫+智能路由器"的集合体, 采用了全新的硬件.外观及智能操 ...

  6. GitHub Sponsors

    GitHub Sponsors https://github.com/sponsors https://github.com/sponsors/xgqfrms?preview=true https:/ ...

  7. 找出 int 数组的平衡点 & 二叉树 / 平衡二叉树 / 满二叉树 / 完全二叉树 / 二叉查找树

    找出 int 数组的平衡点 左右两边和相等, 若存在返回平衡点的值(可能由多个); 若不存在返回 -1; ``java int [] arr = {2,3,4,2,4}; ```js const ar ...

  8. React Hooks & react forwardRef hooks & useReducer

    React Hooks & react forwardref hooks & useReducer react how to call child component method i ...

  9. NGK算力生态建设者狂欢!SPC之后又有VAST!

    想致富,先挖矿.这句话已经成为了币圈的一句名言.挖矿一词始终贯穿着区块链以及数字加密领域. 据小道消息透露,NGK官最近将会推出两款挖矿产品---SPC星空币以及其子币VAST维萨币. 下面笔者就来一 ...

  10. K8S部署Redis Cluster集群

    kubernetes部署单节点redis: https://www.cnblogs.com/zisefeizhu/p/14282299.html Redis 介绍 • Redis代表REmote DI ...