← 所有文章

rust宏魔法(1)

Rust宏魔法——第一辑(不知道后面还会不会更新,总之这是个系列)

注:本文默认你已经有一定的Rust基础

rust的宏的强大相信大家肯定听的不少,本篇就简单说说rust的过程宏

rust的宏有好几种,有基于模式匹配的,还有可以直接对语法树动手的,过程宏就是可以修改语法树,可以在编译器派生代码:比如有个test crate,然后它依赖于test_macro crate,而后者是一个proc-macro库,也就是里面定义了过程宏,编译时就会先编译test_macro然后用test_macro的对test的语法解析流修改再丢给编译器,所以可以实现很多非常抽象的操作。

写本文的契机是,前段时间在Rust中文社区群里看到有人问能不能用宏把json生成一个enum(json只有一层),这个需求和过程宏还是很契合的,所以就简单实现了一下,以下内容就是对实现的讲解

首先开个crate吧

cargo new json_to_enum

直到撰写本文时,想要定义过程宏还是必须要单独开个crate,并且在cargo.toml里面设置 lib.proc-macro = true

至于原因是rust的编译单元是crate,而前面也说要拿过程宏的库编译好对其他库使用的,所以就不可避免要分割,单独开个crate。

然后创建过程宏库,这里我一般喜欢把依赖的其他自己写的库放在src下

cd src
cargo new --lib json_to_enum_macro
cd ..

然后创建一个json文件用于测试,内容比如:

// test.json
{
    "app_name_not_set": "应用名称未设置",
    "app_name_too_long": "应用名称过长",
    "app_name_invalid": "应用名称无效",
    "app_name_already_exists": "应用名称已存在"
}

现在的文件结构应该是:

.
├─src
│  ├─main.rs
│  └─json_to_enum_macro
│      └─src
│ 		  └─lib.rs
└─test.json

emm,先展示一下调用的效果吧

use json_to_enum_macro::json_to_enum;

json_to_enum! {
    #[from_json("test.json")]
    pub enum Test {}
}

fn main() {
    for i in 0..4 {
        let t = unsafe { std::mem::transmute::<u8, Test>(i) };
        println!("{:?} {}", t, t);
    }
}
// output
/*
AppNameAlreadyExists "应用名称已存在"
AppNameInvalid "应用名称无效"
AppNameNotSet "应用名称未设置"
AppNameTooLong "应用名称过长"
*/

json_to_enum宏里面的匹配规则是一看到这个需求时的想法,所以别问我为什么要这样写()

里面塞一个空的enum也是方便指定这个解析出的enum的名字而已,最终的效果是会替换掉enum的内容,所以里面写什么内容都没有用

好了,现在开始正式编写!

开始编写你的proc-macro

先在json_to_enum_macro的cargo.toml里面设置 lib.proc-macro = true

然后导入proc-macro的依赖 quote,syn,以及用于解析json文件的serde_json库

cargo add quote syn serde_json

现在文件应该是这样

[package]
name = "json_to_enum_macro"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
quote = "1.0.36"
serde_json = "1.0.116"
syn = "2.0.59"

把这个库添加到json_to_enum的依赖

[workspace]
members = [
    "src/json_to_enum_macro"
]

[package]
name = "json_to_enum"
version = "0.1.0"
edition = "2021"

[dependencies]

[dependencies.json_to_enum_macro]
path = "src/json_to_enum_macro"

接下来rust-analyser就会一起解析你的macro库了

打开lib.rs文件导入依赖

extern crate proc_macro;
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Ident, Lit};

一般来说基本都会用上这几个:proc_macro::TokenStream, quote::quote, syn::{parse_macro_input, DeriveInput}

proc macro也有好几种,比如 proc_macro_derive 可以给enum,struct啥的实现某个特征, 使用方法比如: #[derive(Debug)] struct DebugStruct;, 比如 proc_macro_attribute 可以扩展函数,enum字段等的功能。

而这里由于我们连enum都要派生,上面说的两者都要依托于某个对象,所以我们这里也就采用proc_macro

#[proc_macro]
pub fn json_to_enum(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    
    let gen = quote! {
        
    };
    gen.into()
}

我们要先解析一个attribute,得到json文件的路径,比如这个attribute是 #[from_json("test.json")]

// 由于可以有很多个attribute,所以这里返回的是一个Vec
let attribute = &input.attrs;
// 解析出attr里提供的json文件路径
let json_name = attribute
	.iter()
	.find_map(|attr| {
        if attr.path().is_ident("from_json") {
            // 尝试把attr解析成字面量(literal)
            let lit = match attr.parse_args::<Lit>() {
                Ok(lit) => lit,
                Err(e) => panic!("{:?}", e),
            };
            if let Lit::Str(lit) = lit {
                Some(lit.value())
            } else {
                None
            }
        } else {
            None
        }
	})
	.unwrap();
// 读取文件内容
let json_content = std::fs::read_to_string(json_name).expect("file not found");
// 解析文件内容
let json: serde_json::Value = serde_json::from_str(&json_content).expect("json parse error");

然后获取你传入的enum的名字(标识符),和json的keys就可以派生enum了

let name = &input.ident;
let variants = json
    .as_object()
    .expect("json should be object")
    .keys()
    .map(|k| {
        // json里面是snake命名,转成驼峰式命名
        let ident = Ident::new(&snake_to_camel(k), Span::call_site().into());
        quote! {
            #ident,
        }
    });

这时候就可以像这样派生一个enum

是不是感觉和写macro_rules!很像呢

quote! {
    pub enum #name {
        *(#variants)*
    }
}

然后还要实现一下Display,需求说的是要json字段对应的值(也就是那些中文),这也显然只要获取json的values即可

let msgs = json.as_object().unwrap().values().map(|v| {
        let msg = v.as_str().expect("value should be string");
        quote! {
            #msg
        }
    });

后面实现Display,(我也顺便实现了Debug)也就跟上面一样

impl std::fmt::Display for #name {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            #(
                #name::#keys => write!(f, "{}", stringify!(#msgs)),
            )*
        }
    }
}

到这里就已经完成了,如果你不知道后面这部分怎么弄就好好理解一下前面的代码,我故意没有全都写出来()

实现完后记得测试一下

use json_to_enum_macro::json_to_enum;

json_to_enum! {
    #[from_json("test.json")]
    pub enum Test {}
}

fn main() {
    let a = Test::AppNameNotSet;
    assert_eq!(a.to_string(), "应用名称未设置");
}

能正常跑就基本没啥问题了,还担心就展开看看

cargo expand

展开结果:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use parser_macro::json_to_enum;
pub enum Test {
    AppNameAlreadyExists,
    AppNameInvalid,
    AppNameNotSet,
    AppNameTooLong,
}
impl std::fmt::Display for Test {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Test::AppNameAlreadyExists => {
                f.write_fmt(
                    format_args!(
                        "{0}",
                        "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{5df2}\u{5b58}\u{5728}\"",
                    ),
                )
            }
            Test::AppNameInvalid => {
                f.write_fmt(
                    format_args!(
                        "{0}",
                        "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{65e0}\u{6548}\"",
                    ),
                )
            }
            Test::AppNameNotSet => {
                f.write_fmt(
                    format_args!(
                        "{0}",
                        "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{672a}\u{8bbe}\u{7f6e}\"",
                    ),
                )
            }
            Test::AppNameTooLong => {
                f.write_fmt(
                    format_args!(
                        "{0}",
                        "\"\u{5e94}\u{7528}\u{540d}\u{79f0}\u{8fc7}\u{957f}\"",
                    ),
                )
            }
        }
    }
}
impl std::fmt::Debug for Test {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Test::AppNameAlreadyExists => {
                f.write_fmt(format_args!("{0}", "AppNameAlreadyExists"))
            }
            Test::AppNameInvalid => f.write_fmt(format_args!("{0}", "AppNameInvalid")),
            Test::AppNameNotSet => f.write_fmt(format_args!("{0}", "AppNameNotSet")),
            Test::AppNameTooLong => f.write_fmt(format_args!("{0}", "AppNameTooLong")),
        }
    }
}
fn main() {
    let a = Test::AppNameNotSet;
    match (&a.to_string(), &"应用名称未设置") {
        (left_val, right_val) => {
            if !(*left_val == *right_val) {
                let kind = ::core::panicking::AssertKind::Eq;
                ::core::panicking::assert_failed(
                    kind,
                    &*left_val,
                    &*right_val,
                    ::core::option::Option::None,
                );
            }
        }
    };
}
· · ·

感谢阅读。愿这份记录也能帮到你。

继续探索其他文章 ↗