您的当前位置:首页正文

Macro宏编程

2024-11-24 来源:个人技术集锦

在 Rust 中宏分为两大类:声明式宏 macro_rules! 和过程宏(派生宏、类属性宏、类函数宏)。println! 、vec! 、assert_eq! 都是相当常用的宏。宏是通过一种代码来生成另一种代码,且可以拥有可变数量的参数,这个展开过程是发生在编译器对代码进行解释之前,并没有运行期的性能损耗。

声明式宏

声明式宏允许我们写出类似 match 的代码,将表达式的结果与多个模式进行匹配,一旦匹配了某个模式,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。例如,一个简单的vec!实现:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

#[macro_export] 注释将宏进行了导出,macro_rules! 进行了宏定义,宏的名称是 vec,而感叹号是在调用的时候才加上。

( $( $x:expr ),* ) 的含义:

1、圆括号 () 将整个宏模式包裹其中。紧随其后的是$() ,包含的是模式 $x:expr,该模式中的 expr 表示会匹配任何 Rust 表达式,并给予该模式一个名称 $x,例如,$x 模式可以跟整数 1 进行匹配,也可以跟字符串 "hello" 进行匹配。

2、$() 之后的逗号说明在 $() 所匹配的代码的后面会有一个可选的逗号分隔符,紧随逗号之后的 * 说明 * 之前的模式会被匹配零次或任意多次,例如使用 vec![1, 2, 3] 来调用该宏时,$x 模式将被匹配三次

3、$() 中的 temp_vec.push() 将根据模式匹配的次数生成对应的代码,当执行let v = vec![1, 2, 3] 时,下面这段生成的代码将替代传入的源代码,替代 vec![1, 2, 3] ,并将返回的值temp_vec赋予给变量v

let v = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

过程宏

过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。输出的代码并不会替换之前的代码,这与声明宏不同。而且创建过程宏时,它的定义必须要放入一个独立的包中。

自定义过程宏

$ cargo new hello_macro
$ cd hello_macro/
$ touch src/lib.rs
$ cargo new hello_macro_derive --lib

//hello_macro 项目的目录结构
hello_macro
├── Cargo.toml
├── src
│   ├── main.rs
│   └── lib.rs
└── hello_macro_derive
    ├── Cargo.toml
    ├── src
        └── lib.rs

$ cat hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
# 也可以使用下面的相对路径
# hello_macro_derive = { path = "./hello_macro_derive" }

$ cat hello_macro/src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Sunfei;

#[derive(HelloMacro)]
struct Sunface;

fn main() {
    Sunfei::hello_macro();
    Sunface::hello_macro();
}

$ cat hello_macro/src/lib.rs  //定义过程宏所需的 HelloMacro特征和其关联函数:
pub trait HelloMacro {
    fn hello_macro();
}

$ cat hello_macro/hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"
$ cat hello_macro/hello_macro_derive/src/lib.rs
extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn;
use syn::DeriveInput;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 基于 input 构建 AST 语法树
    let ast:DeriveInput = syn::parse(input).unwrap();

    // 构建特征实现代码
    impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident; //将结构体的名称赋予给name,
    let gen = quote! {     //使用 quote! 定义我们想要返回的Rust代码。
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name)); //#name 是字面值形式
            }
        }
    };
    gen.into()
}

$ cargo run

  Running `target/debug/hello_macro`
Hello, Macro! My name is Sunfei!
Hello, Macro! My name is Sunface!

为了在项目的 src/main.rs 中引用 hello_macro_derive包的内容,第一种是将 hello_macro_derive 发布到 crates.io 或 GitHub 中,另一种就是使用相对路径引入的本地化方式,即在hello_macro/Cargo.toml文件添加依赖,此时hello_macro 项目就可以成功的引用到 hello_macro_derive 本地包了

在 hello_macro_derive/Cargo.toml 文件中,syn 和 quote 依赖包都是定义过程宏所必需的,同时,还需要在 [lib] 中将过程宏的开关开启 

在 hello_macro_derive/src/lib.rs中,过程宏的格式基本是一样的,只在过程宏的实现impl_hello_macro(&ast)有所区别,其中proc_macro 包是 Rust 自带的,它包含了相关的编译器 API,可以用于读取和操作 Rust 源代码。syn 将字符串形式的 Rust 代码解析为一个 AST 树的数据结构。syn::parse 调用会返回一个 DeriveInput 结构体来代表解析后的 Rust 代码:

DeriveInput {
    // --snip--
    vis: Visibility,
    ident: Ident {
        ident: "Sunfei",
        span: #0 bytes(95..103)
    },
    generics: Generics,
    // Data是一个枚举,分别是DataStruct,DataEnum,DataUnion,这里以 DataStruct 为例
    data: Data(
        DataStruct {
            struct_token: Struct,
            fields: Fields,
            semi_token: Some(
                Semi
            )
        }
    )
}

derive过程宏只能用在struct/enum/union上,多数用在结构体上

在运行之前,可以先用 expand 展开宏,观察是否有错误或符合预期,如果没有需要先安装

$ cargo install cargo-expand

$ cargo expand --bin hello_macro

显示全文