域名 AXUM.RS 将于2025年10月到期。我们无意再对其进行续费,我们希望你能够接续这个域名,让更多 AXUM 开发者继续受益。
  • 方案1️⃣AXUM.RS 域名 = 3000
  • 方案2️⃣方案1️⃣ + 本站所有专题原始 Markdown 文档 = 5000
  • 方案3️⃣方案2️⃣ + 本站原始数据库 = 5500
如果你有意接续这份 AXUM 情怀,请与我们取得联系。
说明:
  1. 如果有人购买 AXUM.RS 域名(方案1️⃣),或者该域名到期,本站将启用新的免费域名继续提供服务。
  2. 如果有人购买了 AXUM.RS 域名,且同时购买了内容和/或数据库(方案2️⃣/方案3️⃣),本站将关闭。届时我们或许会以另一种方式与你再相遇。

解析 derive(Db)

创建项目及导出过程宏

cargo new db-derive --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 去除。

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)]

在项目根目录下(和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() {}
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 目录来进行测试

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

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

要查看完整内容,请先登录