定义一个 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<TokenTree>) 里

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; // <T: Default>
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<syn::Ident> = fields.iter().map(|f|f.ident.clone().unwrap()).collect();
let field_ty: Vec<syn::Type> = 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 版本,反之亦然。

Rust 过程宏 proc-macro 是个啥的更多相关文章

  1. Rust中的宏:声明宏和过程宏

    Rust中的声明宏和过程宏 宏是Rust语言中的一个重要特性,它允许开发人员编写可重用的代码,以便在编译时扩展和生成新的代码.宏可以帮助开发人员减少重复代码,并提高代码的可读性和可维护性.Rust中有 ...

  2. Rust 1.7.0 macro宏的复用 #[macro_use]的使用方法

    Rust 1.7.0 中的宏使用范围包含三种情况: 第一种情况是宏定义在当前文件里.这个文件可能是 crate 默认的 module,也可能是随意的 module 模块. 另外一种情况是宏定义在当前 ...

  3. 错误注入 异常行为 环境变量或代码动态激活来触发这些异常行为 模拟错误 容错性 正确性 稳定性 宏 本质 macro

    小结: 1. 微服务中某个服务出现随机延迟.某个服务不可用. 存储系统磁盘 IO 延迟增加.IO 吞吐量过低.落盘时间长. 调度系统中出现热点,某个调度指令失败. 充值系统中模拟第三方重复请求充值成功 ...

  4. zabbix宏(macro)使用:自定义监控阈值

    一.简单应用场景 zabbix在监控cpu load时并没有考虑客户端cpu的个数和核心数量,当平均5分钟的负载达到5时zabbix执行报警动作,这样是非常不合理的,笔者的被监控机器有四核和单核,现在 ...

  5. Hive笔记之宏(macro)

    一.啥是宏 宏可以看做是一个简短的函数,或者是对一个表达式取别名,同时可以将这个表达式中的一些值做成变量调用时传入,比较适合于做分析时为一些临时需要用到很多次的表达式操作封装一下取个简短点的别名来调用 ...

  6. MAKEWORD 宏(macro)

    先看看Microsoft给出的关于MAKEWORD的参考: 从Microsoft给出的参考可以得知,宏MAKEWORD的作用是用于创建一个由bHigh和bLow组成的WORD类型的值. 其中bLow是 ...

  7. SAS 获取系统选项设置的过程步 PROC OPTIONS OPTION=()

    PROC OPTIONS OPTION=(VALIDVARNAME LS);RUN;

  8. tcl之过程/函数-proc

  9. 【译】Rust宏:教程与示例(一)

    原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...

  10. [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro]

    [易学易懂系列|rustlang语言|零基础|快速入门|(22)|宏Macro] 实用知识 宏Macro 我们今天来讲讲Rust中强大的宏Macro. Rust的宏macro是实现元编程的强大工具. ...

随机推荐

  1. JsonCpp JSON格式处理库的介绍和使用(面向业务编程-文件格式处理)

    JsonCpp JSON格式处理库的介绍和使用(面向业务编程-文件格式处理) 介绍 JSON是一种轻量级的数据交换格式,它是一种键值对的集合.它的值可以是数字.字符串.布尔值.序列. 想知道更多有关J ...

  2. Distinctive Image Features from Scale-Invariant Keypoints 论文解读

    Distinctive Image Features from Scale-Invariant Keypoints 论文解读 著名的SIFT local feature提取方法 Scale-space ...

  3. shell读取配置文件-sed命令

    在编写启动脚本时,涉及到读取配置文件,特地记录下shell脚本读取启动文件的方式.主要提供两种格式的读取方式,方式一配置文件采用"[]"进行分区,方式二配置文件中需要有唯一的配置项 ...

  4. 深度学习--PyTorch定义Tensor以及索引和切片

    深度学习--PyTorch定义Tensor 一.创建Tensor 1.1未初始化的方法 ​ 这些方法只是开辟了空间,所附的初始值(非常大,非常小,0),后面还需要我们进行数据的存入. torch.em ...

  5. 天梯赛L1-027 出租

    一.问题描述 下面是新浪微博上曾经很火的一张图: 一时间网上一片求救声,急问这个怎么破.其实这段代码很简单,index数组就是arr数组的下标,index[0]=2 对应 arr[2]=1,index ...

  6. 五天学会Deep Learning

    五天学完deep learning......是时候来证明chatGPT和new bing的能力了...... DAY1 Sigmoid function Sigmoid 函数是一种常用的激活函数,它 ...

  7. AI降临,前端启用面壁计划

    作者:京东零售 郑炳懿 开篇: "在我们有生之年,你觉得会看到AI兵临城下的那一天吗?就像电影黑客帝国里面演的一样",Barry从红色的烟盒里取出一根烟发问道. "不可能 ...

  8. node使用react项目启动错误TSError: ⨯ Unable to compile TypeScript:

    1.错误内容 return new TSError(diagnosticText, diagnosticCodes) ^ TSError: ⨯ Unable to compile TypeScript ...

  9. Apache hudi 核心功能点分析

    Hudi 文中部分代码对应 0.14.0 版本 发展背景 初始的需求是Uber公司会有很多记录级别的更新场景,Hudi 在Uber 内部主要的一个场景,就是乘客打车下单和司机接单的匹配,乘客和司机分别 ...

  10. 【Azure 存储服务】Java Storage SDK 调用 uploadWithResponse 代码示例(询问ChatGTP得代码原型后人力验证)

    问题描述 查看Java Storage SDK,想找一个 uploadWithResponse  的示例代码,但是通过全网搜索,结果没有任何有帮助的代码.使用最近ChatGPT来寻求答案,得到非常有格 ...