最近几天在弄 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!