解析 derive(Db)

98
2024/07/03 13:39:05

本章我们将开始实现第一步:解析 derive(Db)

创建项目及导出过程宏

首先,我们创建一个 lib 项目:

接着,我们需要做最重要的一步:导出过程宏。打开 Cargo.toml,增加以下配置:

[lib]
proc-macro = true

加入最需要的3个依赖:

[dependencies]
syn = { version = "2", features = ["extra-traits"] }
quote = "1"
proc-macro2 = "1"

synextra-traits 可以让 syn 的数据实现 Debug,以便于我们调试。开发完成后,可以将此 feature 去除。

由于我们是基于 sqlx 进行 CRUD 的,所以将它加入到开发依赖。同时,我们还需要其它一些常用的开发依赖:

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "chrono"] }

定义名为Db的 dervie

打开 lib.rs ,新建一个 db_derive 函数(函数名随意,只是为了明确语义,我们选择了这个函数名):

// src/lib.rs

#[proc_macro_derive(Db)]
pub fn db_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    println!("{:#?}", input);
    proc_macro::TokenStream::default()
}

这个函数非常简单:接收一个 proc_macro::TokenStream,然后返回一个 proc_macro::TokenStreamproc_macro::TokenStream 可以理解为代码的抽象树,它可以构成合法的 rust 代码。

  • input 参数是指,应用到该 derive 的实体(比如结构体)的完成代码抽象树
  • 返回值是指,如何处理 input ,并根据需要生成新的代码抽象树

注意,要使用 proc_macro::TokenStream,必须加上 #[proc_macro], #[proc_macro_attribute]#[proc_macro_derive]

  • 我们只是简单的打印了实体的代码抽象树:println!("{:#?}", input);
  • 然后返回了一个空的抽象树:proc_macro::TokenStream::default()

三种过程宏

Rust 的过程宏支持三种过程宏,分别是:

  • #[proc_macro]:函数形式的过程宏。使用方式类似我们常用的 println!()以及用声明宏(即使用 macro_rules!)定义的宏。
  • #[proc_macro_attribute]:属性宏。使用方式类似于 #[cfg(test)]
  • #[proc_macro_derive]:Derive 宏。本专题要讲述的内容,使用方式类似于:#[derive(Debug)]。Derive 宏也可以定义属性,所以对于专题来讲,该宏符合我们的需求。

#proc_macro_derive最简单的用法就是 #proc_macro_derive(宏名称),比如我们定义的 #[proc_macro_derive(Db)],是指我们定义了一个名为 Db的 Derive宏,可以这样使用:#[derive(Db)]

使用 Db

在项目根目录下(和src 目录同级)创建一个 examples目录,用于测试我们的宏。首先,创建 examples/ch01-parse-derive.rs 文件,并输入以下内容:

// examples/ch01-parse-derive.rs

use db_derive::Db;

#[derive(Db)]
pub struct User {
    pub id: String,
    pub email: String,
    pub password: String,
    pub nickname: String,
    pub dateline: chrono::DateTime<chrono::Local>,
}

fn main() {}

我们定义了一个 User 结构体,并使用了我们刚刚创建的 Db Derive宏。现在通过下面的命令运行:

cargo test --examples ch01-parse-derive

神奇的一幕发生了,我们看到 User 结构体的抽象树被打印出来了:

TokenStream [
    Ident {
        ident: "pub",
        span: #0 bytes(34..37),
    },
    Ident {
        ident: "struct",
        span: #0 bytes(38..44),
    },
    Ident {
        ident: "User",
        span: #0 bytes(45..49),
    },
    Group {
        delimiter: Brace,
        stream: TokenStream [
            Ident {
                ident: "pub",
                span: #0 bytes(56..59),
            },
            Ident {
                ident: "id",
                span: #0 bytes(60..62),
            },
            Punct {
                ch: ':',
                spacing: Alone,
                span: #0 bytes(62..63),
            },
            Ident {
                ident: "String",
                span: #0 bytes(64..70),
            },
            Punct {
                ch: ',',
                spacing: Alone,
                span: #0 bytes(70..71),
            },
            // ...
        ],
        span: #0 bytes(50..199),
    },
]

你也可以通过创建 tests 目录来进行测试

本章代码位于01/解析derive分支。