• Rust 过程宏 proc-macro 是个啥


    定义一个 procedural macro

    新建一个 lib 类型的 crate:

    cargo new hello-macro --lib
    

    procedural macros 只能在 proc-macro 类型的 crate 内定义,所以需要修改 Cargo.toml:

    [lib]
    proc-macro = true
    

    删除 src/lib.rs 里的全部内容,然后定义第一个过程宏(procedural macro):

    use proc_macro::TokenStream;
    
    #[proc_macro]
    pub fn hello_proc(input: TokenStream) -> TokenStream {
        input
    }
    

    目前它的作用跟下面这个声明宏(declarative macro) 是等价的:

    #[macro_export]
    macro_rules! hello_macro {
        (
            $($tt: tt)*
        ) => {
            $($tt)*
        };
    }
    

    就是把所有传入的 token 全部都原样返回. TokenStream 相当于声明宏里的 $($tt: tt)*,
    一连串的 token(TokenTree)
    全部放到了一个 stream(其实内部就是个 Vec) 里

    pub enum TokenTree {
        Group(Group), // [...], {...}, (...)
        Ident(Ident), // 函数名, struct 名等
        Punct(Punct), // 各种符号: + - * / ; &
        Literal(Literal), // 各种字面值: 123 'a' "hello" 
    }
    

    其中 Ident, PunctLiteral 都属于单个的 token,
    Group 是被三种括号(() [] {})包裹起来的 tokens

    测试一下, 修改代码

    #[proc_macro]
    pub fn hello_proc(input: TokenStream) -> TokenStream {
        for tt in input.into_iter() {
            println!("tt: {:#?}", tt);
        }
    
        TokenStream::new()
    }
    

    然后

    cargo new hello # 新建 bin 类型的 crate
    cd hello
    cargo add --path ../hello-macro # 添加我们的过程宏依赖
    

    然后在 src/main.rs 里调用 hello_proc

    use hello_macro::hello_proc;
    
    fn main() {
        hello_proc! {
            let a=8;[1,2,] {1+2 "hello world"}
        }
    }
    

    build 一下

    cargo build
    tt: Ident {
        ident: "let",
        span: #0 bytes(514..517),
    }
    tt: Ident {
        ident: "a",
        span: #0 bytes(518..519),
    }
    tt: Punct {
        ch: '=',
        spacing: Alone,
        span: #0 bytes(519..520),
    }
    tt: Literal {
        kind: Integer,
        symbol: "8",
        suffix: None,
        span: #0 bytes(520..521),
    }
    tt: Punct {
        ch: ';',
        spacing: Alone,
        span: #0 bytes(521..522),
    }
    tt: Group {
        delimiter: Bracket,
        stream: TokenStream [
            Literal {
                kind: Integer,
                symbol: "1",
                suffix: None,
                span: #0 bytes(523..524),
            },
            Punct {
                ch: ',',
                spacing: Alone,
                span: #0 bytes(524..525),
            },
            Literal {
                kind: Integer,
                symbol: "2",
                suffix: None,
                span: #0 bytes(525..526),
            },
            Punct {
                ch: ',',
                spacing: Alone,
                span: #0 bytes(526..527),
            },
        ],
        span: #0 bytes(522..528),
    }
    tt: Group {
        delimiter: Brace,
        stream: TokenStream [
            Literal {
                kind: Integer,
                symbol: "1",
                suffix: None,
                span: #0 bytes(530..531),
            },
            Punct {
                ch: '+',
                spacing: Alone,
                span: #0 bytes(531..532),
            },
            Literal {
                kind: Integer,
                symbol: "2",
                suffix: None,
                span: #0 bytes(532..533),
            },
            Literal {
                kind: Str,
                symbol: "hello world",
                suffix: None,
                span: #0 bytes(534..547),
            },
        ],
        span: #0 bytes(529..548),
    }
    

    能干啥

    过程宏的入参是一连串的 tokens, 这些都是编译器在进行语法分析之前的 tokens, 而且我们可以在过程宏的函数里执行复杂的逻辑, 且是在编译期执行, 因此我们可以对这些 tokens 做任何事情, 比如定义一套新的语法,解析其它语言等等

    甚至我可以在过程宏函数内执行一些毫不相干的代码,比如挖矿。这是一些恶意的过程宏可能会做的事情

    Builder Pattern

    先看需求:

    derive_struct! {
        struct Foo {}
    }
    
    // derive_struct 展开后变成下面的代码
    struct Foo {}
    struct FooBuilder{}
    

    分析一下, 我们需要给传入的 struct 加一个 Builder. 如果用「声明式宏」来做, 怎样才能把一个 ident(Foo) 变成
    另一个 ident(FooBuilder) 呢? 好像没有办法(如果你知道的话, 请一定告诉我). 那么我们用过程宏呢, 我们可以取得 ident(Foo),
    也可以定义新的 ident(FooBuilder), 理论上完全 OK.

    来,让我们在不借助第三方库的情况下试一下

    #[proc_macro]
    pub fn derive_struct(mut input: TokenStream) -> TokenStream {
       let mut iter = input.clone().into_iter();
    
       assert_eq!(iter.next().unwrap().to_string().as_str(), "struct");
    
       let Some(proc_macro::TokenTree::Ident(ident)) = iter.next() else {
           panic!("parse struct identifier error");
       };
    
       let builder: TokenStream = format!(
               "struct {}{} {}", 
               ident, "Builder", "{}"
           )
           .parse().unwrap();
    
       input.extend(builder.into_iter());
    
       input
    }
    

    测试代码 main.rs

    use hello_macro::derive_struct;
    
    derive_struct! {
        struct Foo {
            a: u8,
        }
    }
    
    fn main() {}
    

    查看展开后的代码

    # 安装 cargo-expand
    # cargo install cargo-expand
    cargo expand
    

    展开后的代码:

    struct Foo {
        a: u8,
    }
    struct FooBuilder {}
    

    我们目前只解析了最简单形式的 struct, 如果要再复杂一些, 比如带泛型和 meta data, 那么解析起来就会麻烦很多。
    幸运的是我们可以借助 syn 来代替我们手动 parse,
    这篇文章 中所有 Metavariables 都能用 syn 来解析,
    我们现在需要解析出 ItemStruct 就够了

    在 hello-macro 目录下添加依赖:

    cargo add syn --features full # syn::Item 需要 full feature
    

    然后修改 derive_struct:

    #[proc_macro]
    pub fn derive_struct(mut input: TokenStream) -> TokenStream {
        let item_struct: syn::ItemStruct = syn::parse(input.clone()).unwrap();
    
        let ident = item_struct.ident;
    
        let builder: TokenStream = format!(
                "struct {}{} {}", 
                ident, "Builder", "{}"
            )
            .parse().unwrap();
    
        input.extend(builder.into_iter());
    
        input
    }
    

    TokenStremsyn::Item 简单了,那反方向解析有没有方便使用的 crate 呢?
    有, quote

    添加依赖

    cargo add quote
    

    修改我们的 derive_struct:

    #[proc_macro]
    pub fn derive_struct(input: TokenStream) -> TokenStream {
        let item_struct: syn::ItemStruct = syn::parse(input.clone()).unwrap();
    
        let vis = &item_struct.vis;
        let ident = quote::format_ident!("{}Builder", item_struct.ident);
        let generics = &item_struct.generics;
    
        quote! {
            #item_struct
    
            #vis struct #ident #generics {}
        }
        .into()
    }
    

    quote::quote 是一个「声明式宏」, 它的内部其实是将 (# $var:ident) 替换为 var.to_tokens()(需要 var 的类型实现 ToTokens trait),
    #(#var)* 的用法也跟声明式宏类似

    继续改进:

    #[proc_macro]
    pub fn derive_struct(input: TokenStream) -> TokenStream {
        let mut item_struct: syn::ItemStruct = syn::parse(input.clone()).unwrap();
    
        let attr: syn::Attribute = syn::parse_quote! {
            #[derive(Default)]
        };
    
        if item_struct.attrs.iter().all(|x| {
            x.to_token_stream().to_string() != attr.to_token_stream().to_string()
        }) {
            item_struct.attrs.push(attr);
        }
    
        item_struct.generics.make_where_clause();
    
        let vis = &item_struct.vis;
        let generics = &item_struct.generics; // 
        let generic_where_clause = &generics.where_clause;
    
        let mut generic_params = generics.params.clone();
        generic_params = generic_params.into_iter().filter_map(|mut v| {
            match &mut v {
                syn::GenericParam::Lifetime(_) => None,
                syn::GenericParam::Type(ty) => {
                    ty.bounds.clear();
                    ty.attrs.clear();
                    Some(v)
                },
                syn::GenericParam::Const(c) => {
                    let ident = c.ident.clone();
                    Some(syn::parse_quote! {
                        #ident
                    })
                },
            }
        }).collect();
    
        // println!("generics: {}", generics.to_token_stream());
        // println!("generic_params: {}", generic_params.to_token_stream());
        // println!("generic_where_clause: {}", generic_where_clause.to_token_stream());
    
        let ident = &item_struct.ident;
        let builder_ident = quote::format_ident!("{}Builder", item_struct.ident);
        let fields = &item_struct.fields;
    
        let syn::Fields::Named(_) = fields else {
            panic!("struct with unnamed fields like `struct Foo(String);` is not supported.");
        };
    
        let field_ident: Vec = fields.iter().map(|f|f.ident.clone().unwrap()).collect();
        let field_ty: Vec = fields.iter().map(|f|f.ty.clone()).collect();
    
        quote! {
            #item_struct
    
            impl #generics #ident <#generic_params> {
                pub fn builder() -> #builder_ident <#generic_params>{
                    Default::default()
                }
            }
    
            #[derive(Default)]
            #vis struct #builder_ident #generics {
                inner: #ident <#generic_params>,
            }
    
            impl #generics #builder_ident <#generic_params> {
                pub fn build(self) -> #ident <#generic_params> {
                    self.inner
                }
                #(
                    pub fn #field_ident(mut self, #field_ident: #field_ty) -> Self {
                        self.inner.#field_ident = #field_ident;
                        self
                    }
                )*
            }
        }
        .into()
    }
    

    目前的 derive_struct 已经可以支持下面这种 struct 了

    derive_struct! {
        #[derive(Debug)]
        pub struct Bar<const N: usize, T: Default> {
            a: u8,
            b: String,
            c: T,
        }
    }
    

    派生宏

    我们前面定义的过程宏 derive_struct 中文名叫「函数式宏」, 在这个场景下虽然能用, 但是每次都要把整个 struct 包裹起来,还是很麻烦的。这时 proc_macro_derive(中文叫「派生宏」) 就该出场了,
    定义一个名为 Builder 的派生宏:

    // attributes 可以加到 fields 上, 如果不需要可以不要这个 attributes
    #[proc_macro_derive(Builder, attributes(attr1, attr2,))]
    pub fn my_builder(input: TokenStream) -> TokenStream {
        let input: syn::DeriveInput = syn::parse(input).unwrap();
        let syn::Data::Struct(data) = input.data else {
            panic!("Sorry, we only support struct.");
        };
    
        let vis = input.vis;
        let generics = input.generics;
        let builder_ident = quote::format_ident!("{}Builder", input.ident);
    
        // input.attrs;
        // data.fields;
    
        quote! {
            #vis struct #builder_ident #generics {}
        }
        .into()
    }
    

    proc_macro_derive 是专门用来处理 derive 类型的过程宏的, 函数名可以随意, input 参数是跟宏相关联的某个 item, 在这里它总是 enum, struct 或 union 其中的一种, 因为只有这
    三种 item 可以标注 derive 属性。函数返回值会被追加到 item 后面(「函数式宏」会完全替换掉原来的 TokenStream)

    #[derive(Debug, Builder)]
    struct Foo {
        a: u32,
        #[attr1]
        b: String,
        #[attr2(hello = world)]
        c: (u32, u32),
    }
    // struct FooBuilder {} // 会被追加到这里
    

    属性宏

    「属性宏」的返回值也是会完全替换掉输入的 item

    #[proc_macro_attribute]
    pub fn hello_attr(attr: TokenStream, item: TokenStream) -> TokenStream {
        // println!("hello_attr attr: {}, item: {}", attr, item);
        item
    }
    
    #[hello_attr(hello world)]
    fn foo() {}
    

    总结

    • 「过程宏」是比「声明式宏」能力更强的一种宏,可以在编译期执行复杂逻辑
    • 熟练写「声明式宏」对理解「过程宏」很有帮助,建议学习「过程宏」之前先学习好「声明式宏」
    • 写宏的时候多多参阅 The Rust Reference, 可以更深入地理解 Rust 语言
    • 在学习过程中,使用 proc-macro2, synquote 之前,建议先尝试用 Rust 标准库代码实现,这样可以更好的理解这几个库
    • 写宏的过程会强迫你对 Rust 语言的细节有更多的理解

    关于 proc-macro2

    https://crates.io/crates/proc-macro2

    https://veykril.github.io/tlborm/proc-macros/third-party-crates.html

    由于 proc_macro crate 是专门为 proc_macro 类型 crate 设计的,因此使它们可进行单元测试或从非 proc_macro 代码中访问它们几乎是不可能的。鉴于此,proc-macro2 crate 模仿了原始 proc_macro crate 的 API,在 proc_macro crates 中充当包装器,在非 proc_macro crates 中则可独立使用。因此,建议针对 proc_macro 代码构建库时,使用 proc-macro2 来进行构建,这将使这些库可进行单元测试,这也是为什么下面列出的 crate 取出和发射 proc-macro2::TokenStreams 的原因。当需要 proc_macro token stream 时,可以简单地将 proc-macro2 token stream 转换为 proc_macro 版本,反之亦然。

  • 相关阅读:
    typescript开发环境搭建
    Linux安装MySQL5
    Linux_应用篇(08) 信号-基础
    基于session推荐的论文阅读
    Learn Git Branching:在游戏中学会Git
    基于ssm共享充电宝管理系统(java毕业设计)
    Android 11.0 默认授予app获取序列号SerialNo权限
    [Qt][C++]static与extern关键字
    完美解决-RuntimeError: CUDA error: device-side assert triggered
    采用arm-none-eabi-gcc交叉编译工具链 以及使用xmake构建 STM32 RT-Thread nano工程 笔记
  • 原文地址:https://www.cnblogs.com/hangj/p/17503684.html