嵌套宏的展开顺序 :[1]
对于嵌套宏而言,宏展开顺序是 由外及内 的。例如,假设有这样的代码:
macro bar($i: ident) { $i }
macro foo($i: ident) { $i }
foo!(bar!(baz));
rust 将会首先展开 foo!,之后才展开 bar!。
派生宏
派生宏要求位于单独的一个库中。创建派生宏的一般步骤为:
创建 crate:
crago new macro_derive
添加派生宏相关的依赖。修改 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"
撰写派生宏相关的代码。
下面是一个最简单的派生宏的示例:
#[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()
}
派生宏的名字为 QoS
获取语法结构
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 {
}
表明是一个过程宏。
属性。
词法结构。
和上面的代码对应,则 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!()
}
}
}
使用
[<>]
将其中的名字拼接在一起。
同样的,在 [<>]
中可以进行大小写转换。例如:
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();
}