Rust中的宏:带有示例的教程

2021-02-26 07:38:17

在本教程中,我们将介绍有关Rust宏的所有必要知识,包括Rust中的宏介绍以及示例中如何使用Rust宏的演示。

Rust对宏提供了出色的支持。宏使您能够编写编写其他代码的代码,这称为元编程。

宏提供的功能类似于功能,但没有运行时成本。但是,由于在编译期间扩展了宏,因此存在一些编译时的开销。

Rust宏与C中的宏非常不同。Rust宏应用于令牌树,而C宏是文本替换。

声明性宏使您可以编写类似于在作为参数提供的Rust代码上运行的match表达式的内容。它使用您提供的代码来生成替换宏调用的代码

程序宏允许您对给出的Rust代码的抽象语法树(AST)进行操作。 proc宏是从TokenStream(或两个)到另一个TokenStream的函数,其中输出替换宏调用

让我们放大声明性和过程宏,并探讨一些如何在Rust中使用宏的示例。

这些宏使用macro_rules!声明。声明性宏的功能稍差一些,但提供了易于使用的接口来创建宏以删除重复的代码。常见的声明性宏之一是println!。声明性宏提供了与接口类似的匹配,在匹配时,匹配的宏将替换为匹配臂内部的代码。

//使用macro_rules! <宏的名称> {<正文>} macro_rules! add {//宏类似$的宏($ a:expr,$ b:expr)=> {{宏扩展为此代码{// $ a和$ b将使用提供给宏的值/变量进行模板化$ a + $ b}}} fn main(){//调用宏,$ a = 1和$ b = 2相加!(1,2);}

这段代码创建了一个宏,将两个数字相加。 [macro_rules!]与宏的名称,添加和宏的主体一起使用。

宏不会将两个数字相加,它只是将自己替换为添加两个数字的代码。宏的每个分支都为函数提供一个参数,并且可以将多种类型分配给参数。如果add函数也可以接受单个参数,则添加另一条手臂。

macro_rules! add {//第一臂匹配add!(1,2),add!(2,3)etc($ a:expr,$ b:expr)=> {{$ a + $ b}}; //第二臂macth add!(1),add!(2)等($ a:expr)=> {{$ a}}} fn main(){//调用宏let x = 0;加(1,2);加!(x);}

一个宏中可以有多个分支,可以根据不同的参数扩展为不同的代码。每个分支可以采用多个参数,以$符号开头,后跟令牌类型:

块-块(即语句和/或表达式的块,用大括号括起来)

在示例中,我们将令牌类型为ty的$ typ参数用作u8,u16等数据类型。在添加数字之前,此宏会转换为特定类型。

macro_rules! add_as {//将ty令牌类型用于传递给maccro的macthing数据类型($ a:expr,$ b:expr,$ typ:ty)=> {$ a为$ typ + $ b为$ typ}} fn main (){println!(" {}",add_as!(0,2,u8));}

Rust宏还支持采用非固定数量的参数。运算符与正则表达式非常相似。 *用于零个或多个令牌类型,而+用于零个或一个参数。

macro_rules! add_as {(//重复的块$($ a:expr)//分隔符,//零个或多个*)=> {{//处理不带任何参数的情况0 //重复的块$(+ $ a)*}}} fn main(){println!(" {}",add_as!(1,2,3,4)); // => println!(" {}",{0 + 1 + 2 + 3 + 4})}

重复的令牌类型包含在$()中,后跟分隔符和*或+,表示令牌将重复的次数。分隔符用于将令牌彼此区分开。 $()块后跟*或+表示重复的代码块。在上面的示例中,+ $ a是重复代码。

如果仔细观察,您会发现在代码中添加了另一个零,以使语法有效。要删除此零并使add表达式与参数相同,我们需要创建一个称为TT muncher的新宏。

macro_rules! add {//如果有单个参数,则第一个分支,最后一个剩余变量/数字($ a:expr)=> {$ a}; //如果传递了两个元素,则第二个分支;如果是奇数,则停止递归ofarguments($ a:expr,$ b:expr)=> {{$ a + $ b}}; //添加数字和剩余参数的结果($ a:expr,$($ b:tt)*) => {{$ a + add!($($ b)*)}}} fn main(){println!(" {}",add!(1,2,3,4 ));}

TT muncher以递归方式分别处理每个令牌。一次处理一个令牌比较容易。宏具有三个分支:

宏参数不需要用逗号分隔。可以将多个令牌与不同的令牌类型一起使用。例如,方括号可与ident令牌类型一起使用。 Rust编译器采用匹配的分支,并从参数字符串中提取变量。

macro_rules! ok_or_return {//匹配某些内容(q,r,t,6,7,8)etc //编译器提取函数名称和参数。它将值注入到各个变量中。 ($ a:ident($($ b:tt)*))=> {{match $ a($($ b)*){Ok(value)=> value,Err(err)=> {返回Err(err); }}}};} fn some_work(i:i64,j:i64)-> Result<(i64,i64),String> {如果i + j> 2 {Ok((i,j))}}其他{错误(" error" .to_owned())}} fn main()-> Result<(),String> {ok_or_return!(some_work(1,4)); ok_or_return!(some_work(1,0));好的(())}

如果某个操作返回Err或某个操作的值返回Ok,则ok_or_return宏将返回该函数。它以一个函数作为参数,并在match语句中执行它。对于传递给函数的参数,它使用重复。

通常,很少需要将宏分组为一个宏。在这些情况下,将使用内部宏规则。它有助于操纵宏输入并编写干净的TT muncher。

要创建内部规则,请添加以@开头的规则名称作为参数。现在,除非明确将其指定为参数,否则该宏将永远不会匹配内部规则。

macro_rules! ok_or_return {//内部规则。 (@error $ a:ident,$($ b:tt)*)=> {{match $ a($($ b)*){Ok(value)=> value,Err(err)=> {return Err(err); }} //}公共规则可以由用户调用。 ($ a:ident($($ b:tt)*))=> {ok_or_return!(@ error $ a,$($ b)*)};} fn some_work(i:i64,j:i64)- > Result<(i64,i64),String> {如果i + j> 2 {Ok((i,j))}否则{Err(" error" .to_owned())}} fn main ()-> Result<(),String> {//也可以使用ok_or_return!代替圆括号大括号!{some_work(1,4)}; ok_or_return!(some_work(1,0));好的(())}

务必将到目前为止已涵盖的所有概念放在一起,让我们创建一个宏,通过在pub关键字后缀将结构公开。

首先,我们需要解析Rust结构以获得结构的名称,结构的字段和字段类型。

struct声明的开头有一个可见性关键字(例如pub),其后是struct关键字,然后是该结构的名称和该结构的主体。

macro_rules! make_public {(//使用vis类型作为可见性关键字,使用ident作为结构名称$ vis:vis struct $ struct_name:ident {})=> {{pub struct $ struct_name {}}}}

$ vis将具有可见性,$ struct_name将具有结构名称。要公开一个结构,我们只需要添加pub关键字而忽略$ vis变量。

一个结构可以包含具有相同或不同数据类型和可见性的多个字段。 ty令牌类型用于数据类型,vis用于可见性,ident用于字段名称。我们将对零个或多个字段使用*重复。

macro_rules! make_public {($ vis:vis struct $ struct_name:ident {$(// vis表示字段可见性,ident表示字段名称,ty表示字段数据类型$ field_vis:vis $ field_name:ident:$ field_type:ty),*})) => {{pub struct $ struct_name {$(pub $ field_name:$ field_type,)*}}}}

通常,该结构具有一些附加的元数据或过程宏,例如#[derive(Debug)]。此元数据需要保持不变。使用元类型来解析此元数据。

macro_rules! make_public {(// //有关结构$(#[$ meta:meta])* * vis:vis的结构$ 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,)*}}}}

我们的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!{#[派生(调试)]结构名称{n:i64,t:i64,g:i64,}}}

//一些importmacro_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,酒吧t:i64,酒吧g:i64,}}

声明性宏有一些限制。一些与Rust宏本身相关,而另一些与声明性宏更特定。

过程宏是宏的更高级版本。过程宏允许您扩展Rust的现有语法。它接受任意输入并返回有效的Rust代码。

程序宏是将TokenStream作为输入并返回另一个Token Stream的函数。程序宏操纵输入的TokenStream生成输出流。

类似于属性的宏使您可以创建将其自身附加到项目并允许对该项目进行操作的自定义属性。它还可以接受参数。

要编写类似属性的宏,请先使用cargo new macro-demo --lib创建一个项目。项目准备就绪后,更新Cargo.toml以通知货物该项目将创建程序宏。

程序宏是将TokenStream作为输入并返回另一个TokenStream的公共函数。要编写程序宏,我们需要编写解析器以解析TokenStream。 Rust社区有一个很好的板条箱syn,用于解析TokenStream。

为Rust语法提供了现成的解析器,可用于解析TokenStream。您还可以通过组合提供syn的低级解析器来解析语法。

现在,我们可以使用编译器提供的proc_macro包在lib.rs中编写类似属性的宏,以编写过程宏。程序宏板条箱不能导出程序宏以外的其他任何东西,并且在板条箱中定义的程序宏不能在板条箱本身中使用。

// lib.rsextern板条箱proc_macro;使用proc_macro :: {TokenStream};使用quote :: {quote}; //使用proc_macro_attribute声明属性,例如程序宏#[proc_macro_attribute] // _metadata是提供给宏调用和_input的参数是代码,其属性如宏Attachespub fn my_custom_attribute(_metadata:TokenStream,_input:TokenStream)-> TokenStream {//为Struct TokenStream :: from(quote!{struct H {}})}重现一个简单的TokenStream

要测试我们添加的宏,请通过创建一个名为tests的文件夹并在该文件夹中添加文件attribute_macro.rs来创建一个证书测试。在此文件中,我们可以使用类似属性的宏进行测试。

// // tests / attribute_macro.rsuse macro_demo :: *; //宏将struct S转换为struct H#[my_custom_attribute] struct S {}#[test] fn test_macro(){// demo = H {};}

现在,我们了解了程序宏的基础,让我们使用syn进行一些高级TokenStream操作和解析。

要了解syn如何用于解析和操作,让我们以syn GitHub存储库中的示例为例。本示例创建一个Rust宏,该宏在值更改时跟踪变量。

trace_vars宏使用它需要跟踪的变量的名称,并在每次输入变量的值(即a更改)插入一条print语句。它跟踪输入变量的值。

首先,解析类似于属性的宏所附加的代码。 syn提供了用于Rust函数语法的内置解析器。如果语法无效,ItemFn将解析该函数​​并引发错误。

#[proc_macro_attribute] pub fn trace_vars(_metadata:TokenStream,输入:TokenStream)-> TokenStream {//将rust函数解析为易于使用的结构let input_fn = parse_macro_input!(输入为ItemFn); TokenStream :: from(quote!{fn dummy(){}})}

现在我们有了已解析的输入,让我们转到元数据。对于元数据,没有内置的解析器将起作用,因此我们必须使用syn’s parse模块自己编写一个。

为了使syn正常工作,我们需要实现syn提供的Parse特性。标点符号用于创建由,分隔的缩进向量。

struct Args {vars:HashSet< Ident>}}用于Args的impl解析{fn parse(input:ParseStream)->结果< Self> {//解析a,b,c或a,b,c,其中a,b和c为Indent let vars =标点符号::< Ident,Token![,]> :: parse_terminated(input)?; Ok(Args {vars:vars.into_iter()。collect(),})}}

#[proc_macro_attribute] pub fn trace_vars(元数据:TokenStream,输入:TokenStream)-> TokenStream {let input_fn = parse_macro_input!(input as ItemFn); //使用新创建的struct Args let args = parse_macro_input!(metadata as Args); TokenStream :: from(quote!{fn dummy(){}})}

现在,我们将修改input_fn以添加println!当变量更改值时。要添加此内容,我们需要过滤具有赋值的轮廓,并在该行之后插入打印语句。

impl Args {fn should_print_expr(& self,e:& Expr)-> bool {match * e {Expr :: Path(ref e)=> {//变量不应该从::开始,如果e.path.leading_colon.is_some(){false //应该是一个像`x = 8`而不是n :: x = 0}的变量,否则为e。 path.segments.len()!= 1 {false} else {//得到第一部分let first = e.path.segments.first()。unwrap(); //检查变量名是否在Args中。 vars hashset self.vars.contains(& first.ident)&& first.arguments.is_empty()}} _ => false,}} //用于检查是否要打印让i = 0等等fn should_print_pat(&self; p:& Pat)-> bool {match p {//检查变量名是否存在于集合Pat :: Ident(ref p)=>中。 self.vars.contains(& p.ident),_ => false,}} ///操纵树以插入打印语句fn Assign_and_print(& mut self,左:Expr,op:& dyn ToTokens,右:Expr)-> Expr {//赋权语句右边的递归调用let right = fold :: fold_expr(self,right); //返回操纵的子树parse_quote!({#left #op #right; println!(concat!(stringify !(#left)," = {:?}"),#left);})} //操作let语句fn let_and_print(& mut self,local:Local)-> Stmt {让本地{pat,init,..} =本地; let init = self.fold_expr(* init.unwrap()。1); //获取分配变量的变量名称let ident = match pat {Pat :: Ident(ref p)=> & p.ident,_ => (),}; //新的子树parse_quote! {let #pat = {#[allow(unused_mut)] let #pat = #init; println!(concat!(stringify!(#ident)," = {:?}"),#ident); #ident}; }}}

在上面的示例中,quote宏用于模板化和编写Rust。 #用于注入变量的值。

现在,我们将在input_fn上执行DFS并插入print语句。 syn提供了Fold特质,可以在任何项目上为DFS实施该特质。我们只需要修改与我们要操作的标记类型相对应的特征方法。

对Args进行impl折叠{fn fold_expr(& mut self,e:Expr)-> Expr {match e {//用于更改赋值,例如a = 5 Expr :: Assign(e)=> {//检查是否应该打印self.should_print_expr(& e.left){self.assign_and_print(* e.left,& e.eq_token,* e.right)}否则{//使用默认方法继续默认穿越Expr :: Assign(fold :: fold_expr_assign(self,e))}} //用于更改分配和操作,例如a + = 1 Expr :: AssignOp(e)=> {//检查是否应该打印self.should_print_expr(& e.left){self.assign_and_print(* e.left,& e.op,* e.right)}否则{//继续使用默认行为Expr :: AssignOp(fold :: fold_expr_assign_op(self,e))}} //其余表达式继续默认行为_ => fold :: fold_expr(self,e),}} // for let语句,例如let d = 9 fn fold_stmt(& mut self,s:Stmt)-> Stmt {match s {Stmt :: Local(s)=> {如果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),}}}

折叠特征用于对物料进行DFS。它使您可以对各种令牌类型使用不同的行为。

#[proc_macro_attribute] pub fn trace_var(args:TokenStream,输入:TokenStream)-> TokenStream {//解析输入let input = parse_macro_input!(input as ItemFn); //解析参数let mut args = parse_macro_input!(args as Args); //创建输出let output = args.fold_item_fn(input); //返回TokenStream TokenStream :: from(quote!(#output))}

此代码示例摘自syn示例存储库,这是学习程序宏的绝妙资源。

Rust中的自定义派生宏允许自动实现特征。这些宏使您可以使用#[derive(Trait)]实现特征。

要在Rust中编写一个自定义的派生宏,我们可以使用DeriveInput来解析要派生宏的输入。我们还将使用proc_macro_derive宏定义自定义衍生宏。

#[proc_macro_derive(Trait)] pub fn generate_trait(input:proc_macro :: TokenStream)-> proc_macro :: TokenStream {让输入= parse_macro_input!(输入为DeriveInput);让名称= input.ident;让扩展=报价! {impl Trait for #name {fn print(& self)->使用ize {println!(" {}","你好,来自#name")}}}; proc_macro :: TokenStream :: from(展开)}

可以使用syn编写更高级的程序宏。从syn’s仓库中查看此示例。

类似函数的宏与声明性宏类似,因为它们是通过宏调用运算符来调用的!看起来像函数调用。它们对括号内的代码进行操作。

类似函数的宏不是在运行时执行,而是在编译时执行。它们可以在Rust代码的任何地方使用。类似函数的宏也需要TokenStream并返回TokenStream。

在本Rust宏教程中,我们介绍了Rust中的宏基础知识,定义的声明性和过程宏,并逐步介绍了如何使用各种语法和社区构建的包编写两种类型的宏。我们还概述了使用每种类型的Rust宏的优点。

调试Rust应用程序可能很困难,尤其是当用户遇到难以重现的问题时。如果您希望监视和跟踪Rust应用程序的性能,自动显示错误以及跟踪缓慢的网络请求和加载时间,请尝试LogRocket。 LogRocket就像Web应用程序的DVR,实际上记录了Rust应用程序中发生的所有事情。无需猜测为什么会发生问题,您可以汇总并报告问题发生时应用程序所处的状态。 LogRocket还监视您的应用程序的性能,报告诸如客户端CPU负载,客户端内存使用情况等指标。