研读Rust圣经解析——Rust learn-16(高级trait,宏)
- 高级trait
- 关联类型Type
- 为什么不用泛型而是Type
- 运算符重载(重要等级不高)
- 重名方法消除歧义
- never type
- continue 的值是 !
- 返回闭包
- 宏
- 自定义宏(声明宏)
- 宏的运作机制
- Rust编译过程
- 新建空白宏
- 宏选择器
- 什么是词条树
- 宏选择器设置各类入参
- 实现一个log宏
- 运行重复模式匹配
- 自定义derive宏(过程宏)
- 构建项目结构(一定要照着做不然会错)
- 设置工作空间
- 创建lib和main
- hello_macro
- lib.rs
- hello_macro_derive
- 添加依赖和激活`proc-macro`
- lib.rs
- 注意点(请好好读,官网上说的很清楚了,这个地方一定要搞懂)
- pancakes
- 添加依赖
- main.rs
- 错误
- can't find library `marco_t`, rename file to `src/lib.rs` or specify lib.path (为什么不能在单项目包里构建)
- can't use a procedural macro from the same crate that defines it
- 自定义类属性宏(个人认为最重要)
- 一个简单的例子
- 项目包结构
- json_marco
- 添加依赖
- 编写lib
- json_test
- 一些例子
- base
- lib
- main
- flaky_test
- lib
- main
- json_parse
- lib.rs
- main.rs
- fn_time
- lib
- main
高级trait
关联类型Type
我们使用type关键字即可声明一个关联类型,关联类型的作用就是简化和隐藏显示类型(个人认为)
- 简化:一个很长的类型总是被需要时,需要开发者耗费精力的重复书写,而且若有改动,则需要改多个地方
- 隐藏:对外部调用者隐藏,外部调用者无需知道它指的是什么,只要可快速使用即可
trait test {
type Res = Result<i32, Box<&'static str>>;
fn test_res() -> Res{
//...
}
}
为什么不用泛型而是Type
使用泛型,我们就需要在每次使用实现时显示的标注类型,但是当针对一个多处使用且无需修改类型的场景时,无疑耗时耗力,换而言之,Type牺牲部分灵活度换取常用性
运算符重载(重要等级不高)
Rust 并不允许创建自定义运算符或重载任意运算符,不过 std::ops 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载
因此我们就可以重载例如+,/,-,*
等,
以下是官方给出的例子:
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
重名方法消除歧义
当我们实现多个trait的时候,若遇到多个trait有同样的方法名,那么就会产生重名歧义,此时最晚实现的会覆盖前面的,为了消除歧义,我们可以采用trait::fn(&type)
来申明调用
struct a {}
trait b {
fn get(&self) {}
}
trait c {
fn get(&self) {}
}
impl b for a {
fn get(&self) {
todo!()
}
}
impl c for a {
fn get(&self) {
todo!()
}
}
fn main() {
let a_struct = a {};
b::get(&a_struct);
c::get(&a_struct);
}
never type
Rust 有一个叫做 ! 的特殊类型,我们称作never type因为他表示函数从不返回的时候充当返回值
fn no_feedback()->!{
//...
}
continue 的值是 !
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
返回闭包
一个函数的返回值是可以为一个闭包的,这个没有限制,具体来说我们简单了解写法即可
fn test()->Box<dyn Fn()>{
//...
}
我们通过返回一个Box即将返回值写入堆中
例如:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
宏
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming)。在附录 C 中会探讨 derive 属性,其生成各种 trait 的实现。我们也在本书中使用过 println! 宏和 vec! 宏。所有的这些宏以 展开 的方式来生成比你所手写出的更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数扮演的角色。但宏有一些函数所没有的附加能力。
一个函数签名必须声明函数参数个数和类型。相比之下,宏能够接收不同数量的参数:用一个参数调用 println!(“hello”) 或用两个参数调用 println!(“hello {}”, name) 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现。
实现宏不如实现函数的一面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在一个文件里调用宏 之前 必须定义它,或将其引入作用域,而函数则可以在任何地方定义和调用。
—https://kaisery.github.io/trpl-zh-cn/ch19-06-macros.html
自定义宏(声明宏)
接下来我们就直接自定义宏,少说废话,直接开干(为什么要学这个?因为甚至可以使用这个自己写一门语言)
宏的运作机制
Rust编译过程
新建空白宏
创建空白宏的方式很简单,直接使用macro_rules!
进行声明,内部形似模式匹配推断(其实根本就是)
macro_rules! test {
() => {};
}
宏选择器
- item:条目,例如函数、结构、模块等
- block:代码块
- stmt:语句
- pat:模式
- expr:表达式
- ty:类型
- ident:标识符
- path:路径,例如 foo、 ::std::mem::replace, transmute::<_, int>, …
- meta:元信息条目,例如 #[…]和 #![rust macro…] 属性
- tt:词条树
什么是词条树
tt词条树是指Rust编译器使用的一种数据结构,通常用于处理宏(Macro)和代码生成(Code Generation)。
tt指的是"Token Tree",它是由一系列"Token"构成的树形结构。"Token"是编程语言中最基础的语法单元,例如关键字、标识符、运算符、括号等等。而"Token Tree"则是这些"Token"按一定的层次结构排列而成的树。
在Rust语言中,宏通常是使用tt词条树作为输入,它可以让宏定义更加灵活和强大。通过对tt词条树进行递归、遍历和变换,宏可以生成代码,实现元编程(Metaprogramming)的效果。
除了宏之外,Rust编译器还会使用tt词条树来处理一些代码生成工作,例如构建抽象语法树(AST)或者生成代码的中间表示(IR)等等。
宏选择器设置各类入参
我们通过挑选适合的宏选择器,才能对应我们宏接受的参数
实现一个log宏
use std::time::{Instant, SystemTime, UNIX_EPOCH};
macro_rules! log {
($log_name:tt)=>{
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
println!("=======================start-{}=========================",$log_name);
println!("----------------createTime:{:?}",timestamp);
println!("----------------title:{}",$log_name);
println!("========================end-{}========================",$log_name);
};
}
fn main() {
log!("zhangsan");
}
运行重复模式匹配
当我们有多个入参的时候就需要用到这个了,比如println!这个宏,我们可能会传入多个需要打印的内容,如果各个要取个名字,那么这样为什么还要去编写一个统一的,简化的宏呢?
重复模式匹配语法:
($($x:expr),*)=>{}
use std::time::{Instant, SystemTime, UNIX_EPOCH};
macro_rules! eq_judge {
($($left:expr => $right:expr),*)=>{{
$(if $left == $right{
println!("true")
})*
}}
}
fn main() {
eq_judge!(
"hello"=>"hi",
"no"=>"no"
);
}
自定义derive宏(过程宏)
与上面的不一样的是,这个derive宏标注的位置在一般在结构体、enum上
比如:
#[derive(Debug)]
struct a{}
构建项目结构(一定要照着做不然会错)
以下是官方案例,我做了一遍之后重写顺序并强调犯错点,请大家一定要按照顺序做,遇到错误查看我这里写的错误
设置工作空间
首先随便创建一个项目,然后修改toml文件
- hello_macro:声明需要实现的trait
- hello_macro_derive:具体的解析,转化,处理逻辑
- pancakes:主执行包
[workspace]
members=[
"hello_macro","hello_macro_derive","pancakes"
]
创建lib和main
cargo new hello_macro --lib
cargo new hello_macro_derive --lib
cargo new pancakes
结构如下图:
hello_macro
lib.rs
书写需要实现的trait并使用pub暴露
pub trait HelloMacro {
fn hello_macro();
}
hello_macro_derive
添加依赖和激活proc-macro
syn crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。quote 则将 syn 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
proc-macro表示这个cratq是一个proc-macro,增加这个配置以后,这个crate的特性就会发生一些变化,例如,这个crate将只能对外导出内部定义的过程宏,而不能导出内部定义的其他内容。
cargo add syn
cargo add quote
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro=true
[dependencies]
quote = "1.0.26"
syn = "2.0.15"
lib.rs
#[proc_macro_derive(HelloMacro)]
标识只要是结构体、enum上标注#[derive(HelloMacro)]
后就会自动实现HelloMacro这个trait,具体的实现逻辑实际上在impl_hello_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()
}
注意点(请好好读,官网上说的很清楚了,这个地方一定要搞懂)
当用户在一个类型上指定 #[derive(HelloMacro)]时,hello_macro_derive 函数将会被调用。因为我们已经使用 proc_macro_derive 及其指定名称HelloMacro对 hello_macro_derive 函数进行了注解,指定名称HelloMacro就是 trait 名,这是大多数过程宏遵循的习惯。
该函数首先将来自 TokenStream 的 input 转换为一个我们可以解释和操作的数据结构。这正是 syn 派上用场的地方。syn 中的 parse 函数获取一个 TokenStream 并返回一个表示解析出 Rust 代码的 DeriveInput 结构体。以下展示了从字符串 struct Pancakes; 中解析出来的 DeriveInput 结构体的相关部分:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
定义 impl_hello_macro 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 TokenStream。所返回的 TokenStream 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会通过修改后的 TokenStream 获取到我们所提供的额外功能。
当调用 syn::parse 函数失败时,我们用 unwrap 来使 hello_macro_derive 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 proc_macro_derive 函数必须返回 TokenStream 而不是 Result,以此来符合过程宏的 API。这里选择用 unwrap 来简化了这个例子;在生产代码中,则应该通过 panic! 或 expect 来提供关于发生何种错误的更加明确的错误信息
pancakes
添加依赖
这里我们需要依赖我们自己写的lib所以需要用path指明
[package]
name = "pancakes"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro_derive" }
main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
错误
can’t find library marco_t
, rename file to src/lib.rs
or specify lib.path (为什么不能在单项目包里构建)
若你仅仅在一个包中构建,当你添加[lib] proc-macro = true
你会出现以下错误:
Caused by:
can't find library `marco_t`, rename file to `src/lib.rs` or specify lib.path
这说明我们不能把当前的包作为lib,因为是主执行包
原理︰考虑过程宏是在编译一个crate之前,对crate的代码进行加工的一段程序,这段程序也是需要编译后执行的。如果定义过程宏和使用过程宏的代码写在一个crate中,那就陷入了死锁:
要编译的代码首先需要运行过程宏来展开,否则代码是不完整的,没法编译crate.
不能编译crate,crate中的过程宏代码就没法执行,就不能展开被过程宏装饰的代码
can’t use a procedural macro from the same crate that defines it
那假如直接去掉不管这个,你会看到这个错误,意味着你必须将过程宏构建在lib中
自定义类属性宏(个人认为最重要)
类属性宏与自定义派生宏相似,不同的是 derive 属性生成代码,它们(类属性宏)能让你创建新的属性。它们也更为灵活;derive 只能用于结构体和枚举;属性还可以用于其它的项,比如函数
常见于各类框架中!
一个简单的例子
项目包结构
同自定义过程宏
我们需要把正在的解析处理逻辑放在lib下
[workspace]
members=[
"json_marco","json_test"
]
json_marco
添加依赖
[package]
name = "json_marco"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = { version = "2.0.15", features = ["full"] }
编写lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_attribute]
pub fn my_macro(attr:TokenStream,item:TokenStream)->TokenStream{
println!("test");
println!("{:#?}",attr);
println!("{:#?}",item);
item
}
这很简单就是单纯输出一下
json_test
toml映引入
[package]
name = "json_test"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
json_marco={path= "../json_marco"}
main.rs
use json_marco::my_macro;
#[my_macro("test111")]
fn test(a: i32) {
println!("{}", a);
}
fn main() {
test(5);
}
一些例子
base
lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_attribute]
pub fn my_macro(attr: TokenStream, item: TokenStream) -> TokenStream {
// 解析输入的类型
let input = parse_macro_input!(item as DeriveInput);
// 获取类型名
let name = input.ident;
// 构建实现代码
let expanded = quote! {
impl #name {
fn my_function(&self) {
println!("This is my custom function!");
}
}
};
// 将生成的代码转换回 TokenStream 以供返回
TokenStream::from(expanded)
}
main
#[my_macro]
struct MyStruct {
field1: u32,
field2: String,
}
fn main() {
let my_instance = MyStruct { field1: 42, field2: "hello".to_string() };
my_instance.my_function();
}
flaky_test
lib
extern crate proc_macro;
extern crate syn;
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_attribute]
pub fn flaky_test(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = syn::parse_macro_input!(input as syn::ItemFn);
let name = input_fn.sig.ident.clone();
TokenStream::from(quote! {
#[test]
fn #name() {
#input_fn
for i in 0..3 {
println!("flaky_test retry {}", i);
let r = std::panic::catch_unwind(|| {
#name();
});
if r.is_ok() {
return;
}
if i == 2 {
std::panic::resume_unwind(r.unwrap_err());
}
}
}
})
}
main
#[flaky_test::flaky_test]
fn my_test() {
assert_eq!(1, 2);
}
json_parse
lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_attribute]
pub fn serde_json(_args: TokenStream, input: TokenStream) -> TokenStream {
// 将输入解析为 DeriveInput 类型,这是所有 Rust 结构体和枚举的通用 AST
let input = parse_macro_input!(input as DeriveInput);
// 检查这是否是一个结构体,并拿到它的名称、字段列表等信息
let struct_name = input.ident;
let fields = match input.data {
Data::Struct(data_struct) => data_struct.fields,
_ => panic!("'serde_json' can only be used with structs!"),
};
// 生成代码,将结构体转换为 JSON 字符串
let output = match fields {
Fields::Named(fields_named) => {
let field_names = fields_named.named.iter().map(|f| &f.ident);
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!({
#(stringify!(#field_names): self.#field_names,)*
})).unwrap()
}
}
}
}
Fields::Unnamed(fields_unnamed) => {
let field_indices = 0..fields_unnamed.unnamed.len();
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!([
#(self.#field_indices,)*
])).unwrap()
}
}
}
}
Fields::Unit => {
quote! {
impl #struct_name {
pub fn to_json(&self) -> String {
serde_json::to_string(&json!({})).unwrap()
}
}
}
}
};
// 将生成的代码作为 TokenStream 返回
output.into()
}
main.rs
#[serde_json]
struct MyStruct {
name: String,
age: u32,
}
fn main() {
let my_struct = MyStruct {
name: "Alice".to_string(),
age: 25,
};
let json_str = my_struct.to_json();
println!("JSON string: {}", json_str);
}
fn_time
lib
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn run(_args: TokenStream, input: TokenStream) -> TokenStream {
// 将输入解析为函数节点
let input = parse_macro_input!(input as ItemFn);
// 获取函数名称、参数列表等信息
let func_name = &input.ident;
let func_args = &input.decl.inputs;
// 生成代码,在函数开始和结束时分别打印时间戳
let output = quote! {
#input
fn #func_name(#func_args) -> () {
println!("{} started", stringify!(#func_name));
let start = std::time::Instant::now();
let result = #func_name(#func_args);
let end = start.elapsed();
println!("{} finished in {}ms", stringify!(#func_name), end.as_millis());
result
}
};
// 将生成的代码作为 TokenStream 返回
output.into()
}
main
#[run]
fn my_function() -> i32 {
// 模拟一些处理时间
std::thread::sleep(std::time::Duration::from_secs(1));
42
}
fn main() {
let result = my_function();
println!("Result = {}", result);
}