Rust 过程宏之derive派生宏入门

需求:

获取枚举类型附加信息,如备注说明,传统作法如下:

实现 comment 方法根据匹配值返回对应字符串

enum Robot {
      Alex,
      Bob
}

impl Robot {
      fn comment(&self) -> &'static str {
            match self {
                  Robot::Alex => "这是Alex",
                  Robot::Bob => "这是Bob",
            }
      }
}

// test
println!("alex comment: {}", Robot::Alex.comment()); // alex comment: 这是Alex

传统作法有一个问题,如果有很多枚举,就需要各自实现对应的 comment 方法实现,很麻烦,是否可以简化呢?

可以的!

其中 comment 方法的实现就属于模板代码。这种重复性的工作就可以考虑使用宏,最终简化如下:

除了 comment 也可以附加 其它信息如 姓名、年龄等

#[derive(HelloMacro)]
enum Robot {
    #[oic_column(name = "alex_name", age = 22, comment = "这是Alex")]
    Alex,
    #[oic_column(name = "bob_name", age = 50, comment = "这是Bob")]
    Bob,
}

// 测试可以获取一样的结果
println!("alex comment: {}", Robot::Alex.comment()); // alex comment: 这是Alex

什么是过程宏

过程宏中文文档参考: https://zjp-cn.github.io/rust-note/proc/quote.html

过程宏(Procedure Macro) 是Rust中的一种特殊形式的宏,提供比普通宏更强大的功能。过程宏主要分三类:

  • 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
  • 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的#[inline]、#[derive(...)]等都是属性宏。
  • 函数式宏(Function-like macro):用法与普通的规则宏类似,但功能更加强大,可实现任意语法树层面的转换功能。

1.派生宏示例:

#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

其使用方法如下:

#[derive(Builder)]
struct Command {
    // ...
}

2.属性宏示例

#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;
    let _ = input;

    unimplemented!()
}

其使用方法如下:

#[sorted]
enum Letter {
    A,
    B,
    C,
    // ...
}

3.函数式宏示例

#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

其使用方法如下:

seq! { n in 0..10 {
    /* ... */
}}

功能实现

本例主要使用派生宏实现

项目创建可以参数示例仓库https://github.com/imbolc/rust-derive-macro-guide

第一步 创建测试项目

测试项目: my-macro-test
宏项目: my-derives

项目结构最终如下:

my-macro-test/
      my-derives/             // 子项目
            src/
                  attributes.rs
                  lib.rs        
            Cargo.toml
      src/
            main.rs             // 主项目入口
      Cargo.toml

文件 my-macro-test/Cargo.toml

# my-macro-test/Cargo.toml
[package]
name = "my-macro-test"
version = "0.1.0"
edition = "2021"

[dependencies]
# 根据路径指定子项目
my_derives = { path = "my-derives" }

文件 my-macro-test/my-derives/Cargo.toml

[package]
# my-macro-test/my-derives/Cargo.toml
name = "my-derives"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = { version = "^1", features = ["full"] }
quote = "^1"
proc-macro2 = "^1"
bae = "^0"

重要的第三方库:

  • quote 把 Rust 语法树的数据结构转化为源代码的标记 (tokens)
  • syn 主要是一个解析库,用于把 Rust 标记流解析为 Rust 源代码的语法树
  • bae 简化属性数据的处理

第二步 自定义属性定义

文件: my-derives/attributes.rs

// my-derives/attributes.rs
use bae::FromAttributes;
use syn;

#[derive(Default, FromAttributes, Debug)]
pub struct OicColumn {
    pub name: Option<syn::Lit>,
    pub age: Option<syn::Lit>,
    pub comment: Option<syn::Lit>,
}

第三步 HelloMacro 派生宏实现

文件: my-derives/lib.rs

mod attributes;

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

// HelloMacro 定义
#[proc_macro_derive(HelloMacro, attributes(oic_column))]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let input: DeriveInput = parse_macro_input!(input);
    // ident 当前枚举名称
    let DeriveInput { ident, .. } = input;

    let mut comment_arms = Vec::new();

    if let syn::Data::Enum(syn::DataEnum { variants, .. }) = input.data {
        for variant in variants {
            // 当前枚举项名称如 Alex, Box
            let ident_item = &variant.ident;
            // 根据属性值转为 OicColumn 定义的结构化数据
            if let Ok(column) = OicColumn::from_attributes(&variant.attrs) {
                // 获取属性中的comment信息
                let msg: &syn::Lit = &column.comment.unwrap();

                // 生成 match 匹配项 Robot::Alex => "msg"
                comment_arms.push(quote! ( #ident::#ident_item => #msg ));
            } else {
                comment_arms.push(quote! ( #ident::#ident_item => "" ));
            }
        }
    }

    if comment_arms.is_empty() {
        comment_arms.push(quote! ( _ => "" ));
    }

    // 实现 comment 方法
    let output = quote! {
        impl #ident {
            fn comment(&self) -> &'static str {
                match self {
                    #(#comment_arms),*
                }
            }
        }
    };
    output.into()
}

#(#comment_arms),* 为数据解构语法,具体参考:https://docs.rs/quote/1.0.21/quote/macro.quote.html

第四步 测试代码实现

文件: my-macro-test/main.rs

use my_derives::HelloMacro;

#[derive(HelloMacro)]
enum Robot {
    #[oic_column(name = "alex_name", age = 22, comment = "这是Alex")]
    Alex,
    #[oic_column(name = "bob_name", age = 50, comment = "这是Bob")]
    Bob,
    Apple,
}

fn main() {
    // test comment: Alex: "这是Alex", ----- Bob: "这是Bob"
    println!("test comment: Alex: {:?}, ----- Bob: {:?}", Robot::Alex.comment(), Robot::Bob.comment());
    // test comment apple: ""
    println!("test comment apple: {:?}", Robot::Apple.comment());
}

cargo run 运行测试输出结果如注释部分。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容