最近幾天在弄 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! {
(
#[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!