Rust中的过程宏

前言

过程宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)TokenStream到另一个TokenStream的函数,用输出的结果来替换宏调用。

工具准备

安装cargo-expand

安装后,使用cargo expand命令可以打印出应用于当前 crate 的宏扩展结果。

// 安装nightly
rustup toolchain install nightly
// 并设置为默认,或者直接执行该语句会自动下载并设置为默认
rustup default nightly

// 安装cargo-expand
cargo install cargo-expand

// 如果提示link.exe,按提示安装C++

项目准备

项目目录

// 创建新项目
cargo new rust_macro_test

// 创建宏目录
mkdir proc_macro_crate

cd proc_macro_crate

// 初始化项目并创建lib.rs文件
cargo init --lib


项目结构为:

rust_macro_test
├─proc_macro_crate
│  └─src
└─src

注意:

1、过程宏必须定义在一个独立的crate中。不能在一个crate中既定义过程宏,又使用过程宏。

原理:过程宏是在编译一个crate之前,对crate的代码进行加工的一段程序,这段程序也是需要编译后执行的。如果定义过程宏和使用过程宏的代码写在一个crate中,那就陷入了死锁:

  • 要编译的代码首先需要运行过程宏来展开,否则代码是不完整的,没法编译crate。
  • 不能编译crate,crate中的过程宏代码就没法执行,就不能展开被过程宏装饰的代码

2、过程宏必须定义定义在lib目标中,不能定义在bin目标中

引入过程宏三件套

在proc_macro_crate下Cargo.toml添加:

# 表示这个crate是过程宏
[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.7"
quote = "1"
syn = { version = "1.0.56", features = ["full","extra-traits"] } # "extra-traits"方便后续打印调试信息
  • proc_macro2:是对proc_macro的封装,是由 Rust 官方提供的。
  • syn:是基于proc_macro2中暴露的 TokenStream API 来生成 AST,提供方便的操作AST的接口。
  • quote:配合syn,将AST转回TokenSteam,回归到普通文本代码生成的TokenSteam中。

添加过程宏crate依赖

在rust_macro_test下Cargo.toml添加:

[dependencies]
proc_macro_crate = {path="./proc_macro_crate"}

Rust过程宏

属性式过程宏

Attribute macros,用在结构体、字段、函数等地方,为其指定属性等功能。

定义

在lib.rs中加入:

// 属性式过程宏 custom_proc_macro_attribute 为宏的名字
#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(
    attrs: proc_macro::TokenStream, // 过程宏传的属性
    input: proc_macro::TokenStream, // 作用的代码
) -> proc_macro::TokenStream {
    eprintln!("attrs:{:#?}", attrs); // 注意输出要用epringln!
    eprintln!("input:{:#?}", input);
    input

    // 如果返回空对象,相当于去除方法
    // proc_macro::TokenStream::new()
}

使用

在main.rs加入:

use proc_macro_crate::custom_proc_macro_attribute;

fn main() {
    custom_proc_macro_attribute_fn();
    custom_proc_macro_attribute_fn_ha_attribute();
}

// 属性过程宏:空属性
#[custom_proc_macro_attribute]
fn custom_proc_macro_attribute_fn() {
    println!("this is custom_proc_macro_attribute_fn()");
}

// 属性过程宏:自定义属性
#[custom_proc_macro_attribute("this is custom_proc_macro_attribute")]
fn custom_proc_macro_attribute_fn_ha_attribute() {
    println!("this is custom_proc_macro_attribute_fn()");
}


查看宏扩展结果

运行cargo expand

fn main() {
    custom_proc_macro_attribute_fn();
    custom_proc_macro_attribute_fn_ha_attribute();
}
fn custom_proc_macro_attribute_fn() {
    {
        ::std::io::_print(
            ::core::fmt::Arguments::new_v1(
                &["this is custom_proc_macro_attribute_fn()\n"],
                &[],
            ),
        );
    };
}
fn custom_proc_macro_attribute_fn_ha_attribute() {
    {
        ::std::io::_print(
            ::core::fmt::Arguments::new_v1(
                &["this is custom_proc_macro_attribute_fn() with attribute\n"],
                &[],
            ),
        );
    };
}

查看TokenStream输出

运行cargo check或cargo run。

可以看到attrs为属性宏传递的自定义属性,input为方法内容。

// 空属性输出
attrs:TokenStream []
input:TokenStream [
    Ident {
        ident: "fn",
        span: #0 bytes(218..220),
    },
    Ident {
        ident: "custom_proc_macro_attribute_fn",
        span: #0 bytes(221..251),
    },
    Group {
        delimiter: Parenthesis,
        stream: TokenStream [],
        span: #0 bytes(251..253),
    },
    Group {
        delimiter: Brace,
        stream: TokenStream [
            Ident {
                ident: "println",
                span: #0 bytes(260..267),
            },
            Punct {
                ch: '!',
                spacing: Alone,
                span: #0 bytes(267..268),
            },
            Group {
                delimiter: Parenthesis,
                stream: TokenStream [
                    Literal {
                        kind: Str,
                        symbol: "this is custom_proc_macro_attribute_fn()",
                        suffix: None,
                        span: #0 bytes(269..311),
                    },
                ],
                span: #0 bytes(268..312),
            },
            Punct {
                ch: ';',
                spacing: Alone,
                span: #0 bytes(312..313),
            },
        ],
        span: #0 bytes(254..315),
    },
]

// 自定义属性输出
attrs:TokenStream [
    Literal {
        kind: Str,
        symbol: "this is custom_proc_macro_attribute",
        suffix: None,
        span: #0 bytes(384..421),
    },
]
input:TokenStream [
    Ident {
        ident: "fn",
        span: #0 bytes(424..426),
    },
    Ident {
        ident: "custom_proc_macro_attribute_fn_ha_attribute",
        span: #0 bytes(427..470),
    },
    Group {
        delimiter: Parenthesis,
        stream: TokenStream [],
        span: #0 bytes(470..472),
    },
    Group {
        delimiter: Brace,
        stream: TokenStream [
            Ident {
                ident: "println",
                span: #0 bytes(479..486),
            },
            Punct {
                ch: '!',
                spacing: Alone,
                span: #0 bytes(486..487),
            },
            Group {
                delimiter: Parenthesis,
                stream: TokenStream [
                    Literal {
                        kind: Str,
                        symbol: "this is custom_proc_macro_attribute_fn() with attribute",
                        suffix: None,
                        span: #0 bytes(488..545),
                    },
                ],
                span: #0 bytes(487..546),
            },
            Punct {
                ch: ';',
                spacing: Alone,
                span: #0 bytes(546..547),
            },
        ],
        span: #0 bytes(473..549),
    },
]

进阶

修改定义中的代码,使用synquote库来将TokenStream转化为语法树,再对语法树进行处理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, Item};

#[proc_macro_attribute]
pub fn custom_proc_macro_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {
    // 使用parse_macro_input!宏将attr转换为语法树
    eprintln!("{:#?}", parse_macro_input!(attr as AttributeArgs));
    // 转换成语法树
    let body_ast = parse_macro_input!(item as Item);
    // eprintln!("{:#?}", body_ast);

    match body_ast {
        // 获取到函数及相关属性
        Item::Fn(ref o) => {
            eprintln!("o:{:#?}", o);
            eprintln!("vis:{:#?}", o.vis); // 可见性
            eprintln!("attrs:{:#?}", o.attrs); // 特性
            eprintln!("sig:{:#?}", o.sig); // 方法签名
            eprintln!("block:{:#?}", o.block); // 方法体
        }
        _ => {}
    }

    // quote!宏将语法树重新转换为TokenStream
    // 返回的实际结果为proc_macro2::TokenStream,要into转换为proc_macro::TokenStream
    quote!(#body_ast).into()

    // 返回空内容,将清楚函数内容
    // quote!({}).into()
}

以上列出了部分属性的获取,更多属性可以点击类型进去查看。

派生式过程宏

Derive macro,用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。

定义

// 派生式过程宏 CuctomProcMacroDerive 为宏名称
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    eprint!("cuctom_proc_macro_derive:{:#?}", input);   
    
    // 派生一个叫cuctom_proc_macro_derive_fn的方法
    "fn cuctom_proc_macro_derive_fn() -> i32 {100}"
        .parse()
        .unwrap()
    
    // 返回空代表没派生内容
    // proc_macro::TokenStream::new()
}

使用

#[derive(CuctomProcMacroDerive)]
struct Student;

fn main() {
    // 派生宏中派生的新方法,可以直接使用。
    cuctom_proc_macro_derive_fn();
}

查看宏扩展结果

可以看到展开后有了自定义的方法cuctom_proc_macro_derive_fn,这个方法我们可以直接使用。

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
struct Student;
fn cuctom_proc_macro_derive_fn() -> i32 {
    100
}
fn main() {
    cuctom_proc_macro_derive_fn();
}

查看TokenStream输出结果

cuctom_proc_macro_derive:TokenStream [
    Ident {
        ident: "struct",
        span: #0 bytes(59..65),
    },
    Ident {
        ident: "Student",
        span: #0 bytes(66..73),
    },
    Punct {
        ch: ';',
        spacing: Alone,
        span: #0 bytes(73..74),
    },
]

进阶

修改定义中的代码,使用synquote库来将TokenStream转化为语法树,再对语法树进行处理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

// 派生式过程宏 CuctomProcMacroDerive 为宏名称
#[proc_macro_derive(CuctomProcMacroDerive)]
pub fn cuctom_proc_macro_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // 转换成语法树,注意这里的类型为DeriveInput
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident; // 名称
    let attrs = input.attrs; // 特性
    let vis = input.vis; // 可见性
    let generics = input.generics; // 泛型
    let data = input.data; // 数据

    // 生成输出
    let expanded = quote! {
      fn cuctom_proc_macro_derive_fn() -> i32 {100}

      impl Trait for #name {
        fn print(&self) -> usize {
            println!("{}","hello from #name")
        }
      }
    };

    // 返回TokenStream
    expanded.into()
    // 等同于
    // proc_macro::TokenStream::from(expanded)
}

以上列出了部分属性的获取,更多属性可以点击类型进去查看。

函数式过程宏

Function-like macro,用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。

定义

// 函数式过程宏 custom_proc_macro 为宏名称
#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    eprint!("custom_proc_macro:{:#?}", input);
    "fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()

    // proc_macro::TokenStream::new()
}

使用

fn main() {
    custom_proc_macro!();
    custom_proc_macro!(123);    

    // 宏中定义的函数可直接使用
    custom_proc_macro_fn();
}

查看宏扩展结果

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use proc_macro_crate::*;
fn main() {
    fn custom_proc_macro_fn() -> i32 {
        100
    }
    custom_proc_macro_fn();
}

查看TokenStream输出结果

可以看到input内容为使用时传入的内容。

custom_proc_macro:TokenStream []
custom_proc_macro:TokenStream [
    Literal {
        kind: Integer,
        symbol: "123",
        suffix: None,
        span: #0 bytes(233..236),
    },
]

进阶

修改定义中的代码,使用synquote库来将TokenStream转化为语法树,再对语法树进行处理。

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro]
pub fn custom_proc_macro(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // eprint!("custom_proc_macro:{:#?}", input);
    // "fn custom_proc_macro_fn() -> i32 {100}".parse().unwrap()
    quote!({
        fn custom_proc_macro_fn() -> i32 {
            100
        }
    })
    .into()
}

总结

本文先介绍了项目搭建以及过程宏的定义和简单使用及进阶使用。可以看出原始代码都被解析成TokenStream类型的树形结构数据,但如果我们想对TokenStream做一些处理,去直接解析、修改这样的结构是非常复杂的。我们可以使用syn库来将TokenStream转化为具有语义信息、抽象程度更高的语法树,再对语法树进行处理,处理完成后再使用quote返回TokenStream

posted @ 2022-12-10 17:15  gaozejie  阅读(1185)  评论(0编辑  收藏  举报