定义一个 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. [Java/LeetCode]算法练习:二进制间距(868/simple)

    1 题目描述 题目来源: https://leetcode-cn.com/problems/binary-gap/ 给定一个正整数 n,找到并返回 n 的二进制表示中两个 相邻 1 之间的 最长距离 ...

  2. Python程序笔记20230304

    抛硬币实验 random 模块 import random random.randint(a, b) 返回一个随机整数 N,范围是:a <= N <= b random.choice(&q ...

  3. String知识点整理

    使用双引号创建字符串时,JVM会现在字符串常量池中查找是否已存在该字符串,存在则返回,不存在则在池中创建后再返回.与此同时,使用String的intern方法也是类似处理. 使用new String的 ...

  4. CesiumJS 源码杂谈 - 从光到 Uniform

    目录 1. 有什么光 2. 光如何转换成 Uniform 以及何时被调用 2.1. 统一值状态对象(UniformState) 2.2. 上下文(Context)执行 DrawCommand 2.3. ...

  5. c/c++零基础坐牢第三天

    c/c++从入门到入土(3) 开始时间2023-04-17 19:07:20 结束时间2023-04-17 20:53:40 前言:经过三天的算法训练,大家肯定对后面的编程知识产生浓厚的兴趣,有了前两 ...

  6. Abp框架Web站点的安全性提升

    本文将从GB/T 28448-2019<信息安全技术 网络安全等级保护测评要求>规定的安全计算环境中解读.摘要若干安全要求,结合Abp框架,对站点进行安全升级. [身份鉴别]应对登录的用户 ...

  7. MAPPO学习笔记(1):从PPO算法开始

    由于这段时间的学习内容涉及到MAPPO算法,并且我对MAPPO算法这种多智能体算法的信息交互机制不甚了解,于是写了这个系列的笔记,目的是巩固知识,并且进行一些粗浅又滑稽的总结. 1.PPO算法的介绍 ...

  8. 驱动开发:通过MDL映射实现多次通信

    在前几篇文章中LyShark通过多种方式实现了驱动程序与应用层之间的通信,这其中就包括了通过运用SystemBuf缓冲区通信,运用ReadFile读写通信,运用PIPE管道通信,以及运用ASYNC反向 ...

  9. UML类图——类之间的关系

    关联关系(实线箭头) 是一种结构化关系,表示一类对象与另一类对象之间有联系.Java,c++,c#等编程语言在实现关联关系时,通常将一个类的对象作为另一个类的属性 - 双向关联 - 单向关联 - 自关 ...

  10. 【Python基础】集合的基本使用

    Python中的集合是一种无序且唯一的数据结构.集合是通过花括号{}或者set()函数来创建的. 创建集合 s = set() 声明空集合 s = {1,2,3,4,5} 声明非空集合 添加元素 s. ...