用 Rust 的 declarative macro 做了個小東西

2023-09-12 15:02:36

最近幾天在弄 ddnspod 的時候,寫了個宏: custom_meta_struct

解決什麼問題

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct ActionA {
    url: String, // https://example.com
    version: String, // v1.2.3
    a: u64,
    // ...
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[some custome attribute] // 這個 action 獨有 attribute
struct ActionB {
    url: String, // https://example.com
    version: String, // v1.2.3
    b: bool,
    // ...
}

// 後面很多的 Action 
// ...

上面程式碼中有很多個 struct Action

每一個 Action 都有一些像 #[derive(Debug)] 這樣的共同的 Attributes

每個 struct 內同樣也都有像 url version 這樣相同的 fields

並且大部分的值都相同, 此時我該如何利用 macro 來減少重複程式碼的編寫?

custom_meta_struct! {}

我的 custom_meta_struct 就是專門來幹這個活兒的

簡單用法

custom_meta_struct! {
	(
    	#[derive(Debug)]
        #[derive(Clone)]
    ),

    struct A;

    #[derive(Copy)]
    struct B;
}

這段程式碼展開後會變成這樣:

#[derive(Debug)]
#[derive(Clone)]
struct A;

#[derive(Debug)]
#[derive(Clone)]
#[derive(Copy)]
struct B;

複雜點的用法

對於 url version 也避免重複的用法:

首先定一個 trait

trait CommonParams {
	fn url(&self) -> String { "https://hangj.cnblogs.com" }
    fn version(&self) -> String { "v1.2.3".into() }
}

然後讓所有的 Action 都

impl CommonParams for ActionX {
	// 如果這個 Action 的 url 或 version 比較特殊, 就過載一下
}

具體解法:

custom_meta_struct! {
	(
    	define_structs, // callback macro
        #[derive(Debug)]
    ),

    #[derive(Clone)]
    struct A;

	@[version = "v2.3.4".into()]
    #[derive(serde::Serialize)]
    struct B;

    @[url = "https://crates.io/crates/ddnspod".into()]
    struct C;
}

其中的 define_structs 也是一個宏, 用來作為回撥, custom_meta_struct 會對將要展開的程式碼做一個格式化, 程式碼格式化之後傳遞給 define_structs

@[..] 是我們的自定義屬性, 用來輔助實現 trait CommonParams 內函數過載的

接下來看具體實現:

macro_rules! define_structs {
	(
    	$(
        	$(#[$meta: meta])*
            $(@[$($my_meta: tt)*])*
            $vis: vis struct $name: ident $body: tt
        )*
    ) => {
    	$(
            $(#[$meta])*
            $vis struct $name $body

            impl CommonParams for $name {
                $(
                	overriding_method!( $($my_meta)* );
                )*
            }
        )*
    };
}

overriding_method 也是一個宏:

macro_rules! overriding_method {
	(url = $expr: expr) => {
    	fn url(&self) -> String { $expr }
    };
	(version = $expr: expr) => {
        fn version(&self) -> String { $expr }
    };
    ($($tt: tt)*) => {
        compile_error!("This macro only accepts `url` and `version`");
    };
}

經過這一系列操作, 就完美解決了最前面的問題

完整範例程式碼

trait CommonParams {
	fn url(&self) -> String { "https://hangj.cnblogs.com" }
    fn version(&self) -> String { "v1.2.3".into() }
}

macro_rules! overriding_method {
	(url = $expr: expr) => {
    	fn url(&self) -> String { $expr }
    };
	(version = $expr: expr) => {
        fn version(&self) -> String { $expr }
    };
    ($($tt: tt)*) => {
        compile_error!("This macro only accepts `url` and `version`");
    };
}

macro_rules! define_structs {
	(
    	$(
        	$(#[$meta: meta])*
            $(@[$($my_meta: tt)*])*
            $vis: vis struct $name: ident $body: tt
        )*
    ) => {
    	$(
            $(#[$meta])*
            $vis struct $name $body

            impl CommonParams for $name {
                $(
                	overriding_method!{ $($my_meta)* }
                )*
            }
        )*
    };
}

custom_meta_struct! {
	(
    	define_structs, // callback macro
        #[derive(Debug)]
    ),

    #[derive(Clone)]
    struct A;

	@[version = "v2.3.4".into()]
    #[derive(serde::Serialize)]
    struct B;

    @[url = "https://crates.io/crates/ddnspod".into()]
    struct C;
}

被展開後:

#[derive(Debug)]
#[derive(Clone)]
struct A;

impl CommonParams for A {}

#[derive(Debug)]
#[derive(serde::Serialize)]
struct B;

impl CommonParams for B {
    fn version(&self) -> String { "v2.3.4".into() }
}

#[derive(Debug)]
struct C;

impl CommonParams for C {
    fn url(&self) -> String { "https://crates.io/crates/ddnspod".into() }
}

最後

custom_meta_struct 的程式碼有 300 行左右, 花了我好多精力

要想編寫出符合預期且行為複雜的 declarative macro 還是挺有挑戰性的, 但是寫完之後很有成就感 ✌️✌️

如果你想了解更多細節,不妨直接看程式碼 https://github.com/hangj/dnspod-lib/tree/main/src/macros

Have fun!