定义一个 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. 最简单Openwrt ipv6配置,局域网WAN6中继模式获取原生ipv6地址

    条件 condition wan 和 wan6 是默认配置     Wan and wan6 are the default configurations 同时wan6可以获取到原生IPv6     ...

  2. [Java EE]缓存技术初探

    1 背景 使用场景:计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存. 高并发下,为提高 频繁 查询 大量 可能常用的 数据库数据的 查询效率. 大部分情况下, ...

  3. 【AIGC未来的发展方向】面向人工智能的第一步,一文告诉你人工智能是什么以及未来的方向分析

    人工智能的概念 当人们提到"人工智能(AI)"时,很多人会想到机器人和未来世界的科幻场景,但AI的应用远远不止于此.现在,AI已经广泛应用于各种行业和生活领域,为我们带来了无限可能 ...

  4. elasticsearch 官方优化建议

    1.一般建议   a.不要返回过大的结果集.这个建议对一般数据库都是适用的,如果要获取大量结果,可以使用search_after api,或者scroll (新版本中已经不推荐).   b.避免大的文 ...

  5. mysql迁移:docker迁入迁出mysql

    docker迁出mysql数据库 测试环境: docker服务器 mysql服务器 IP 192.168.163.19 192.168.163.16 操作系统 CentOS7.8 CentOS7.8 ...

  6. 使用Python代码远程连接服务器

    目录 一.paramiko模块的介绍 二.基本使用(用户名密码登录) 三.用公钥私钥连接 一.paramiko模块的介绍 模块介绍 使用Python的第三方模块paramiko实现远程连接服务器 功能 ...

  7. 五天学会Deep Learning

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

  8. React 监听页面滚动,界面动态显示

    以下是常见的监听滚动以及相应的操作 窗口滚动事件 当页面滚动时,如何动态切换布局/样式 1. 添加滚动事件的监听/注销 1 //在componentDidMount,进行scroll事件的注册,绑定一 ...

  9. Python变量的数据类型

    主要内容 jupyter notebook的用法 变量 跟vi/vim的编辑模式很像 # 声明的三种格式 # 格式1 s1 = "我爱王晓静" # 格式2 s2 = s1 = &q ...

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

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