嵌套宏的展开顺序 :[1]

对于嵌套宏而言,宏展开顺序是 由外及内 的。例如,假设有这样的代码:

macro bar($i: ident) { $i }
macro foo($i: ident) { $i }

foo!(bar!(baz));

rust 将会首先展开 foo!,之后才展开 bar!。

派生宏

派生宏要求位于单独的一个库中。创建派生宏的一般步骤为:

  1. 创建 crate:

    crago new macro_derive
  2. 添加派生宏相关的依赖。修改 Cargo.toml,将其改为下述内容:

    [package]
    edition = "2021"
    name = "macro_derive"
    version = "0.1.0"
    
    [lib]
    proc-macro = true
    
    [dependencies]
    quote = "1.0.26"
    syn = "2.0.15"
  3. 撰写派生宏相关的代码。

下面是一个最简单的派生宏的示例:

#[proc_macro_derive(QoS)] (1)
pub fn qos_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap(); (2)
    let name = &ast.ident;

    let res = quote! { (3)
        impl QoS for #name {};
    };

    res.into()
}
  1. 派生宏的名字为 QoS

  2. 获取语法结构

  3. quote! 中的内容将会被拼接到源码中。

语法结构

对于派生宏而言,能够看到的语法结构如下:

// vis,可视范围             ident,标识符     generic,范型    fields: 结构体的字段
pub              struct    User            <'a, T>          {

// vis   ident   type
   pub   name:   &'a T,
}

DeriveInput

syn::parse 返回的是一个 DeriveInput 结构体。其结构体如下:

DeriveInput {
    // --snip--
    vis: Visibility,
    generics: Generics
    ident: Ident {
        ident: "Sunfei",
        span: #0 bytes(95..103)
    },
    // Data是一个枚举,分别是DataStruct,DataEnum,DataUnion,这里以 DataStruct 为例
    data: Data(
        DataStruct {
            struct_token: Struct,
            fields: Fields,
            semi_token: Some(
                Semi
            )
        }
    )
}

quote!

quote! 宏将其中的内容视为纯文本。并将其拼接到源码的后面。quote! 中可以使用 # 来拼接多个 quote! 或者名字。例如:

let mut field_ast = quote! {};

if let Data::Struct(data) = &ast.data {
    for (_, f) in data.fields.iter().enumerate() {
        if let Some(field_id) = &f.ident {
            field_ast.extend(quote! {
                if let Some(v) = self.#field_id {
                    v.validate();
                }
            });
        }
    }
} else {
    return field_ast.into();
}

let res = quote! {
    impl Entity for #name {
        fn validate(&self){
            #field_ast
        }
    };
};

类属性宏

类属性宏。读作 Attribute-like macros。可以用于给类或者函数 添加属性 。一个类属性宏类似于:

#[route(GET, "/")]
fn index() {
}

一个类属性宏的结构为:

#[proc_macro_attribute] (1)
pub fn validate ( (2)
    attr: TokenStream, (3)
    input: TokenStream (4)
) -> TokenStream {
}
  1. 表明是一个过程宏。

  2. 属性。

  3. 词法结构。

和上面的代码对应,则 attr 的内容为 GET, "/" 。也就是说属性只是一个简单的字符串。具体如何解析需要自己处理。

最简单的一个类属性宏为:

#[proc_macro_attribute]
pub fn validate(attr: TokenStream, input: TokenStream) -> TokenStream {
    if attr.is_empty() {
        return TokenStream::new();
    }
    let ast: DeriveInput = syn::parse(input).unwrap();
    let id = &ast.ident;
    let syntax = attr.to_string();

    let res = quote! {
        impl Entity for #id {
            fn validate(&mut self){
                assert!(#syntax);
            }
        };
    };

    res.into()
}
类属性宏会使用宏输出结果替换原本的内容。因此即使是解析过程中发现了错误也需要把原本的内容返回。

类函数宏

类函数的调用方式和普通的函数调用方式相同。但是类函数宏有两种实现方式:

基于 proc_macro 的类函数宏

基于 proc_macro 的类函数宏和上面的宏类似。基本结构如下:

#[proc_macro]
pub fn jsonized_from_file(input: TokenStream) -> TokenStream {
}

macro_rules!

paste

paste 使得能够进行名字拼接和简单的大小写转换。使用下面的方式可以进行代码拼接:

macro_rules! paste {
    ($type:ident) => {
        paste!{
            fn [<create_ $type>](){ (1)
                todo!()
            }
        }
    }
  1. 使用 [<>] 将其中的名字拼接在一起。

同样的,在 [<>] 中可以进行大小写转换。例如:

macro_rules! paste {
    ($type:ident) => {
        paste!{
            fn [<create_ $type:lower>](){
                todo!()
            }
        }
    }

过程宏的解析

对于简单的过程宏而言,一般 parse_macro_input 和 syn 自带的结构进行组合即可。但是对于比较复杂的宏,亦或是 rust 原本语法结构无法解析的内容,最佳的解析方式是为自定义结构实现 syn::parse::Parse trait。

一个简单的结构体实现如下:

use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{parse::Parse, Expr, ExprAssign, LitBool};

#[derive(Debug)]
pub struct Editable(pub LitBool);

impl Parse for Editable {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let expr: ExprAssign = input.parse()?;
        let rhs = match expr.right.as_ref() {
            Expr::Lit(lit) => match &lit.lit {
                syn::Lit::Bool(lit) => lit.clone(),
                var => Err(syn::Error::new(
                    var.span(),
                    format!("expect a bool, but it's {}", var.into_token_stream()),
                ))?,
            },
            var => Err(syn::Error::new(
                var.span(),
                format!("expect a bool, but it's {}", var.into_token_stream()),
            ))?,
        };

        Ok(Self(rhs))
    }
}

实现了 Parse trait 的结构体可以同 parse_macro_input 一起使用。

获取文件路径

在过程宏中获取调用宏的文件的路径方式如下:

#![feature(proc_macro_span)]

fn main() {
    Span::call_site()
        .source_file()
        .path();
}
Last moify: 2025-04-07 07:03:40
Build time:2025-07-18 09:41:42
Powered By asphinx