新建一个 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
, Punct
和 Literal
都属于单个的 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 类型的 cratecd hellocargo 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 buildtt: 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-expandcargo 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}
从 TokenStrem
到 syn::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 { 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
, syn
和 quote
之前,建议先尝试用 Rust 标准库代码实现,这样可以更好的理解这几个库写宏的过程会强迫你对 Rust 语言的细节有更多的理解关于 proc-macro2https://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 版本,反之亦然。