# 前言
过程宏,它更像函数,接受一些代码作为参数输入,然后对他们进行加工,生成新的代码,他不是在做声明式宏那样的模式匹配。三种过程式宏都是这种思路。
过程宏分为三种:
- 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
- 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的 #[inline]、#[derive (...)] 等都是属性宏。
- 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。
不能在原始的 crate 中直接写过程式宏,需要把过程式宏放到一个单独的 crate 中(以后可能会消除这种约定)。定义过程式宏的方法如下:
use proc_macro; | |
#[some_attribute] | |
pub fn some_name(input: TokenStream) -> TokenStream { | |
} |
需要引入 proc_macro
这个 crate,然后标签是用来声明它是哪种过程式宏的,接着就是一个函数定义,函数接受 TokenStream
,返回 TokenStream
。 TokenStream
类型就定义在 proc_macro
包中,表示 token 序列。除了标准库中的这个包,还可以使用 proc_macro2
包,使用 proc_macro2::TokenStream::from()
和 proc_macro::TokenStream::from()
可以很便捷地在两个包的类型间进行转换。使用 proc_macro2
的好处是可以在过程宏外部使用 proc_macro2
的类型,相反 proc_macro
中的类型只可以在过程宏的上下文中使用。且 proc_macro2
写出的宏更容易编写测试代码。
下面详细说明如何定义三类过程宏。
# Custom Derive 宏
在本节中,我们的目的是实现下面的代码,使用编译器为我们生成名为 HelloMacro
的 Trait
use hello_macro::HelloMacro; | |
use hello_macro_derive::HelloMacro; | |
#[derive(HelloMacro)] | |
struct Pancakes; | |
fn main() { | |
Pancakes::hello_macro(); | |
} |
该 Trait
的定义如下,目的是打印实现该宏的类型名
pub trait HelloMacro { | |
fn hello_macro(); | |
} |
由于过程宏不能在原 crate 中实现,我们需要如下在 hello_crate
的目录下新建一个 hello_macro_derive
crate
cargo new hello_macro_derive --lib |
在新的 crate 内,我们需要修改 Cargo.toml
配置文件,
[lib] | |
proc-macro = true | |
[dependencies] | |
syn = "1.0" | |
quote = "1.0" |
在 src/lib.rs
中可以着手实现该宏,其中 syn
是用来解析 rust 代码的,而 quote 则可以用已有的变量生成代码的 TokenStream
,可以认为 quote!
宏内的就是我们想要生成的代码
extern crate proc_macro; | |
use proc_macro::TokenStream; | |
use quote::quote; | |
use syn; | |
#[proc_macro_derive(HelloMacro)] | |
pub fn hello_macro_derive(input: TokenStream) -> TokenStream { | |
// Construct a representation of Rust code as a syntax tree | |
// that we can manipulate | |
let ast = syn::parse(input).unwrap(); | |
// Build the trait implementation | |
impl_hello_macro(&ast) | |
} | |
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { | |
let name = &ast.ident; | |
let gen = quote! { | |
impl HelloMacro for #name { | |
fn hello_macro() { | |
println!("Hello, Macro! My name is {}!", stringify!(#name)); | |
} | |
} | |
}; | |
gen.into() | |
} |
另外,Custom Derive 宏可以携带 Attributes,称为 Derive macro helper attributes,具体编写方法可以参考 Reference(Rust 中共有四类 Attributes)。关于 Derive macro helper attributes 这里有一个坑就是在使用 cfg_attr
时,需要把 Attributes 放在宏之前。
举个例子:
使用 kube-rs 可以很方便地定义 CRD(Custom Resource Definition):
#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)] | |
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] | |
struct FooSpec { | |
info: String, | |
} |
我第一反应是 #[kube]
是一个 Attribute-Like 宏,但是查阅 kube-rs 文档才发现它其实是 CustomResource
Custom Derive 宏的 Attribute。这里我们想用 cfg_attr
来控制是否去做 derive,一开始就想当然地这么写了:
#[cfg_attr(feature="use_kube_rs", | |
derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema), | |
kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced) | |
)] | |
struct FooSpec { | |
info: String, | |
} |
然而这是错误的打开方式,需要写成:
#[cfg_attr(feature="use_kube_rs", | |
kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced), | |
derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema) | |
)] | |
struct FooSpec { | |
info: String, | |
} |
Attributes 需要写在宏的 derive 前面。
# Attribute-Like 宏
attribute-like 宏和 custom derive 宏很相似,只是标签可以自定义,更加灵活,甚至可以使用在函数上。他的使用方法如下,比如假设有一个宏为 route
的宏
#[route(GET, "/")] | |
fn index() { ... } |
按下面的语法定义 route
宏
#[proc_maco_attribute] | |
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... } |
其中 attr
参数是上面的 Get
, "/"
; item
参数是 fn index(){}
。
# Function-Like 宏
这种宏看上去和 macro_rules!
比较类似,但是在声明式宏只能用 match
去做模式匹配,但是在这里可以有更复杂的解析方式,所以可以写出来
let sql = sql!(SELECT * FROM posts WHERE id=1); |
上面这个 sql
宏的定义方法如下
#[proc_macro] | |
pub fn sql(input: TokenStream) -> TokenStream { ... } |
# 好用的库
proc_macro:默认 token 流库,只能在过程宏中使用,编译器要用它,将它作为过程宏的返回值,大多数情况我们不需要,只需要在宏返回结果的时候把 proc_macro2::TokenSteam
的流 into()
到 proc_macro::TokenSteam
就行了。
proc_macro2:我们真正在使用的过程宏库,可以在过程宏外使用。
syn:过程宏左护法,可以将 TokenStream
解析成语法树,注意两个 proc_macro
和 proc_macro
都支持,需要看文档搞清楚库函数到底是在解析哪个库中的 TokenStream
。
quote:过程宏右护法,将语法树解析成 TokenStream
。只要一个 quote!{}
就够了! quote!{}
宏内都是字面量,即纯纯的代码,要替换进去的变量是用的 #
符号标注,为了和声明宏中使用的 $
相区分(也就意味着用 quote
写过程宏的时候,可以和声明宏结合 🤤 )。模式匹配时用到的表示重复的符号和声明宏中一样,是使用 *
。
darling 好用的标签宏解析库。